mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 08:46:37 -04:00
Merge branch 'master' into ref
This commit is contained in:
commit
020fa0b2f8
55 changed files with 2655 additions and 743 deletions
|
@ -20,11 +20,13 @@ jobs:
|
|||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt install portaudio19-dev
|
||||
pip install -r requirements.qt
|
||||
- name: Set up Protoc
|
||||
uses: Arduino/actions/setup-protoc@master
|
||||
- name: Run checks
|
||||
run: |
|
||||
make check
|
||||
|
||||
sudo apt install portaudio19-dev
|
||||
python${{ matrix.python-version }} -m venv ~/pyenv
|
||||
. ~/pyenv/bin/activate
|
||||
pip install -r requirements.qt
|
||||
pip install --upgrade setuptools pip
|
||||
make check RUSTARGS=""
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -3,15 +3,18 @@
|
|||
*\#
|
||||
*~
|
||||
.*.swp
|
||||
.DS_Store
|
||||
.build
|
||||
.coverage
|
||||
.DS_Store
|
||||
.mypy_cache
|
||||
.pytype
|
||||
__pycache__
|
||||
anki/buildhash.py
|
||||
anki/backend_pb2.*
|
||||
aqt/forms
|
||||
locale
|
||||
rs/ankirs/src/backend_proto.rs
|
||||
rs/target
|
||||
tools/runanki.system
|
||||
ts/node_modules
|
||||
web/deckbrowser.js
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[settings]
|
||||
skip=aqt/forms
|
||||
skip=aqt/forms,anki/backend_pb2.py,backend_pb2.pyi
|
||||
multi_line_output=3
|
||||
include_trailing_comma=True
|
||||
force_grid_wrap=0
|
||||
|
|
165
Makefile
165
Makefile
|
@ -1,13 +1,14 @@
|
|||
PREFIX := /usr
|
||||
SHELL := bash
|
||||
.SHELLFLAGS := -eu -o pipefail -c
|
||||
.ONESHELL:
|
||||
.DELETE_ON_ERROR:
|
||||
MAKEFLAGS += --warn-undefined-variables
|
||||
MAKEFLAGS += --no-builtin-rules
|
||||
RUNARGS :=
|
||||
.SUFFIXES:
|
||||
BLACKARGS := -t py36 anki aqt
|
||||
BLACKARGS := -t py36 anki aqt tests
|
||||
RUSTARGS := --release --strip
|
||||
ISORTARGS := anki aqt tests
|
||||
|
||||
$(shell mkdir -p .build)
|
||||
|
||||
|
@ -40,6 +41,8 @@ install:
|
|||
-xdg-mime default anki.desktop application/x-apkg
|
||||
@echo
|
||||
@echo "Install complete."
|
||||
# fixme: _ankirs.so needs to be copied into system python env or
|
||||
# 'maturin build' used
|
||||
|
||||
uninstall:
|
||||
rm -rf ${DESTDIR}${PREFIX}/share/anki
|
||||
|
@ -55,20 +58,48 @@ uninstall:
|
|||
# Prerequisites
|
||||
######################
|
||||
|
||||
RUNREQS := .build/pyrunreqs .build/jsreqs
|
||||
RUNREQS := .build/py-run-deps .build/ts-deps
|
||||
|
||||
.build/pyrunreqs: requirements.txt
|
||||
# Python prerequisites
|
||||
######################
|
||||
|
||||
.build/py-run-deps: requirements.txt
|
||||
pip install -r $<
|
||||
touch $@
|
||||
@touch $@
|
||||
|
||||
.build/pycheckreqs: requirements.check .build/pyrunreqs
|
||||
.build/py-check-reqs: requirements.check .build/py-run-deps
|
||||
pip install -r $<
|
||||
./tools/typecheck-setup.sh
|
||||
touch $@
|
||||
@touch $@
|
||||
|
||||
.build/jsreqs: ts/package.json
|
||||
# TS prerequisites
|
||||
######################
|
||||
|
||||
.build/ts-deps: ts/package.json
|
||||
(cd ts && npm i)
|
||||
touch $@
|
||||
@touch $@
|
||||
|
||||
# Rust prerequisites
|
||||
######################
|
||||
|
||||
.build/rust-deps: .build/py-run-deps
|
||||
pip install maturin
|
||||
@touch $@
|
||||
|
||||
RUST_TOOLCHAIN := $(shell cat rs/rust-toolchain)
|
||||
|
||||
.build/rs-fmt-deps:
|
||||
rustup component add rustfmt-preview --toolchain $(RUST_TOOLCHAIN)
|
||||
@touch $@
|
||||
|
||||
.build/rs-clippy-deps:
|
||||
rustup component add clippy-preview --toolchain $(RUST_TOOLCHAIN)
|
||||
@touch $@
|
||||
|
||||
# Protobuf
|
||||
######################
|
||||
|
||||
PROTODEPS := $(wildcard proto/*.proto)
|
||||
|
||||
# Typescript source
|
||||
######################
|
||||
|
@ -76,18 +107,31 @@ RUNREQS := .build/pyrunreqs .build/jsreqs
|
|||
TSDEPS := $(wildcard ts/src/*.ts)
|
||||
JSDEPS := $(patsubst ts/src/%.ts, web/%.js, $(TSDEPS))
|
||||
|
||||
# Rust source
|
||||
######################
|
||||
|
||||
RSDEPS := $(shell find rs -type f | egrep -v 'target|/\.|proto.rs')
|
||||
|
||||
# Building
|
||||
######################
|
||||
|
||||
BUILDDEPS := .build/ui .build/js
|
||||
BUILDDEPS := .build/ui .build/js .build/rs .build/py-proto
|
||||
|
||||
.build/ui: $(RUNREQS) $(shell find designer -type f)
|
||||
./tools/build_ui.sh
|
||||
touch $@
|
||||
@touch $@
|
||||
|
||||
.build/js: .build/jsreqs $(TSDEPS)
|
||||
.build/js: .build/ts-deps $(TSDEPS)
|
||||
(cd ts && npm run build)
|
||||
touch $@
|
||||
@touch $@
|
||||
|
||||
.build/rs: .build/rust-deps $(RUNREQS) $(RSDEPS) $(PROTODEPS)
|
||||
(cd rs/pymod && maturin develop $(RUSTARGS))
|
||||
@touch $@
|
||||
|
||||
.build/py-proto: $(RUNREQS) $(PROTODEPS)
|
||||
protoc --proto_path=proto --python_out=anki --mypy_out=anki proto/backend.proto
|
||||
@touch $@
|
||||
|
||||
.PHONY: build clean
|
||||
|
||||
|
@ -97,6 +141,7 @@ build: $(BUILDDEPS)
|
|||
clean:
|
||||
rm -rf .build
|
||||
rm -rf $(JSDEPS)
|
||||
rm -rf rs/target
|
||||
|
||||
# Running
|
||||
######################
|
||||
|
@ -109,61 +154,89 @@ run: build
|
|||
######################
|
||||
|
||||
.PHONY: check
|
||||
check: mypy pyimports pyfmt pytest pylint checkpretty
|
||||
check: rs-test rs-fmt rs-clippy py-mypy py-test py-fmt py-imports py-lint ts-fmt
|
||||
|
||||
.PHONY: fix
|
||||
fix: fix-py-fmt fix-py-imports fix-rs-fmt fix-ts-fmt
|
||||
|
||||
# Checking python
|
||||
######################
|
||||
|
||||
PYCHECKDEPS := $(BUILDDEPS) .build/pycheckreqs $(shell find anki aqt -name '*.py' | grep -v buildhash.py)
|
||||
PYCHECKDEPS := $(BUILDDEPS) .build/py-check-reqs $(shell find anki aqt -name '*.py' | grep -v buildhash.py)
|
||||
PYTESTDEPS := $(wildcard tests/*.py)
|
||||
|
||||
.build/mypy: $(PYCHECKDEPS)
|
||||
.build/py-mypy: $(PYCHECKDEPS)
|
||||
mypy anki aqt
|
||||
touch $@
|
||||
@touch $@
|
||||
|
||||
.build/pytest: $(PYCHECKDEPS) $(wildcard tests/*.py)
|
||||
.build/py-test: $(PYCHECKDEPS) $(PYTESTDEPS)
|
||||
./tools/tests.sh
|
||||
touch $@
|
||||
@touch $@
|
||||
|
||||
.build/pylint: $(PYCHECKDEPS)
|
||||
pylint -j 0 --rcfile=.pylintrc -f colorized --extension-pkg-whitelist=PyQt5 anki aqt
|
||||
touch $@
|
||||
.build/py-lint: $(PYCHECKDEPS)
|
||||
pylint -j 0 --rcfile=.pylintrc -f colorized --extension-pkg-whitelist=PyQt5,_ankirs anki aqt
|
||||
@touch $@
|
||||
|
||||
.build/pyimports: $(PYCHECKDEPS)
|
||||
isort anki aqt --check # if this fails, run 'make fixpyimports'
|
||||
touch $@
|
||||
.build/py-imports: $(PYCHECKDEPS) $(PYTESTDEPS)
|
||||
isort $(ISORTARGS) --check # if this fails, run 'make fix-py-imports'
|
||||
@touch $@
|
||||
|
||||
.build/pyfmt: $(PYCHECKDEPS)
|
||||
black --check $(BLACKARGS) # if this fails, run 'make fixpyfmt'
|
||||
touch $@
|
||||
.build/py-fmt: $(PYCHECKDEPS) $(PYTESTDEPS)
|
||||
black --check $(BLACKARGS) # if this fails, run 'make fix-py-fmt'
|
||||
@touch $@
|
||||
|
||||
.PHONY: mypy pytest pylint pyimports pyfmt
|
||||
mypy: .build/mypy
|
||||
pytest: .build/pytest
|
||||
pylint: .build/pylint
|
||||
pyimports: .build/pyimports
|
||||
pyfmt: .build/pyfmt
|
||||
.PHONY: py-mypy py-test py-lint py-imports py-fmt
|
||||
py-mypy: .build/py-mypy
|
||||
py-test: .build/py-test
|
||||
py-lint: .build/py-lint
|
||||
py-imports: .build/py-imports
|
||||
py-fmt: .build/py-fmt
|
||||
|
||||
.PHONY: fixpyimports fixpyfmt
|
||||
.PHONY: fix-py-imports fix-py-fmt
|
||||
|
||||
fixpyimports:
|
||||
isort anki aqt
|
||||
fix-py-imports:
|
||||
isort $(ISORTARGS)
|
||||
|
||||
fixpyfmt:
|
||||
fix-py-fmt:
|
||||
black $(BLACKARGS) anki aqt
|
||||
|
||||
# Checking rust
|
||||
######################
|
||||
|
||||
.build/rs-test: $(RSDEPS)
|
||||
(cd rs/ankirs && cargo test)
|
||||
@touch $@
|
||||
|
||||
.build/rs-fmt: .build/rs-fmt-deps $(RSDEPS)
|
||||
(cd rs && cargo fmt -- --check) # if this fails, run 'make fix-rs-fmt'
|
||||
@touch $@
|
||||
|
||||
.build/rs-clippy: .build/rs-clippy-deps $(RSDEPS)
|
||||
(cd rs && cargo clippy -- -D warnings)
|
||||
@touch $@
|
||||
|
||||
.PHONY: rs-test rs-fmt fix-rs-fmt rs-clippy
|
||||
|
||||
rs-test: .build/rs-test
|
||||
rs-fmt: .build/rs-fmt
|
||||
rs-clippy: .build/rs-clippy
|
||||
|
||||
fix-rs-fmt:
|
||||
(cd rs && cargo fmt)
|
||||
|
||||
|
||||
# Checking typescript
|
||||
######################
|
||||
|
||||
TSCHECKDEPS := $(BUILDDEPS) $(TSDEPS)
|
||||
|
||||
.build/checkpretty: $(TSCHECKDEPS)
|
||||
(cd ts && npm run check-pretty) # if this fails, run 'make pretty'
|
||||
touch $@
|
||||
.build/ts-fmt: $(TSCHECKDEPS)
|
||||
(cd ts && npm run check-pretty) # if this fails, run 'make fix-ts-fmt'
|
||||
@touch $@
|
||||
|
||||
.build/pretty: $(TSCHECKDEPS)
|
||||
.PHONY: fix-ts-fmt ts-fmt
|
||||
ts-fmt: .build/ts-fmt
|
||||
|
||||
fix-ts-fmt:
|
||||
(cd ts && npm run pretty)
|
||||
touch $@
|
||||
|
||||
.PHONY: pretty checkpretty
|
||||
pretty: .build/pretty
|
||||
checkpretty: .build/checkpretty
|
||||
|
|
|
@ -18,6 +18,16 @@ To start, make sure you have the following installed:
|
|||
- mpv
|
||||
- lame
|
||||
- npm
|
||||
- your platform's C compiler, eg gcc, Xcode or Visual Studio 2017.
|
||||
- GNU make
|
||||
- protoc v3 (https://github.com/protocolbuffers/protobuf/releases)
|
||||
- rustup (https://rustup.rs/)
|
||||
- pip 19+
|
||||
|
||||
Next, build a Python virtual environment and activate it:
|
||||
|
||||
$ python3 -m venv ~/pyenv
|
||||
$ . ~/pyenv/bin/activate
|
||||
|
||||
If the distro you are using has PyQt5 installed, make sure you have the PyQt5
|
||||
WebEngine module and development tools (eg pyqt5-dev-tools) installed as well.
|
||||
|
@ -49,11 +59,11 @@ Mac users
|
|||
|
||||
You can use homebrew to install some dependencies:
|
||||
|
||||
$ brew install python mpv lame portaudio
|
||||
$ brew install python mpv lame portaudio protobuf npm rustup-init
|
||||
|
||||
Windows users
|
||||
--------------
|
||||
|
||||
The build scripts have not been tested on Windows, and you'll find things
|
||||
easiest if you build Anki using WSL.
|
||||
https://docs.microsoft.com/en-us/windows/wsl/install-win10
|
||||
The build process uses a GNU makefile, so you'll either need to run
|
||||
GNU make via WSL (https://docs.microsoft.com/en-us/windows/wsl/install-win10)
|
||||
or Cygwin, or manually execute the build steps.
|
||||
|
|
72
anki/backend.py
Normal file
72
anki/backend.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
# pylint: skip-file
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
import _ankirs # pytype: disable=import-error
|
||||
|
||||
import anki.backend_pb2 as pb
|
||||
|
||||
from .types import AllTemplateReqs
|
||||
|
||||
|
||||
class BackendException(Exception):
|
||||
def __str__(self) -> str:
|
||||
err: pb.BackendError = self.args[0] # pylint: disable=unsubscriptable-object
|
||||
kind = err.WhichOneof("value")
|
||||
if kind == "invalid_input":
|
||||
return f"invalid input: {err.invalid_input.info}"
|
||||
elif kind == "template_parse":
|
||||
return f"template parse: {err.template_parse.info}"
|
||||
else:
|
||||
return f"unhandled error: {err}"
|
||||
|
||||
|
||||
def proto_template_reqs_to_legacy(
|
||||
reqs: List[pb.TemplateRequirement],
|
||||
) -> AllTemplateReqs:
|
||||
legacy_reqs = []
|
||||
for (idx, req) in enumerate(reqs):
|
||||
kind = req.WhichOneof("value")
|
||||
# fixme: sorting is for the unit tests - should check if any
|
||||
# code depends on the order
|
||||
if kind == "any":
|
||||
legacy_reqs.append((idx, "any", sorted(req.any.ords)))
|
||||
elif kind == "all":
|
||||
legacy_reqs.append((idx, "all", sorted(req.all.ords)))
|
||||
else:
|
||||
l: List[int] = []
|
||||
legacy_reqs.append((idx, "none", l))
|
||||
return legacy_reqs
|
||||
|
||||
|
||||
class Backend:
|
||||
def __init__(self):
|
||||
self._backend = _ankirs.Backend()
|
||||
|
||||
def _run_command(self, input: pb.BackendInput) -> pb.BackendOutput:
|
||||
input_bytes = input.SerializeToString()
|
||||
output_bytes = self._backend.command(input_bytes)
|
||||
output = pb.BackendOutput()
|
||||
output.ParseFromString(output_bytes)
|
||||
kind = output.WhichOneof("value")
|
||||
if kind == "error":
|
||||
raise BackendException(output.error)
|
||||
else:
|
||||
return output
|
||||
|
||||
def plus_one(self, num: int) -> int:
|
||||
input = pb.BackendInput(plus_one=pb.PlusOneIn(num=num))
|
||||
output = self._run_command(input)
|
||||
return output.plus_one.num
|
||||
|
||||
def template_requirements(
|
||||
self, template_fronts: List[str], field_map: Dict[str, int]
|
||||
) -> AllTemplateReqs:
|
||||
input = pb.BackendInput(
|
||||
template_requirements=pb.TemplateRequirementsIn(
|
||||
template_front=template_fronts, field_names_to_ordinals=field_map
|
||||
)
|
||||
)
|
||||
output = self._run_command(input).template_requirements
|
||||
reqs: List[pb.TemplateRequirement] = output.requirements # type: ignore
|
||||
return proto_template_reqs_to_legacy(reqs)
|
|
@ -19,6 +19,7 @@ import anki.find
|
|||
import anki.latex # sets up hook
|
||||
import anki.notes
|
||||
import anki.template
|
||||
from anki.backend import Backend
|
||||
from anki.cards import Card
|
||||
from anki.consts import *
|
||||
from anki.db import DB
|
||||
|
@ -84,8 +85,12 @@ class _Collection:
|
|||
ls: int
|
||||
conf: Dict[str, Any]
|
||||
_undo: List[Any]
|
||||
backend: Backend
|
||||
|
||||
def __init__(self, db: DB, server: bool = False, log: bool = False) -> None:
|
||||
def __init__(
|
||||
self, db: DB, backend: Backend, server: bool = False, log: bool = False
|
||||
) -> None:
|
||||
self.backend = backend
|
||||
self._debugLog = log
|
||||
self.db = db
|
||||
self.path = db._path
|
||||
|
|
|
@ -8,6 +8,7 @@ import operator
|
|||
import unicodedata
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
import anki # pylint: disable=unused-import
|
||||
from anki.consts import *
|
||||
from anki.errors import DeckRenameError
|
||||
from anki.hooks import runHook
|
||||
|
@ -98,7 +99,7 @@ class DeckManager:
|
|||
# Registry save/load
|
||||
#############################################################
|
||||
|
||||
def __init__(self, col) -> None:
|
||||
def __init__(self, col: "anki.storage._Collection") -> None:
|
||||
self.col = col
|
||||
self.decks = {}
|
||||
self.dconf = {}
|
||||
|
|
|
@ -608,9 +608,7 @@ def findDupes(col, fieldName, search="") -> List[Tuple[Any, List]]:
|
|||
# empty does not count as duplicate
|
||||
if not val:
|
||||
continue
|
||||
if val not in vals:
|
||||
vals[val] = []
|
||||
vals[val].append(nid)
|
||||
vals.setdefault(val, []).append(nid)
|
||||
if len(vals[val]) == 2:
|
||||
dupes.append((val, vals[val]))
|
||||
return dupes
|
||||
|
|
|
@ -556,6 +556,9 @@ select id from notes where mid = ?)"""
|
|||
##########################################################################
|
||||
|
||||
def _updateRequired(self, m: NoteType) -> None:
|
||||
self._updateRequiredNew(m)
|
||||
|
||||
def _updateRequiredLegacy(self, m: NoteType) -> None:
|
||||
if m["type"] == MODEL_CLOZE:
|
||||
# nothing to do
|
||||
return
|
||||
|
@ -566,6 +569,14 @@ select id from notes where mid = ?)"""
|
|||
req.append([t["ord"], ret[0], ret[1]])
|
||||
m["req"] = req
|
||||
|
||||
def _updateRequiredNew(self, m: NoteType) -> None:
|
||||
fronts = [t["qfmt"] for t in m["tmpls"]]
|
||||
field_map = {}
|
||||
for (idx, fld) in enumerate(m["flds"]):
|
||||
field_map[fld["name"]] = idx
|
||||
reqs = self.col.backend.template_requirements(fronts, field_map)
|
||||
m["req"] = [list(l) for l in reqs]
|
||||
|
||||
def _reqForTemplate(
|
||||
self, m: NoteType, flds: List[str], t: Template
|
||||
) -> Tuple[Union[str, List[int]], ...]:
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
import anki # pylint: disable=unused-import
|
||||
from anki.utils import (
|
||||
fieldChecksum,
|
||||
guid64,
|
||||
|
@ -19,7 +20,10 @@ class Note:
|
|||
tags: List[str]
|
||||
|
||||
def __init__(
|
||||
self, col, model: Optional[Any] = None, id: Optional[int] = None
|
||||
self,
|
||||
col: "anki.storage._Collection",
|
||||
model: Optional[Any] = None,
|
||||
id: Optional[int] = None,
|
||||
) -> None:
|
||||
assert not (model and id)
|
||||
self.col = col
|
||||
|
|
|
@ -8,6 +8,7 @@ import os
|
|||
import re
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
from anki.backend import Backend
|
||||
from anki.collection import _Collection
|
||||
from anki.consts import *
|
||||
from anki.db import DB
|
||||
|
@ -26,6 +27,10 @@ def Collection(
|
|||
path: str, lock: bool = True, server: bool = False, log: bool = False
|
||||
) -> _Collection:
|
||||
"Open a new or existing collection. Path must be unicode."
|
||||
backend = Backend()
|
||||
# fixme: this call is temporarily here to ensure the brige is working
|
||||
# on all platforms, and should be removed in a future beta
|
||||
assert backend.plus_one(5) == 6
|
||||
assert path.endswith(".anki2")
|
||||
path = os.path.abspath(path)
|
||||
create = not os.path.exists(path)
|
||||
|
@ -46,7 +51,7 @@ def Collection(
|
|||
db.execute("pragma journal_mode = wal")
|
||||
db.setAutocommit(False)
|
||||
# add db to col and do any remaining upgrades
|
||||
col = _Collection(db, server, log)
|
||||
col = _Collection(db, backend=backend, server=server, log=log)
|
||||
if ver < SCHEMA_VERSION:
|
||||
_upgrade(col, ver)
|
||||
elif ver > SCHEMA_VERSION:
|
||||
|
@ -60,7 +65,11 @@ def Collection(
|
|||
addBasicModel(col)
|
||||
col.save()
|
||||
if lock:
|
||||
col.lock()
|
||||
try:
|
||||
col.lock()
|
||||
except:
|
||||
col.db.close()
|
||||
raise
|
||||
return col
|
||||
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import json
|
|||
import re
|
||||
from typing import Callable, Dict, List, Tuple
|
||||
|
||||
import anki # pylint: disable=unused-import
|
||||
from anki.hooks import runHook
|
||||
from anki.utils import ids2str, intTime
|
||||
|
||||
|
@ -23,7 +24,7 @@ class TagManager:
|
|||
# Registry save/load
|
||||
#############################################################
|
||||
|
||||
def __init__(self, col) -> None:
|
||||
def __init__(self, col: "anki.storage._Collection") -> None:
|
||||
self.col = col
|
||||
self.tags: Dict[str, int] = {}
|
||||
|
||||
|
|
|
@ -210,14 +210,15 @@ class Template:
|
|||
return "{unknown field %s}" % tag_name
|
||||
return txt
|
||||
|
||||
@classmethod
|
||||
def clozeText(self, txt: str, ord: str, type: str) -> str:
|
||||
"""Processes the given Cloze deletion within the given template."""
|
||||
"""Processe the given Cloze deletion within the given template."""
|
||||
reg = clozeReg
|
||||
currentRegex = clozeReg % ord
|
||||
if not re.search(currentRegex, txt):
|
||||
# No Cloze deletion was found in txt.
|
||||
return ""
|
||||
txt = self._removeFormattingFromMathjax(txt, ord)
|
||||
txt = cls._removeFormattingFromMathjax(txt, ord)
|
||||
|
||||
def repl(m):
|
||||
# replace chosen cloze with type
|
||||
|
@ -237,7 +238,8 @@ class Template:
|
|||
# and display other clozes normally
|
||||
return re.sub(reg % r"\d+", "\\2", txt)
|
||||
|
||||
def _removeFormattingFromMathjax(self, txt, ord) -> str:
|
||||
@classmethod
|
||||
def _removeFormattingFromMathjax(cls, txt, ord) -> str:
|
||||
"""Marks all clozes within MathJax to prevent formatting them.
|
||||
|
||||
Active Cloze deletions within MathJax should not be wrapped inside
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Any, Dict, Tuple, Union
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
|
||||
# Model attributes are stored in a dict keyed by strings. This type alias
|
||||
# provides more descriptive function signatures than just 'Dict[str, Any]'
|
||||
|
@ -31,3 +31,8 @@ QAData = Tuple[
|
|||
# Corresponds to 'cardFlags' column. TODO: document
|
||||
int,
|
||||
]
|
||||
|
||||
TemplateRequirementType = str # Union["all", "any", "none"]
|
||||
# template ordinal, type, list of field ordinals
|
||||
TemplateRequiredFieldOrds = Tuple[int, TemplateRequirementType, List[int]]
|
||||
AllTemplateReqs = List[TemplateRequiredFieldOrds]
|
||||
|
|
|
@ -332,11 +332,19 @@ def _run(argv=None, exec=True):
|
|||
opts, args = parseArgs(argv)
|
||||
|
||||
# profile manager
|
||||
pm = ProfileManager(opts.base)
|
||||
pmLoadResult = pm.setupMeta()
|
||||
pm = None
|
||||
try:
|
||||
pm = ProfileManager(opts.base)
|
||||
pmLoadResult = pm.setupMeta()
|
||||
except:
|
||||
# will handle below
|
||||
pass
|
||||
|
||||
# gl workarounds
|
||||
setupGL(pm)
|
||||
if pm:
|
||||
# gl workarounds
|
||||
setupGL(pm)
|
||||
# apply user-provided scale factor
|
||||
os.environ["QT_SCALE_FACTOR"] = str(pm.uiScale())
|
||||
|
||||
# opt in to full hidpi support?
|
||||
if not os.environ.get("ANKI_NOHIGHDPI"):
|
||||
|
@ -348,9 +356,6 @@ def _run(argv=None, exec=True):
|
|||
if os.environ.get("ANKI_SOFTWAREOPENGL"):
|
||||
QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL)
|
||||
|
||||
# apply user-provided scale factor
|
||||
os.environ["QT_SCALE_FACTOR"] = str(pm.uiScale())
|
||||
|
||||
# create the app
|
||||
QCoreApplication.setApplicationName("Anki")
|
||||
QGuiApplication.setDesktopFileName("anki.desktop")
|
||||
|
@ -359,6 +364,16 @@ def _run(argv=None, exec=True):
|
|||
# we've signaled the primary instance, so we should close
|
||||
return
|
||||
|
||||
if not pm:
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
"Error",
|
||||
"""\
|
||||
Anki could not create its data folder. Please see the File Locations \
|
||||
section of the manual, and ensure that location is not read-only.""",
|
||||
)
|
||||
return
|
||||
|
||||
# disable icons on mac; this must be done before window created
|
||||
if isMac:
|
||||
app.setAttribute(Qt.AA_DontShowIconsInMenus)
|
||||
|
@ -392,12 +407,14 @@ environment points to a valid, writable folder.""",
|
|||
)
|
||||
return
|
||||
|
||||
if pmLoadResult.firstTime:
|
||||
pm.setDefaultLang()
|
||||
|
||||
if pmLoadResult.loadError:
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
"Preferences Corrupt",
|
||||
"""\
|
||||
Anki's prefs21.db file was corrupt and has been recreated. If you were using multiple \
|
||||
"""Anki's prefs21.db file was corrupt and has been recreated. If you were using multiple \
|
||||
profiles, please add them back using the same names to recover your cards.""",
|
||||
)
|
||||
|
||||
|
|
|
@ -148,6 +148,7 @@ system. It's free and open source."
|
|||
"David Bailey",
|
||||
"Arman High",
|
||||
"Arthur Milchior",
|
||||
"Rai (Michael Pokorny)",
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -97,22 +97,7 @@ class ProfileManager:
|
|||
######################################################################
|
||||
|
||||
def ensureBaseExists(self):
|
||||
try:
|
||||
self._ensureExists(self.base)
|
||||
except:
|
||||
# can't translate, as lang not initialized, and qt may not be
|
||||
print("unable to create base folder")
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
"Error",
|
||||
"""\
|
||||
Anki could not create the folder %s. Please ensure that location is not \
|
||||
read-only and you have permission to write to it. If you cannot fix this \
|
||||
issue, please see the documentation for information on running Anki from \
|
||||
a flash drive."""
|
||||
% self.base,
|
||||
)
|
||||
raise
|
||||
self._ensureExists(self.base)
|
||||
|
||||
# Folder migration
|
||||
######################################################################
|
||||
|
@ -385,7 +370,6 @@ create table if not exists profiles
|
|||
"insert or replace into profiles values ('_global', ?)",
|
||||
self._pickle(metaConf),
|
||||
)
|
||||
self._setDefaultLang()
|
||||
return result
|
||||
|
||||
def _ensureProfile(self):
|
||||
|
@ -409,7 +393,7 @@ please see:
|
|||
######################################################################
|
||||
# On first run, allow the user to choose the default language
|
||||
|
||||
def _setDefaultLang(self):
|
||||
def setDefaultLang(self):
|
||||
# create dialog
|
||||
class NoCloseDiag(QDialog):
|
||||
def reject(self):
|
||||
|
@ -452,7 +436,7 @@ please see:
|
|||
None, "Anki", en % name, QMessageBox.Yes | QMessageBox.No, QMessageBox.No
|
||||
)
|
||||
if r != QMessageBox.Yes:
|
||||
return self._setDefaultLang()
|
||||
return self.setDefaultLang()
|
||||
self.setLang(code)
|
||||
|
||||
def setLang(self, code):
|
||||
|
|
|
@ -416,11 +416,16 @@ body {{ zoom: {}; background: {}; {} }}
|
|||
self.evalWithCallback("$(document.body).height()", self._onHeight)
|
||||
|
||||
def _onHeight(self, qvar):
|
||||
from aqt import mw
|
||||
|
||||
if qvar is None:
|
||||
from aqt import mw
|
||||
|
||||
mw.progress.timer(1000, mw.reset, False)
|
||||
return
|
||||
|
||||
height = math.ceil(qvar * self.zoomFactor())
|
||||
scaleFactor = self.zoomFactor()
|
||||
if scaleFactor == 1:
|
||||
scaleFactor = mw.pm.uiScale()
|
||||
|
||||
height = math.ceil(qvar * scaleFactor)
|
||||
self.setFixedHeight(height)
|
||||
|
|
2
mypy.ini
2
mypy.ini
|
@ -34,3 +34,5 @@ ignore_missing_imports = True
|
|||
ignore_missing_imports = True
|
||||
[mypy-jsonschema.*]
|
||||
ignore_missing_imports = True
|
||||
[mypy-_ankirs]
|
||||
ignore_missing_imports = True
|
||||
|
|
68
proto/backend.proto
Normal file
68
proto/backend.proto
Normal file
|
@ -0,0 +1,68 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package backend_proto;
|
||||
|
||||
message Empty {}
|
||||
|
||||
message BackendInput {
|
||||
oneof value {
|
||||
PlusOneIn plus_one = 2;
|
||||
TemplateRequirementsIn template_requirements = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message BackendOutput {
|
||||
oneof value {
|
||||
BackendError error = 1;
|
||||
PlusOneOut plus_one = 2;
|
||||
TemplateRequirementsOut template_requirements = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message BackendError {
|
||||
oneof value {
|
||||
InvalidInputError invalid_input = 1;
|
||||
TemplateParseError template_parse = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message InvalidInputError {
|
||||
string info = 1;
|
||||
}
|
||||
|
||||
message PlusOneIn {
|
||||
int32 num = 1;
|
||||
}
|
||||
|
||||
message PlusOneOut {
|
||||
int32 num = 1;
|
||||
}
|
||||
|
||||
message TemplateParseError {
|
||||
string info = 1;
|
||||
}
|
||||
|
||||
message TemplateRequirementsIn {
|
||||
repeated string template_front = 1;
|
||||
map<string, uint32> field_names_to_ordinals = 2;
|
||||
}
|
||||
|
||||
message TemplateRequirementsOut {
|
||||
repeated TemplateRequirement requirements = 1;
|
||||
}
|
||||
|
||||
message TemplateRequirement {
|
||||
oneof value {
|
||||
TemplateRequirementAll all = 1;
|
||||
TemplateRequirementAny any = 2;
|
||||
Empty none = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message TemplateRequirementAll {
|
||||
repeated uint32 ords = 1;
|
||||
}
|
||||
|
||||
message TemplateRequirementAny {
|
||||
repeated uint32 ords = 1;
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
nose
|
||||
nose2
|
||||
mock
|
||||
mypy==0.750
|
||||
# fixme: when isort 5.0 is released, switch to pypy
|
||||
git+https://github.com/dae/isort#egg=isort
|
||||
# fixme: when pylint supports isort 5.0, switch to pypy
|
||||
|
|
|
@ -8,3 +8,7 @@ jsonschema
|
|||
psutil; sys_platform == "win32"
|
||||
distro; sys_platform != "win32" and sys_platform != "darwin"
|
||||
typing
|
||||
protobuf
|
||||
mypy==0.750
|
||||
mypy_protobuf
|
||||
|
||||
|
|
783
rs/Cargo.lock
generated
Normal file
783
rs/Cargo.lock
generated
Normal file
|
@ -0,0 +1,783 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ankirs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"failure",
|
||||
"nom",
|
||||
"prost",
|
||||
"prost-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9"
|
||||
dependencies = [
|
||||
"nodrop",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "924c76597f0d9ca25d762c25a4d369d51267536465dc5064bdf0eb073ed477ea"
|
||||
dependencies = [
|
||||
"backtrace-sys",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace-sys"
|
||||
version = "0.1.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"iovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "c2-chacha"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52a465a666ca3d838ebbf08b241383421412fe7ebb463527bba275526d89f76"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc"
|
||||
dependencies = [
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
|
||||
|
||||
[[package]]
|
||||
name = "failure"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8273f13c977665c5db7eb2b99ae520952fe5ac831ae4cd09d80c4c7042b5ed9"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"failure_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "failure_derive"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bc225b78e0391e4b8683440bf2e63c2deeeb2ce5189eab46e2b68c6d3725d08"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.11",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86d4de0081402f5e88cdac65c8dcdcc73118c1a7a465e2a05f0da05843a8ea33"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghost"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a36606a68532b5640dc86bb1f33c64b45c4682aad4c50f3937b317ea387f3d6"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9553c1e16c114b8b77ebeb329e5f2876eed62a8d51178c8bc6bff0d65f98f8"
|
||||
dependencies = [
|
||||
"indoc-impl",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc-impl"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b714fc08d0961716390977cdff1536234415ac37b509e34e5a983def8340fb75"
|
||||
dependencies = [
|
||||
"proc-macro-hack",
|
||||
"proc-macro2 1.0.6",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.11",
|
||||
"unindent",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4cece20baea71d9f3435e7bbe9adf4765f091c5fe404975f844006964a71299"
|
||||
dependencies = [
|
||||
"ctor",
|
||||
"ghost",
|
||||
"inventory-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory-impl"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2869bf972e998977b1cb87e60df70341d48e48dca0823f534feb91ea44adaf9"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iovec"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "lexical-core"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304bccb228c4b020f3a4835d247df0a02a7c4686098d4167762cfbbe4c5cb14"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"cfg-if",
|
||||
"rustc_version",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e"
|
||||
|
||||
[[package]]
|
||||
name = "multimap"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eb04b9f127583ed176e163fb9ec6f3e793b87e21deedd5734a69386a18a0151"
|
||||
|
||||
[[package]]
|
||||
name = "nodrop"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c618b63422da4401283884e6668d39f819a106ef51f5f59b81add00075da35ca"
|
||||
dependencies = [
|
||||
"lexical-core",
|
||||
"memchr",
|
||||
"version_check 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4c81ffc11c212fa327657cb19dd85eb7419e163b5b076bede2bdb5c974c07e4"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "423a519e1c6e828f1e73b720f9d9ed2fa643dce8a7737fb43235ce0b41eeaa49"
|
||||
dependencies = [
|
||||
"paste-impl",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste-impl"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4214c9e912ef61bf42b81ba9a47e8aad1b2ffaf739ab162bf96d1e011f54e6c5"
|
||||
dependencies = [
|
||||
"proc-macro-hack",
|
||||
"proc-macro2 1.0.6",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3659d1ee90221741f65dd128d9998311b0e40c5d3c23a62445938214abce4f"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-hack"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecd45702f76d6d3c75a80564378ae228a85f0b59d2f3ed43c91b4a69eb2ebfc5"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "0.4.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
|
||||
dependencies = [
|
||||
"unicode-xid 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27"
|
||||
dependencies = [
|
||||
"unicode-xid 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96d14b1c185652833d24aaad41c5832b0be5616a590227c1fbff57c616754b23"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"prost-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-build"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb788126ea840817128183f8f603dce02cb7aea25c2a0b764359d8e20010702e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"heck",
|
||||
"itertools",
|
||||
"log",
|
||||
"multimap",
|
||||
"petgraph",
|
||||
"prost",
|
||||
"prost-types",
|
||||
"tempfile",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-derive"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e7dc378b94ac374644181a2247cebf59a6ec1c88b49ac77f3a94b86b79d0e11"
|
||||
dependencies = [
|
||||
"failure",
|
||||
"itertools",
|
||||
"proc-macro2 0.4.30",
|
||||
"quote 0.6.13",
|
||||
"syn 0.15.44",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prost-types"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1de482a366941c8d56d19b650fac09ca08508f2a696119ee7513ad590c8bac6f"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pymod"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ankirs",
|
||||
"pyo3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f9df1468dddf8a59ec799cf3b930bb75ec09deabe875ba953e06c51d1077136"
|
||||
dependencies = [
|
||||
"indoc",
|
||||
"inventory",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"pyo3cls",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"spin",
|
||||
"unindent",
|
||||
"version_check 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-derive-backend"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f6e56fb3e97b344a8f87d036f94578399402c6b75949de6270cd07928f790b1"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3cls"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97452dcdf5941627ebc5c06664a07821fc7fc88d7515f02178193a8ebe316468"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6",
|
||||
"pyo3-derive-backend",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "0.6.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
|
||||
dependencies = [
|
||||
"proc-macro2 0.4.30",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_hc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853"
|
||||
dependencies = [
|
||||
"c2-chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc220bd33bdce8f093101afe22a037b8eb0e5af33592e6a9caafff0d4cb81cbd"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
"thread_local",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
|
||||
dependencies = [
|
||||
"semver-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver-parser"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c575e0cc52bdd09b47f330f646cf59afc586e9c4e3ccd6fc1f625b8ea1dad7"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "0.15.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
|
||||
dependencies = [
|
||||
"proc-macro2 0.4.30",
|
||||
"quote 0.6.13",
|
||||
"unicode-xid 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dff0acdb207ae2fe6d5976617f887eb1e35a2ba52c13c7234c790960cdad9238"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6",
|
||||
"quote 1.0.2",
|
||||
"unicode-xid 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.6",
|
||||
"quote 1.0.2",
|
||||
"syn 1.0.11",
|
||||
"unicode-xid 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"rand",
|
||||
"redox_syscall",
|
||||
"remove_dir_all",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
|
||||
|
||||
[[package]]
|
||||
name = "unindent"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63f18aa3b0e35fed5a0048f029558b1518095ffe2a0a31fb87c93dece93a4993"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d"
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b57acb10231b9493c8472b20cb57317d0679a49e0bdbee44b3b803a6473af164"
|
||||
dependencies = [
|
||||
"failure",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
6
rs/Cargo.toml
Normal file
6
rs/Cargo.toml
Normal file
|
@ -0,0 +1,6 @@
|
|||
[workspace]
|
||||
members = ["ankirs", "pymod"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
14
rs/ankirs/Cargo.toml
Normal file
14
rs/ankirs/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "ankirs"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
authors = ["Ankitects Pty Ltd and contributors"]
|
||||
|
||||
[dependencies]
|
||||
nom = "5.0.1"
|
||||
failure = "0.1.6"
|
||||
prost = "0.5.0"
|
||||
bytes = "0.4"
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.5.0"
|
7
rs/ankirs/build.rs
Normal file
7
rs/ankirs/build.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use prost_build;
|
||||
|
||||
fn main() {
|
||||
// avoid default OUT_DIR for now, for code completion
|
||||
std::env::set_var("OUT_DIR", "src");
|
||||
prost_build::compile_protos(&["../../proto/backend.proto"], &["../../proto/"]).unwrap();
|
||||
}
|
139
rs/ankirs/src/backend.rs
Normal file
139
rs/ankirs/src/backend.rs
Normal file
|
@ -0,0 +1,139 @@
|
|||
use crate::backend_proto as pt;
|
||||
use crate::backend_proto::backend_input::Value;
|
||||
use crate::err::{AnkiError, Result};
|
||||
use crate::template::{FieldMap, FieldRequirements, ParsedTemplate};
|
||||
use prost::Message;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub struct Backend {}
|
||||
|
||||
impl Default for Backend {
|
||||
fn default() -> Self {
|
||||
Backend {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an Anki error to a protobuf error.
|
||||
impl std::convert::From<AnkiError> for pt::BackendError {
|
||||
fn from(err: AnkiError) -> Self {
|
||||
use pt::backend_error::Value as V;
|
||||
let value = match err {
|
||||
AnkiError::InvalidInput { info } => V::InvalidInput(pt::InvalidInputError { info }),
|
||||
AnkiError::TemplateParseError { info } => {
|
||||
V::TemplateParse(pt::TemplateParseError { info })
|
||||
}
|
||||
};
|
||||
|
||||
pt::BackendError { value: Some(value) }
|
||||
}
|
||||
}
|
||||
|
||||
// Convert an Anki error to a protobuf output.
|
||||
impl std::convert::From<AnkiError> for pt::backend_output::Value {
|
||||
fn from(err: AnkiError) -> Self {
|
||||
pt::backend_output::Value::Error(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
pub fn new() -> Backend {
|
||||
Backend::default()
|
||||
}
|
||||
|
||||
/// Decode a request, process it, and return the encoded result.
|
||||
pub fn run_command_bytes(&mut self, req: &[u8]) -> Vec<u8> {
|
||||
let mut buf = vec![];
|
||||
|
||||
let req = match pt::BackendInput::decode(req) {
|
||||
Ok(req) => req,
|
||||
Err(_e) => {
|
||||
// unable to decode
|
||||
let err = AnkiError::invalid_input("couldn't decode backend request");
|
||||
let output = pt::BackendOutput {
|
||||
value: Some(err.into()),
|
||||
};
|
||||
output.encode(&mut buf).expect("encode failed");
|
||||
return buf;
|
||||
}
|
||||
};
|
||||
|
||||
let resp = self.run_command(req);
|
||||
resp.encode(&mut buf).expect("encode failed");
|
||||
buf
|
||||
}
|
||||
|
||||
fn run_command(&self, input: pt::BackendInput) -> pt::BackendOutput {
|
||||
let oval = if let Some(ival) = input.value {
|
||||
match self.run_command_inner(ival) {
|
||||
Ok(output) => output,
|
||||
Err(err) => err.into(),
|
||||
}
|
||||
} else {
|
||||
AnkiError::invalid_input("unrecognized backend input value").into()
|
||||
};
|
||||
|
||||
pt::BackendOutput { value: Some(oval) }
|
||||
}
|
||||
|
||||
fn run_command_inner(
|
||||
&self,
|
||||
ival: pt::backend_input::Value,
|
||||
) -> Result<pt::backend_output::Value> {
|
||||
use pt::backend_output::Value as OValue;
|
||||
Ok(match ival {
|
||||
Value::TemplateRequirements(input) => {
|
||||
OValue::TemplateRequirements(self.template_requirements(input)?)
|
||||
}
|
||||
Value::PlusOne(input) => OValue::PlusOne(self.plus_one(input)?),
|
||||
})
|
||||
}
|
||||
|
||||
fn plus_one(&self, input: pt::PlusOneIn) -> Result<pt::PlusOneOut> {
|
||||
let num = input.num + 1;
|
||||
Ok(pt::PlusOneOut { num })
|
||||
}
|
||||
|
||||
fn template_requirements(
|
||||
&self,
|
||||
input: pt::TemplateRequirementsIn,
|
||||
) -> Result<pt::TemplateRequirementsOut> {
|
||||
let map: FieldMap = input
|
||||
.field_names_to_ordinals
|
||||
.iter()
|
||||
.map(|(name, ord)| (name.as_str(), *ord as u16))
|
||||
.collect();
|
||||
// map each provided template into a requirements list
|
||||
use crate::backend_proto::template_requirement::Value;
|
||||
let all_reqs = input
|
||||
.template_front
|
||||
.into_iter()
|
||||
.map(|template| {
|
||||
if let Ok(tmpl) = ParsedTemplate::from_text(&template) {
|
||||
// convert the rust structure into a protobuf one
|
||||
let val = match tmpl.requirements(&map) {
|
||||
FieldRequirements::Any(ords) => Value::Any(pt::TemplateRequirementAny {
|
||||
ords: ords_hash_to_set(ords),
|
||||
}),
|
||||
FieldRequirements::All(ords) => Value::All(pt::TemplateRequirementAll {
|
||||
ords: ords_hash_to_set(ords),
|
||||
}),
|
||||
FieldRequirements::None => Value::None(pt::Empty {}),
|
||||
};
|
||||
Ok(pt::TemplateRequirement { value: Some(val) })
|
||||
} else {
|
||||
// template parsing failures make card unsatisfiable
|
||||
Ok(pt::TemplateRequirement {
|
||||
value: Some(Value::None(pt::Empty {})),
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
Ok(pt::TemplateRequirementsOut {
|
||||
requirements: all_reqs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn ords_hash_to_set(ords: HashSet<u16>) -> Vec<u32> {
|
||||
ords.iter().map(|ord| *ord as u32).collect()
|
||||
}
|
23
rs/ankirs/src/err.rs
Normal file
23
rs/ankirs/src/err.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
pub use failure::{Error, Fail};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AnkiError>;
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum AnkiError {
|
||||
#[fail(display = "invalid input: {}", info)]
|
||||
InvalidInput { info: String },
|
||||
|
||||
#[fail(display = "invalid card template: {}", info)]
|
||||
TemplateParseError { info: String },
|
||||
}
|
||||
|
||||
// error helpers
|
||||
impl AnkiError {
|
||||
pub(crate) fn parse<S: Into<String>>(s: S) -> AnkiError {
|
||||
AnkiError::TemplateParseError { info: s.into() }
|
||||
}
|
||||
|
||||
pub(crate) fn invalid_input<S: Into<String>>(s: S) -> AnkiError {
|
||||
AnkiError::InvalidInput { info: s.into() }
|
||||
}
|
||||
}
|
5
rs/ankirs/src/lib.rs
Normal file
5
rs/ankirs/src/lib.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod backend_proto;
|
||||
|
||||
pub mod backend;
|
||||
pub mod err;
|
||||
pub mod template;
|
360
rs/ankirs/src/template.rs
Normal file
360
rs/ankirs/src/template.rs
Normal file
|
@ -0,0 +1,360 @@
|
|||
use crate::err::{AnkiError, Result};
|
||||
use nom;
|
||||
use nom::branch::alt;
|
||||
use nom::bytes::complete::tag;
|
||||
use nom::error::ErrorKind;
|
||||
use nom::sequence::delimited;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub type FieldMap<'a> = HashMap<&'a str, u16>;
|
||||
|
||||
// Lexing
|
||||
//----------------------------------------
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Token<'a> {
|
||||
Text(&'a str),
|
||||
Replacement(&'a str),
|
||||
OpenConditional(&'a str),
|
||||
OpenNegated(&'a str),
|
||||
CloseConditional(&'a str),
|
||||
}
|
||||
|
||||
/// a span of text, terminated by {{, }} or end of string
|
||||
pub(crate) fn text_until_handlebars(s: &str) -> nom::IResult<&str, &str> {
|
||||
let end = s.len();
|
||||
|
||||
let limited_end = end
|
||||
.min(s.find("{{").unwrap_or(end))
|
||||
.min(s.find("}}").unwrap_or(end));
|
||||
let (output, input) = s.split_at(limited_end);
|
||||
if output.is_empty() {
|
||||
Err(nom::Err::Error((input, ErrorKind::TakeUntil)))
|
||||
} else {
|
||||
Ok((input, output))
|
||||
}
|
||||
}
|
||||
|
||||
/// text outside handlebars
|
||||
fn text_token(s: &str) -> nom::IResult<&str, Token> {
|
||||
text_until_handlebars(s).map(|(input, output)| (input, Token::Text(output)))
|
||||
}
|
||||
|
||||
/// text wrapped in handlebars
|
||||
fn handle_token(s: &str) -> nom::IResult<&str, Token> {
|
||||
delimited(tag("{{"), text_until_handlebars, tag("}}"))(s)
|
||||
.map(|(input, output)| (input, classify_handle(output)))
|
||||
}
|
||||
|
||||
/// classify handle based on leading character
|
||||
fn classify_handle(s: &str) -> Token {
|
||||
let start = s.trim();
|
||||
if start.len() < 2 {
|
||||
return Token::Replacement(start);
|
||||
}
|
||||
if start.starts_with('#') {
|
||||
Token::OpenConditional(&start[1..].trim_start())
|
||||
} else if start.starts_with('/') {
|
||||
Token::CloseConditional(&start[1..].trim_start())
|
||||
} else if start.starts_with('^') {
|
||||
Token::OpenNegated(&start[1..].trim_start())
|
||||
} else {
|
||||
Token::Replacement(start)
|
||||
}
|
||||
}
|
||||
|
||||
fn next_token(input: &str) -> nom::IResult<&str, Token> {
|
||||
alt((handle_token, text_token))(input)
|
||||
}
|
||||
|
||||
fn tokens(template: &str) -> impl Iterator<Item = Result<Token>> {
|
||||
let mut data = template;
|
||||
|
||||
std::iter::from_fn(move || {
|
||||
if data.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match next_token(data) {
|
||||
Ok((i, o)) => {
|
||||
data = i;
|
||||
Some(Ok(o))
|
||||
}
|
||||
Err(e) => Some(Err(AnkiError::parse(format!("{:?}", e)))),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Parsing
|
||||
//----------------------------------------
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum ParsedNode<'a> {
|
||||
Text(&'a str),
|
||||
Replacement {
|
||||
key: &'a str,
|
||||
filters: Vec<&'a str>,
|
||||
},
|
||||
Conditional {
|
||||
key: &'a str,
|
||||
children: Vec<ParsedNode<'a>>,
|
||||
},
|
||||
NegatedConditional {
|
||||
key: &'a str,
|
||||
children: Vec<ParsedNode<'a>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedTemplate<'a>(Vec<ParsedNode<'a>>);
|
||||
|
||||
impl ParsedTemplate<'_> {
|
||||
pub fn from_text(template: &str) -> Result<ParsedTemplate> {
|
||||
let mut iter = tokens(template);
|
||||
Ok(Self(parse_inner(&mut iter, None)?))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_inner<'a, I: Iterator<Item = Result<Token<'a>>>>(
|
||||
iter: &mut I,
|
||||
open_tag: Option<&'a str>,
|
||||
) -> Result<Vec<ParsedNode<'a>>> {
|
||||
let mut nodes = vec![];
|
||||
|
||||
while let Some(token) = iter.next() {
|
||||
use Token::*;
|
||||
nodes.push(match token? {
|
||||
Text(t) => ParsedNode::Text(t),
|
||||
Replacement(t) => {
|
||||
let mut it = t.rsplit(':');
|
||||
ParsedNode::Replacement {
|
||||
key: it.next().unwrap(),
|
||||
filters: it.collect(),
|
||||
}
|
||||
}
|
||||
OpenConditional(t) => ParsedNode::Conditional {
|
||||
key: t,
|
||||
children: parse_inner(iter, Some(t))?,
|
||||
},
|
||||
OpenNegated(t) => ParsedNode::NegatedConditional {
|
||||
key: t,
|
||||
children: parse_inner(iter, Some(t))?,
|
||||
},
|
||||
CloseConditional(t) => {
|
||||
if let Some(open) = open_tag {
|
||||
if open == t {
|
||||
// matching closing tag, move back to parent
|
||||
return Ok(nodes);
|
||||
}
|
||||
}
|
||||
return Err(AnkiError::parse(format!(
|
||||
"unbalanced closing tag: {:?} / {}",
|
||||
open_tag, t
|
||||
)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(open) = open_tag {
|
||||
Err(AnkiError::parse(format!("unclosed conditional {}", open)))
|
||||
} else {
|
||||
Ok(nodes)
|
||||
}
|
||||
}
|
||||
|
||||
// Checking if template is empty
|
||||
//----------------------------------------
|
||||
|
||||
impl ParsedTemplate<'_> {
|
||||
/// true if provided fields are sufficient to render the template
|
||||
pub fn renders_with_fields(&self, nonempty_fields: &HashSet<&str>) -> bool {
|
||||
!template_is_empty(nonempty_fields, &self.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn template_is_empty<'a>(nonempty_fields: &HashSet<&str>, nodes: &[ParsedNode<'a>]) -> bool {
|
||||
use ParsedNode::*;
|
||||
for node in nodes {
|
||||
match node {
|
||||
// ignore normal text
|
||||
Text(_) => (),
|
||||
Replacement { key, filters } => {
|
||||
// Anki doesn't consider a type: reference as a required field
|
||||
if filters.contains(&"type") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if nonempty_fields.contains(*key) {
|
||||
// a single replacement is enough
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Conditional { key, children } => {
|
||||
if !nonempty_fields.contains(*key) {
|
||||
continue;
|
||||
}
|
||||
if !template_is_empty(nonempty_fields, children) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
NegatedConditional { .. } => {
|
||||
// negated conditionals ignored when determining card generation
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
// Compatibility with old Anki versions
|
||||
//----------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum FieldRequirements {
|
||||
Any(HashSet<u16>),
|
||||
All(HashSet<u16>),
|
||||
None,
|
||||
}
|
||||
|
||||
impl ParsedTemplate<'_> {
|
||||
/// Return fields required by template.
|
||||
///
|
||||
/// This is not able to represent negated expressions or combinations of
|
||||
/// Any and All, and is provided only for the sake of backwards
|
||||
/// compatibility.
|
||||
pub fn requirements(&self, field_map: &FieldMap) -> FieldRequirements {
|
||||
let mut nonempty: HashSet<_> = Default::default();
|
||||
let mut ords = HashSet::new();
|
||||
for (name, ord) in field_map {
|
||||
nonempty.clear();
|
||||
nonempty.insert(*name);
|
||||
if self.renders_with_fields(&nonempty) {
|
||||
ords.insert(*ord);
|
||||
}
|
||||
}
|
||||
if !ords.is_empty() {
|
||||
return FieldRequirements::Any(ords);
|
||||
}
|
||||
|
||||
nonempty.extend(field_map.keys());
|
||||
ords.extend(field_map.values().copied());
|
||||
for (name, ord) in field_map {
|
||||
// can we remove this field and still render?
|
||||
nonempty.remove(name);
|
||||
if self.renders_with_fields(&nonempty) {
|
||||
ords.remove(ord);
|
||||
}
|
||||
nonempty.insert(*name);
|
||||
}
|
||||
if !ords.is_empty() && self.renders_with_fields(&nonempty) {
|
||||
FieldRequirements::All(ords)
|
||||
} else {
|
||||
FieldRequirements::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
//---------------------------------------
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{FieldMap, ParsedNode::*, ParsedTemplate as PT};
|
||||
use crate::template::FieldRequirements;
|
||||
use std::collections::HashSet;
|
||||
use std::iter::FromIterator;
|
||||
|
||||
#[test]
|
||||
fn test_parsing() {
|
||||
let tmpl = PT::from_text("foo {{bar}} {{#baz}} quux {{/baz}}").unwrap();
|
||||
assert_eq!(
|
||||
tmpl.0,
|
||||
vec![
|
||||
Text("foo "),
|
||||
Replacement {
|
||||
key: "bar",
|
||||
filters: vec![]
|
||||
},
|
||||
Text(" "),
|
||||
Conditional {
|
||||
key: "baz",
|
||||
children: vec![Text(" quux ")]
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
let tmpl = PT::from_text("{{^baz}}{{/baz}}").unwrap();
|
||||
assert_eq!(
|
||||
tmpl.0,
|
||||
vec![NegatedConditional {
|
||||
key: "baz",
|
||||
children: vec![]
|
||||
}]
|
||||
);
|
||||
|
||||
PT::from_text("{{#mis}}{{/matched}}").unwrap_err();
|
||||
PT::from_text("{{/matched}}").unwrap_err();
|
||||
PT::from_text("{{#mis}}").unwrap_err();
|
||||
|
||||
// whitespace
|
||||
assert_eq!(
|
||||
PT::from_text("{{ tag }}").unwrap().0,
|
||||
vec![Replacement {
|
||||
key: "tag",
|
||||
filters: vec![]
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonempty() {
|
||||
let fields = HashSet::from_iter(vec!["1", "3"].into_iter());
|
||||
let mut tmpl = PT::from_text("{{2}}{{1}}").unwrap();
|
||||
assert_eq!(tmpl.renders_with_fields(&fields), true);
|
||||
tmpl = PT::from_text("{{2}}{{type:cloze:1}}").unwrap();
|
||||
assert_eq!(tmpl.renders_with_fields(&fields), false);
|
||||
tmpl = PT::from_text("{{2}}{{4}}").unwrap();
|
||||
assert_eq!(tmpl.renders_with_fields(&fields), false);
|
||||
tmpl = PT::from_text("{{#3}}{{^2}}{{1}}{{/2}}{{/3}}").unwrap();
|
||||
assert_eq!(tmpl.renders_with_fields(&fields), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_requirements() {
|
||||
let field_map: FieldMap = vec!["a", "b"]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(a, b)| (*b, a as u16))
|
||||
.collect();
|
||||
|
||||
let mut tmpl = PT::from_text("{{a}}{{b}}").unwrap();
|
||||
assert_eq!(
|
||||
tmpl.requirements(&field_map),
|
||||
FieldRequirements::Any(HashSet::from_iter(vec![0, 1].into_iter()))
|
||||
);
|
||||
|
||||
tmpl = PT::from_text("{{#a}}{{b}}{{/a}}").unwrap();
|
||||
assert_eq!(
|
||||
tmpl.requirements(&field_map),
|
||||
FieldRequirements::All(HashSet::from_iter(vec![0, 1].into_iter()))
|
||||
);
|
||||
|
||||
tmpl = PT::from_text("{{c}}").unwrap();
|
||||
assert_eq!(tmpl.requirements(&field_map), FieldRequirements::None);
|
||||
|
||||
tmpl = PT::from_text("{{^a}}{{b}}{{/a}}").unwrap();
|
||||
assert_eq!(tmpl.requirements(&field_map), FieldRequirements::None);
|
||||
|
||||
tmpl = PT::from_text("{{#a}}{{#b}}{{a}}{{/b}}{{/a}}").unwrap();
|
||||
assert_eq!(
|
||||
tmpl.requirements(&field_map),
|
||||
FieldRequirements::All(HashSet::from_iter(vec![0, 1].into_iter()))
|
||||
);
|
||||
|
||||
tmpl = PT::from_text("{{a}}{{type:b}}").unwrap();
|
||||
assert_eq!(
|
||||
tmpl.requirements(&field_map),
|
||||
FieldRequirements::Any(HashSet::from_iter(vec![0].into_iter()))
|
||||
);
|
||||
}
|
||||
}
|
16
rs/pymod/Cargo.toml
Normal file
16
rs/pymod/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "pymod"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
authors = ["Ankitects Pty Ltd and contributors"]
|
||||
|
||||
[dependencies]
|
||||
ankirs = { path = "../ankirs" }
|
||||
|
||||
[dependencies.pyo3]
|
||||
version = "0.8.0"
|
||||
features = ["extension-module"]
|
||||
|
||||
[lib]
|
||||
name = "_ankirs"
|
||||
crate-type = ["cdylib"]
|
33
rs/pymod/src/lib.rs
Normal file
33
rs/pymod/src/lib.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use ankirs::backend::Backend as RustBackend;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyBytes;
|
||||
|
||||
#[pyclass]
|
||||
struct Backend {
|
||||
backend: RustBackend,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl Backend {
|
||||
#[new]
|
||||
fn init(obj: &PyRawObject) {
|
||||
obj.init({
|
||||
Backend {
|
||||
backend: Default::default(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn command(&mut self, py: Python, input: &PyBytes) -> PyResult<PyObject> {
|
||||
let out_bytes = self.backend.run_command_bytes(input.as_bytes());
|
||||
let out_obj = PyBytes::new(py, &out_bytes);
|
||||
Ok(out_obj.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[pymodule]
|
||||
fn _ankirs(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||
m.add_class::<Backend>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
1
rs/rust-toolchain
Normal file
1
rs/rust-toolchain
Normal file
|
@ -0,0 +1 @@
|
|||
nightly-2019-12-15
|
1
rs/rustfmt.toml
Normal file
1
rs/rustfmt.toml
Normal file
|
@ -0,0 +1 @@
|
|||
ignore = ["backend_proto.rs"]
|
|
@ -1,6 +1,10 @@
|
|||
import tempfile, os, shutil
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from anki import Collection as aopen
|
||||
|
||||
|
||||
def assertException(exception, func):
|
||||
found = False
|
||||
try:
|
||||
|
@ -25,6 +29,7 @@ def getEmptyCol():
|
|||
col = aopen(nam)
|
||||
return col
|
||||
|
||||
|
||||
getEmptyCol.master = ""
|
||||
|
||||
# Fallback for when the DB needs options passed in.
|
||||
|
@ -34,10 +39,12 @@ def getEmptyDeckWith(**kwargs):
|
|||
os.unlink(nam)
|
||||
return aopen(nam, **kwargs)
|
||||
|
||||
|
||||
def getUpgradeDeckPath(name="anki12.anki"):
|
||||
src = os.path.join(testDir, "support", name)
|
||||
(fd, dst) = tempfile.mkstemp(suffix=".anki2")
|
||||
shutil.copy(src, dst)
|
||||
return dst
|
||||
|
||||
|
||||
testDir = os.path.dirname(__file__)
|
||||
|
|
|
@ -1,73 +1,57 @@
|
|||
import os.path
|
||||
from nose.tools import assert_equals
|
||||
from mock import MagicMock
|
||||
from tempfile import TemporaryDirectory
|
||||
from zipfile import ZipFile
|
||||
|
||||
from mock import MagicMock
|
||||
from nose2.tools.such import helper
|
||||
|
||||
from aqt.addons import AddonManager
|
||||
|
||||
|
||||
def test_readMinimalManifest():
|
||||
assertReadManifest(
|
||||
'{"package": "yes", "name": "no"}',
|
||||
{"package": "yes", "name": "no"}
|
||||
'{"package": "yes", "name": "no"}', {"package": "yes", "name": "no"}
|
||||
)
|
||||
|
||||
|
||||
def test_readExtraKeys():
|
||||
assertReadManifest(
|
||||
'{"package": "a", "name": "b", "mod": 3, "conflicts": ["d", "e"]}',
|
||||
{"package": "a", "name": "b", "mod": 3, "conflicts": ["d", "e"]}
|
||||
{"package": "a", "name": "b", "mod": 3, "conflicts": ["d", "e"]},
|
||||
)
|
||||
|
||||
|
||||
def test_invalidManifest():
|
||||
assertReadManifest(
|
||||
'{"one": 1}',
|
||||
{}
|
||||
)
|
||||
assertReadManifest('{"one": 1}', {})
|
||||
|
||||
|
||||
def test_mustHaveName():
|
||||
assertReadManifest(
|
||||
'{"package": "something"}',
|
||||
{}
|
||||
)
|
||||
assertReadManifest('{"package": "something"}', {})
|
||||
|
||||
|
||||
def test_mustHavePackage():
|
||||
assertReadManifest(
|
||||
'{"name": "something"}',
|
||||
{}
|
||||
)
|
||||
assertReadManifest('{"name": "something"}', {})
|
||||
|
||||
|
||||
def test_invalidJson():
|
||||
assertReadManifest(
|
||||
'this is not a JSON dictionary',
|
||||
{}
|
||||
)
|
||||
assertReadManifest("this is not a JSON dictionary", {})
|
||||
|
||||
|
||||
def test_missingManifest():
|
||||
assertReadManifest(
|
||||
'{"package": "what", "name": "ever"}',
|
||||
{},
|
||||
nameInZip="not-manifest.bin"
|
||||
'{"package": "what", "name": "ever"}', {}, nameInZip="not-manifest.bin"
|
||||
)
|
||||
|
||||
|
||||
def test_ignoreExtraKeys():
|
||||
assertReadManifest(
|
||||
'{"package": "a", "name": "b", "game": "c"}',
|
||||
{"package": "a", "name": "b"}
|
||||
'{"package": "a", "name": "b", "game": "c"}', {"package": "a", "name": "b"}
|
||||
)
|
||||
|
||||
|
||||
def test_conflictsMustBeStrings():
|
||||
assertReadManifest(
|
||||
'{"package": "a", "name": "b", "conflicts": ["c", 4, {"d": "e"}]}',
|
||||
{}
|
||||
'{"package": "a", "name": "b", "conflicts": ["c", 4, {"d": "e"}]}', {}
|
||||
)
|
||||
|
||||
|
||||
|
@ -80,4 +64,4 @@ def assertReadManifest(contents, expectedManifest, nameInZip="manifest.json"):
|
|||
adm = AddonManager(MagicMock())
|
||||
|
||||
with ZipFile(zfn, "r") as zfile:
|
||||
assert_equals(adm.readManifestFile(zfile), expectedManifest)
|
||||
helper.assertEquals(adm.readManifestFile(zfile), expectedManifest)
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
|
||||
from tests.shared import getEmptyCol
|
||||
|
||||
|
||||
def test_previewCards():
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = '1'
|
||||
f['Back'] = '2'
|
||||
f["Front"] = "1"
|
||||
f["Back"] = "2"
|
||||
# non-empty and active
|
||||
cards = deck.previewCards(f, 0)
|
||||
assert len(cards) == 1
|
||||
|
@ -22,11 +23,12 @@ def test_previewCards():
|
|||
# make sure we haven't accidentally added cards to the db
|
||||
assert deck.cardCount() == 1
|
||||
|
||||
|
||||
def test_delete():
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = '1'
|
||||
f['Back'] = '2'
|
||||
f["Front"] = "1"
|
||||
f["Back"] = "2"
|
||||
deck.addNote(f)
|
||||
cid = f.cards()[0].id
|
||||
deck.reset()
|
||||
|
@ -38,62 +40,65 @@ def test_delete():
|
|||
assert deck.db.scalar("select count() from cards") == 0
|
||||
assert deck.db.scalar("select count() from graves") == 2
|
||||
|
||||
|
||||
def test_misc():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = '1'
|
||||
f['Back'] = '2'
|
||||
f["Front"] = "1"
|
||||
f["Back"] = "2"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
id = d.models.current()['id']
|
||||
assert c.template()['ord'] == 0
|
||||
id = d.models.current()["id"]
|
||||
assert c.template()["ord"] == 0
|
||||
|
||||
|
||||
def test_genrem():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = '1'
|
||||
f['Back'] = ''
|
||||
f["Front"] = "1"
|
||||
f["Back"] = ""
|
||||
d.addNote(f)
|
||||
assert len(f.cards()) == 1
|
||||
m = d.models.current()
|
||||
mm = d.models
|
||||
# adding a new template should automatically create cards
|
||||
t = mm.newTemplate("rev")
|
||||
t['qfmt'] = '{{Front}}'
|
||||
t['afmt'] = ""
|
||||
t["qfmt"] = "{{Front}}"
|
||||
t["afmt"] = ""
|
||||
mm.addTemplate(m, t)
|
||||
mm.save(m, templates=True)
|
||||
assert len(f.cards()) == 2
|
||||
# if the template is changed to remove cards, they'll be removed
|
||||
t['qfmt'] = "{{Back}}"
|
||||
t["qfmt"] = "{{Back}}"
|
||||
mm.save(m, templates=True)
|
||||
d.remCards(d.emptyCids())
|
||||
assert len(f.cards()) == 1
|
||||
# if we add to the note, a card should be automatically generated
|
||||
f.load()
|
||||
f['Back'] = "1"
|
||||
f["Back"] = "1"
|
||||
f.flush()
|
||||
assert len(f.cards()) == 2
|
||||
|
||||
|
||||
def test_gendeck():
|
||||
d = getEmptyCol()
|
||||
cloze = d.models.byName("Cloze")
|
||||
d.models.setCurrent(cloze)
|
||||
f = d.newNote()
|
||||
f['Text'] = '{{c1::one}}'
|
||||
f["Text"] = "{{c1::one}}"
|
||||
d.addNote(f)
|
||||
assert d.cardCount() == 1
|
||||
assert f.cards()[0].did == 1
|
||||
# set the model to a new default deck
|
||||
newId = d.decks.id("new")
|
||||
cloze['did'] = newId
|
||||
cloze["did"] = newId
|
||||
d.models.save(cloze, updateReqs=False)
|
||||
# a newly generated card should share the first card's deck
|
||||
f['Text'] += '{{c2::two}}'
|
||||
f["Text"] += "{{c2::two}}"
|
||||
f.flush()
|
||||
assert f.cards()[1].did == 1
|
||||
# and same with multiple cards
|
||||
f['Text'] += '{{c3::three}}'
|
||||
f["Text"] += "{{c3::three}}"
|
||||
f.flush()
|
||||
assert f.cards()[2].did == 1
|
||||
# if one of the cards is in a different deck, it should revert to the
|
||||
|
@ -101,9 +106,6 @@ def test_gendeck():
|
|||
c = f.cards()[1]
|
||||
c.did = newId
|
||||
c.flush()
|
||||
f['Text'] += '{{c4::four}}'
|
||||
f["Text"] += "{{c4::four}}"
|
||||
f.flush()
|
||||
assert f.cards()[3].did == newId
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
# coding: utf-8
|
||||
|
||||
import os, tempfile
|
||||
from tests.shared import assertException, getEmptyCol
|
||||
from anki.stdmodels import addBasicModel, models
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from anki import Collection as aopen
|
||||
from anki.stdmodels import addBasicModel, models
|
||||
from anki.utils import isWin
|
||||
from tests.shared import assertException, getEmptyCol
|
||||
|
||||
newPath = None
|
||||
newMod = None
|
||||
|
||||
def test_create_open():
|
||||
global newPath, newMod
|
||||
(fd, path) = tempfile.mkstemp(suffix=".anki2", prefix="test_attachNew")
|
||||
try:
|
||||
os.close(fd)
|
||||
|
@ -30,27 +29,32 @@ def test_create_open():
|
|||
deck.close()
|
||||
|
||||
# non-writeable dir
|
||||
assertException(Exception,
|
||||
lambda: aopen("/attachroot.anki2"))
|
||||
if isWin:
|
||||
dir = "c:\root.anki2"
|
||||
else:
|
||||
dir = "/attachroot.anki2"
|
||||
assertException(Exception, lambda: aopen(dir))
|
||||
# reuse tmp file from before, test non-writeable file
|
||||
os.chmod(newPath, 0)
|
||||
assertException(Exception,
|
||||
lambda: aopen(newPath))
|
||||
assertException(Exception, lambda: aopen(newPath))
|
||||
os.chmod(newPath, 0o666)
|
||||
os.unlink(newPath)
|
||||
|
||||
|
||||
def test_noteAddDelete():
|
||||
deck = getEmptyCol()
|
||||
# add a note
|
||||
f = deck.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
n = deck.addNote(f)
|
||||
assert n == 1
|
||||
# test multiple cards - add another template
|
||||
m = deck.models.current(); mm = deck.models
|
||||
m = deck.models.current()
|
||||
mm = deck.models
|
||||
t = mm.newTemplate("Reverse")
|
||||
t['qfmt'] = "{{Back}}"
|
||||
t['afmt'] = "{{Front}}"
|
||||
t["qfmt"] = "{{Back}}"
|
||||
t["afmt"] = "{{Front}}"
|
||||
mm.addTemplate(m, t)
|
||||
mm.save(m)
|
||||
# the default save doesn't generate cards
|
||||
|
@ -61,7 +65,8 @@ def test_noteAddDelete():
|
|||
assert deck.cardCount() == 2
|
||||
# creating new notes should use both cards
|
||||
f = deck.newNote()
|
||||
f['Front'] = "three"; f['Back'] = "four"
|
||||
f["Front"] = "three"
|
||||
f["Back"] = "four"
|
||||
n = deck.addNote(f)
|
||||
assert n == 2
|
||||
assert deck.cardCount() == 4
|
||||
|
@ -72,36 +77,39 @@ def test_noteAddDelete():
|
|||
assert not f.dupeOrEmpty()
|
||||
# now let's make a duplicate
|
||||
f2 = deck.newNote()
|
||||
f2['Front'] = "one"; f2['Back'] = ""
|
||||
f2["Front"] = "one"
|
||||
f2["Back"] = ""
|
||||
assert f2.dupeOrEmpty()
|
||||
# empty first field should not be permitted either
|
||||
f2['Front'] = " "
|
||||
f2["Front"] = " "
|
||||
assert f2.dupeOrEmpty()
|
||||
|
||||
|
||||
def test_fieldChecksum():
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = "new"; f['Back'] = "new2"
|
||||
f["Front"] = "new"
|
||||
f["Back"] = "new2"
|
||||
deck.addNote(f)
|
||||
assert deck.db.scalar(
|
||||
"select csum from notes") == int("c2a6b03f", 16)
|
||||
assert deck.db.scalar("select csum from notes") == int("c2a6b03f", 16)
|
||||
# changing the val should change the checksum
|
||||
f['Front'] = "newx"
|
||||
f["Front"] = "newx"
|
||||
f.flush()
|
||||
assert deck.db.scalar(
|
||||
"select csum from notes") == int("302811ae", 16)
|
||||
assert deck.db.scalar("select csum from notes") == int("302811ae", 16)
|
||||
|
||||
|
||||
def test_addDelTags():
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = "1"
|
||||
f["Front"] = "1"
|
||||
deck.addNote(f)
|
||||
f2 = deck.newNote()
|
||||
f2['Front'] = "2"
|
||||
f2["Front"] = "2"
|
||||
deck.addNote(f2)
|
||||
# adding for a given id
|
||||
deck.tags.bulkAdd([f.id], "foo")
|
||||
f.load(); f2.load()
|
||||
f.load()
|
||||
f2.load()
|
||||
assert "foo" in f.tags
|
||||
assert "foo" not in f2.tags
|
||||
# should be canonified
|
||||
|
@ -110,6 +118,7 @@ def test_addDelTags():
|
|||
assert f.tags[0] == "aaa"
|
||||
assert len(f.tags) == 2
|
||||
|
||||
|
||||
def test_timestamps():
|
||||
deck = getEmptyCol()
|
||||
assert len(deck.models.models) == len(models)
|
||||
|
@ -117,23 +126,24 @@ def test_timestamps():
|
|||
addBasicModel(deck)
|
||||
assert len(deck.models.models) == 100 + len(models)
|
||||
|
||||
|
||||
def test_furigana():
|
||||
deck = getEmptyCol()
|
||||
mm = deck.models
|
||||
m = mm.current()
|
||||
# filter should work
|
||||
m['tmpls'][0]['qfmt'] = '{{kana:Front}}'
|
||||
m["tmpls"][0]["qfmt"] = "{{kana:Front}}"
|
||||
mm.save(m)
|
||||
n = deck.newNote()
|
||||
n['Front'] = 'foo[abc]'
|
||||
n["Front"] = "foo[abc]"
|
||||
deck.addNote(n)
|
||||
c = n.cards()[0]
|
||||
assert c.q().endswith("abc")
|
||||
# and should avoid sound
|
||||
n['Front'] = 'foo[sound:abc.mp3]'
|
||||
n["Front"] = "foo[sound:abc.mp3]"
|
||||
n.flush()
|
||||
assert "sound:" in c.q(reload=True)
|
||||
# it shouldn't throw an error while people are editing
|
||||
m['tmpls'][0]['qfmt'] = '{{kana:}}'
|
||||
m["tmpls"][0]["qfmt"] = "{{kana:}}"
|
||||
mm.save(m)
|
||||
c.q(reload=True)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
from anki.errors import DeckRenameError
|
||||
from tests.shared import assertException, getEmptyCol
|
||||
|
||||
|
||||
def test_basic():
|
||||
deck = getEmptyCol()
|
||||
# we start with a standard deck
|
||||
|
@ -34,21 +35,22 @@ def test_basic():
|
|||
# parents with a different case should be handled correctly
|
||||
deck.decks.id("ONE")
|
||||
m = deck.models.current()
|
||||
m['did'] = deck.decks.id("one::two")
|
||||
m["did"] = deck.decks.id("one::two")
|
||||
deck.models.save(m, updateReqs=False)
|
||||
n = deck.newNote()
|
||||
n['Front'] = "abc"
|
||||
n["Front"] = "abc"
|
||||
deck.addNote(n)
|
||||
# this will error if child and parent case don't match
|
||||
deck.sched.deckDueList()
|
||||
|
||||
|
||||
def test_remove():
|
||||
deck = getEmptyCol()
|
||||
# create a new deck, and add a note/card to it
|
||||
g1 = deck.decks.id("g1")
|
||||
f = deck.newNote()
|
||||
f['Front'] = "1"
|
||||
f.model()['did'] = g1
|
||||
f["Front"] = "1"
|
||||
f.model()["did"] = g1
|
||||
deck.addNote(f)
|
||||
c = f.cards()[0]
|
||||
assert c.did == g1
|
||||
|
@ -62,12 +64,14 @@ def test_remove():
|
|||
assert deck.decks.name(c.did) == "[no deck]"
|
||||
# let's create another deck and explicitly set the card to it
|
||||
g2 = deck.decks.id("g2")
|
||||
c.did = g2; c.flush()
|
||||
c.did = g2
|
||||
c.flush()
|
||||
# this time we'll delete the card/note too
|
||||
deck.decks.rem(g2, cardsToo=True)
|
||||
assert deck.cardCount() == 0
|
||||
assert deck.noteCount() == 0
|
||||
|
||||
|
||||
def test_rename():
|
||||
d = getEmptyCol()
|
||||
id = d.decks.id("hello::world")
|
||||
|
@ -80,8 +84,7 @@ def test_rename():
|
|||
# create another deck
|
||||
id = d.decks.id("tmp")
|
||||
# we can't rename it if it conflicts
|
||||
assertException(
|
||||
Exception, lambda: d.decks.rename(d.decks.get(id), "foo"))
|
||||
assertException(Exception, lambda: d.decks.rename(d.decks.get(id), "foo"))
|
||||
# when renaming, the children should be renamed too
|
||||
d.decks.id("one::two::three")
|
||||
id = d.decks.id("one")
|
||||
|
@ -102,62 +105,66 @@ def test_rename():
|
|||
assertException(DeckRenameError, lambda: d.decks.rename(child, "PARENT::child"))
|
||||
|
||||
|
||||
|
||||
def test_renameForDragAndDrop():
|
||||
d = getEmptyCol()
|
||||
|
||||
def deckNames():
|
||||
return [ name for name in sorted(d.decks.allNames()) if name != 'Default' ]
|
||||
return [name for name in sorted(d.decks.allNames()) if name != "Default"]
|
||||
|
||||
languages_did = d.decks.id('Languages')
|
||||
chinese_did = d.decks.id('Chinese')
|
||||
hsk_did = d.decks.id('Chinese::HSK')
|
||||
languages_did = d.decks.id("Languages")
|
||||
chinese_did = d.decks.id("Chinese")
|
||||
hsk_did = d.decks.id("Chinese::HSK")
|
||||
|
||||
# Renaming also renames children
|
||||
d.decks.renameForDragAndDrop(chinese_did, languages_did)
|
||||
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ]
|
||||
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
||||
|
||||
# Dragging a deck onto itself is a no-op
|
||||
d.decks.renameForDragAndDrop(languages_did, languages_did)
|
||||
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ]
|
||||
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
||||
|
||||
# Dragging a deck onto its parent is a no-op
|
||||
d.decks.renameForDragAndDrop(hsk_did, chinese_did)
|
||||
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ]
|
||||
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
||||
|
||||
# Dragging a deck onto a descendant is a no-op
|
||||
d.decks.renameForDragAndDrop(languages_did, hsk_did)
|
||||
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ]
|
||||
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
||||
|
||||
# Can drag a grandchild onto its grandparent. It becomes a child
|
||||
d.decks.renameForDragAndDrop(hsk_did, languages_did)
|
||||
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::HSK' ]
|
||||
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::HSK"]
|
||||
|
||||
# Can drag a deck onto its sibling
|
||||
d.decks.renameForDragAndDrop(hsk_did, chinese_did)
|
||||
assert deckNames() == [ 'Languages', 'Languages::Chinese', 'Languages::Chinese::HSK' ]
|
||||
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
||||
|
||||
# Can drag a deck back to the top level
|
||||
d.decks.renameForDragAndDrop(chinese_did, None)
|
||||
assert deckNames() == [ 'Chinese', 'Chinese::HSK', 'Languages' ]
|
||||
assert deckNames() == ["Chinese", "Chinese::HSK", "Languages"]
|
||||
|
||||
# Dragging a top level deck to the top level is a no-op
|
||||
d.decks.renameForDragAndDrop(chinese_did, None)
|
||||
assert deckNames() == [ 'Chinese', 'Chinese::HSK', 'Languages' ]
|
||||
assert deckNames() == ["Chinese", "Chinese::HSK", "Languages"]
|
||||
|
||||
# can't drack a deck where sibling have same name
|
||||
new_hsk_did = d.decks.id("HSK")
|
||||
assertException(DeckRenameError, lambda: d.decks.renameForDragAndDrop(new_hsk_did, chinese_did))
|
||||
assertException(
|
||||
DeckRenameError, lambda: d.decks.renameForDragAndDrop(new_hsk_did, chinese_did)
|
||||
)
|
||||
d.decks.rem(new_hsk_did)
|
||||
|
||||
# can't drack a deck where sibling have same name different case
|
||||
new_hsk_did = d.decks.id("hsk")
|
||||
assertException(DeckRenameError, lambda: d.decks.renameForDragAndDrop(new_hsk_did, chinese_did))
|
||||
assertException(
|
||||
DeckRenameError, lambda: d.decks.renameForDragAndDrop(new_hsk_did, chinese_did)
|
||||
)
|
||||
d.decks.rem(new_hsk_did)
|
||||
|
||||
# '' is a convenient alias for the top level DID
|
||||
d.decks.renameForDragAndDrop(hsk_did, '')
|
||||
assert deckNames() == [ 'Chinese', 'HSK', 'Languages' ]
|
||||
d.decks.renameForDragAndDrop(hsk_did, "")
|
||||
assert deckNames() == ["Chinese", "HSK", "Languages"]
|
||||
|
||||
|
||||
def test_check():
|
||||
d = getEmptyCol()
|
||||
|
|
|
@ -1,37 +1,48 @@
|
|||
# coding: utf-8
|
||||
|
||||
import nose, os, tempfile
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from nose2.tools.decorators import with_setup
|
||||
|
||||
from anki import Collection as aopen
|
||||
from anki.exporting import *
|
||||
from anki.importing import Anki2Importer
|
||||
|
||||
from .shared import getEmptyCol
|
||||
|
||||
deck = None
|
||||
ds = None
|
||||
testDir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
def setup1():
|
||||
global deck
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = "foo"; f['Back'] = "bar<br>"; f.tags = ["tag", "tag2"]
|
||||
f["Front"] = "foo"
|
||||
f["Back"] = "bar<br>"
|
||||
f.tags = ["tag", "tag2"]
|
||||
deck.addNote(f)
|
||||
# with a different deck
|
||||
f = deck.newNote()
|
||||
f['Front'] = "baz"; f['Back'] = "qux"
|
||||
f.model()['did'] = deck.decks.id("new deck")
|
||||
f["Front"] = "baz"
|
||||
f["Back"] = "qux"
|
||||
f.model()["did"] = deck.decks.id("new deck")
|
||||
deck.addNote(f)
|
||||
|
||||
|
||||
##########################################################################
|
||||
|
||||
@nose.with_setup(setup1)
|
||||
|
||||
@with_setup(setup1)
|
||||
def test_export_anki():
|
||||
# create a new deck with its own conf to test conf copying
|
||||
did = deck.decks.id("test")
|
||||
dobj = deck.decks.get(did)
|
||||
confId = deck.decks.confId("newconf")
|
||||
conf = deck.decks.getConf(confId)
|
||||
conf['new']['perDay'] = 5
|
||||
conf["new"]["perDay"] = 5
|
||||
deck.decks.save(conf)
|
||||
deck.decks.setConf(dobj, confId)
|
||||
# export
|
||||
|
@ -43,7 +54,7 @@ def test_export_anki():
|
|||
e.exportInto(newname)
|
||||
# exporting should not have changed conf for original deck
|
||||
conf = deck.decks.confForDid(did)
|
||||
assert conf['id'] != 1
|
||||
assert conf["id"] != 1
|
||||
# connect to new deck
|
||||
d2 = aopen(newname)
|
||||
assert d2.cardCount() == 2
|
||||
|
@ -51,10 +62,10 @@ def test_export_anki():
|
|||
did = d2.decks.id("test", create=False)
|
||||
assert did
|
||||
conf2 = d2.decks.confForDid(did)
|
||||
assert conf2['new']['perDay'] == 20
|
||||
assert conf2["new"]["perDay"] == 20
|
||||
dobj = d2.decks.get(did)
|
||||
# conf should be 1
|
||||
assert dobj['conf'] == 1
|
||||
assert dobj["conf"] == 1
|
||||
# try again, limited to a deck
|
||||
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
|
||||
newname = str(newname)
|
||||
|
@ -65,13 +76,14 @@ def test_export_anki():
|
|||
d2 = aopen(newname)
|
||||
assert d2.cardCount() == 1
|
||||
|
||||
@nose.with_setup(setup1)
|
||||
|
||||
@with_setup(setup1)
|
||||
def test_export_ankipkg():
|
||||
# add a test file to the media folder
|
||||
with open(os.path.join(deck.media.dir(), "今日.mp3"), "w") as f:
|
||||
f.write("test")
|
||||
n = deck.newNote()
|
||||
n['Front'] = '[sound:今日.mp3]'
|
||||
n["Front"] = "[sound:今日.mp3]"
|
||||
deck.addNote(n)
|
||||
e = AnkiPackageExporter(deck)
|
||||
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".apkg")
|
||||
|
@ -80,13 +92,14 @@ def test_export_ankipkg():
|
|||
os.unlink(newname)
|
||||
e.exportInto(newname)
|
||||
|
||||
@nose.with_setup(setup1)
|
||||
|
||||
@with_setup(setup1)
|
||||
def test_export_anki_due():
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = "foo"
|
||||
f["Front"] = "foo"
|
||||
deck.addNote(f)
|
||||
deck.crt -= 86400*10
|
||||
deck.crt -= 86400 * 10
|
||||
deck.sched.reset()
|
||||
c = deck.sched.getCard()
|
||||
deck.sched.answerCard(c, 3)
|
||||
|
@ -112,7 +125,8 @@ def test_export_anki_due():
|
|||
deck2.sched.reset()
|
||||
assert c.due - deck2.sched.today == 1
|
||||
|
||||
# @nose.with_setup(setup1)
|
||||
|
||||
# @with_setup(setup1)
|
||||
# def test_export_textcard():
|
||||
# e = TextCardExporter(deck)
|
||||
# f = unicode(tempfile.mkstemp(prefix="ankitest")[1])
|
||||
|
@ -121,7 +135,8 @@ def test_export_anki_due():
|
|||
# e.includeTags = True
|
||||
# e.exportInto(f)
|
||||
|
||||
@nose.with_setup(setup1)
|
||||
|
||||
@with_setup(setup1)
|
||||
def test_export_textnote():
|
||||
e = TextNoteExporter(deck)
|
||||
fd, f = tempfile.mkstemp(prefix="ankitest")
|
||||
|
@ -135,5 +150,6 @@ def test_export_textnote():
|
|||
e.exportInto(f)
|
||||
assert open(f).readline() == "foo\tbar\n"
|
||||
|
||||
|
||||
def test_exporters():
|
||||
assert "*.apkg" in str(exporters())
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
# coding: utf-8
|
||||
from nose.tools import assert_raises
|
||||
from nose2.tools.such import helper
|
||||
|
||||
from anki.find import Finder
|
||||
from tests.shared import getEmptyCol
|
||||
|
||||
|
||||
def test_parse():
|
||||
f = Finder(None)
|
||||
assert f._tokenize("hello world") == ["hello", "world"]
|
||||
|
@ -12,43 +13,54 @@ def test_parse():
|
|||
assert f._tokenize("one --two") == ["one", "-", "two"]
|
||||
assert f._tokenize("one - two") == ["one", "-", "two"]
|
||||
assert f._tokenize("one or -two") == ["one", "or", "-", "two"]
|
||||
assert f._tokenize("'hello \"world\"'") == ["hello \"world\""]
|
||||
assert f._tokenize("'hello \"world\"'") == ['hello "world"']
|
||||
assert f._tokenize('"hello world"') == ["hello world"]
|
||||
assert f._tokenize("one (two or ( three or four))") == [
|
||||
"one", "(", "two", "or", "(", "three", "or", "four",
|
||||
")", ")"]
|
||||
"one",
|
||||
"(",
|
||||
"two",
|
||||
"or",
|
||||
"(",
|
||||
"three",
|
||||
"or",
|
||||
"four",
|
||||
")",
|
||||
")",
|
||||
]
|
||||
assert f._tokenize("embedded'string") == ["embedded'string"]
|
||||
assert f._tokenize("deck:'two words'") == ["deck:two words"]
|
||||
|
||||
|
||||
def test_findCards():
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = 'dog'
|
||||
f['Back'] = 'cat'
|
||||
f["Front"] = "dog"
|
||||
f["Back"] = "cat"
|
||||
f.tags.append("monkey animal_1 * %")
|
||||
f1id = f.id
|
||||
deck.addNote(f)
|
||||
firstCardId = f.cards()[0].id
|
||||
f = deck.newNote()
|
||||
f['Front'] = 'goats are fun'
|
||||
f['Back'] = 'sheep'
|
||||
f["Front"] = "goats are fun"
|
||||
f["Back"] = "sheep"
|
||||
f.tags.append("sheep goat horse animal11")
|
||||
deck.addNote(f)
|
||||
f2id = f.id
|
||||
f = deck.newNote()
|
||||
f['Front'] = 'cat'
|
||||
f['Back'] = 'sheep'
|
||||
f["Front"] = "cat"
|
||||
f["Back"] = "sheep"
|
||||
deck.addNote(f)
|
||||
catCard = f.cards()[0]
|
||||
m = deck.models.current(); mm = deck.models
|
||||
m = deck.models.current()
|
||||
mm = deck.models
|
||||
t = mm.newTemplate("Reverse")
|
||||
t['qfmt'] = "{{Back}}"
|
||||
t['afmt'] = "{{Front}}"
|
||||
t["qfmt"] = "{{Back}}"
|
||||
t["afmt"] = "{{Front}}"
|
||||
mm.addTemplate(m, t)
|
||||
mm.save(m)
|
||||
f = deck.newNote()
|
||||
f['Front'] = 'test'
|
||||
f['Back'] = 'foo bar'
|
||||
f["Front"] = "test"
|
||||
f["Back"] = "foo bar"
|
||||
deck.addNote(f)
|
||||
latestCardIds = [c.id for c in f.cards()]
|
||||
# tag searches
|
||||
|
@ -66,9 +78,7 @@ def test_findCards():
|
|||
assert len(deck.findCards("tag:sheep -tag:monkey")) == 1
|
||||
assert len(deck.findCards("-tag:sheep")) == 4
|
||||
deck.tags.bulkAdd(deck.db.list("select id from notes"), "foo bar")
|
||||
assert (len(deck.findCards("tag:foo")) ==
|
||||
len(deck.findCards("tag:bar")) ==
|
||||
5)
|
||||
assert len(deck.findCards("tag:foo")) == len(deck.findCards("tag:bar")) == 5
|
||||
deck.tags.bulkRem(deck.db.list("select id from notes"), "foo")
|
||||
assert len(deck.findCards("tag:foo")) == 0
|
||||
assert len(deck.findCards("tag:bar")) == 5
|
||||
|
@ -86,7 +96,8 @@ def test_findCards():
|
|||
c.flush()
|
||||
assert deck.findCards("is:review") == [c.id]
|
||||
assert deck.findCards("is:due") == []
|
||||
c.due = 0; c.queue = 2
|
||||
c.due = 0
|
||||
c.queue = 2
|
||||
c.flush()
|
||||
assert deck.findCards("is:due") == [c.id]
|
||||
assert len(deck.findCards("-is:due")) == 4
|
||||
|
@ -97,10 +108,10 @@ def test_findCards():
|
|||
assert deck.findCards("is:suspended") == [c.id]
|
||||
# nids
|
||||
assert deck.findCards("nid:54321") == []
|
||||
assert len(deck.findCards("nid:%d"%f.id)) == 2
|
||||
assert len(deck.findCards("nid:%d" % f.id)) == 2
|
||||
assert len(deck.findCards("nid:%d,%d" % (f1id, f2id))) == 2
|
||||
# templates
|
||||
with assert_raises(Exception):
|
||||
with helper.assertRaises(Exception):
|
||||
deck.findCards("card:foo")
|
||||
assert len(deck.findCards("'card:card 1'")) == 4
|
||||
assert len(deck.findCards("card:reverse")) == 1
|
||||
|
@ -115,16 +126,16 @@ def test_findCards():
|
|||
assert len(deck.findCards("front:do")) == 0
|
||||
assert len(deck.findCards("front:*")) == 5
|
||||
# ordering
|
||||
deck.conf['sortType'] = "noteCrt"
|
||||
deck.conf["sortType"] = "noteCrt"
|
||||
assert deck.findCards("front:*", order=True)[-1] in latestCardIds
|
||||
assert deck.findCards("", order=True)[-1] in latestCardIds
|
||||
deck.conf['sortType'] = "noteFld"
|
||||
deck.conf["sortType"] = "noteFld"
|
||||
assert deck.findCards("", order=True)[0] == catCard.id
|
||||
assert deck.findCards("", order=True)[-1] in latestCardIds
|
||||
deck.conf['sortType'] = "cardMod"
|
||||
deck.conf["sortType"] = "cardMod"
|
||||
assert deck.findCards("", order=True)[-1] in latestCardIds
|
||||
assert deck.findCards("", order=True)[0] == firstCardId
|
||||
deck.conf['sortBackwards'] = True
|
||||
deck.conf["sortBackwards"] = True
|
||||
assert deck.findCards("", order=True)[0] in latestCardIds
|
||||
# model
|
||||
assert len(deck.findCards("note:basic")) == 5
|
||||
|
@ -136,29 +147,30 @@ def test_findCards():
|
|||
assert len(deck.findCards("-deck:foo")) == 5
|
||||
assert len(deck.findCards("deck:def*")) == 5
|
||||
assert len(deck.findCards("deck:*EFAULT")) == 5
|
||||
with assert_raises(Exception):
|
||||
with helper.assertRaises(Exception):
|
||||
deck.findCards("deck:*cefault")
|
||||
# full search
|
||||
f = deck.newNote()
|
||||
f['Front'] = 'hello<b>world</b>'
|
||||
f['Back'] = 'abc'
|
||||
f["Front"] = "hello<b>world</b>"
|
||||
f["Back"] = "abc"
|
||||
deck.addNote(f)
|
||||
# as it's the sort field, it matches
|
||||
assert len(deck.findCards("helloworld")) == 2
|
||||
#assert len(deck.findCards("helloworld", full=True)) == 2
|
||||
# assert len(deck.findCards("helloworld", full=True)) == 2
|
||||
# if we put it on the back, it won't
|
||||
(f['Front'], f['Back']) = (f['Back'], f['Front'])
|
||||
(f["Front"], f["Back"]) = (f["Back"], f["Front"])
|
||||
f.flush()
|
||||
assert len(deck.findCards("helloworld")) == 0
|
||||
#assert len(deck.findCards("helloworld", full=True)) == 2
|
||||
#assert len(deck.findCards("back:helloworld", full=True)) == 2
|
||||
# assert len(deck.findCards("helloworld", full=True)) == 2
|
||||
# assert len(deck.findCards("back:helloworld", full=True)) == 2
|
||||
# searching for an invalid special tag should not error
|
||||
with assert_raises(Exception):
|
||||
with helper.assertRaises(Exception):
|
||||
len(deck.findCards("is:invalid"))
|
||||
# should be able to limit to parent deck, no children
|
||||
id = deck.db.scalar("select id from cards limit 1")
|
||||
deck.db.execute("update cards set did = ? where id = ?",
|
||||
deck.decks.id("Default::Child"), id)
|
||||
deck.db.execute(
|
||||
"update cards set did = ? where id = ?", deck.decks.id("Default::Child"), id
|
||||
)
|
||||
assert len(deck.findCards("deck:default")) == 7
|
||||
assert len(deck.findCards("deck:default::child")) == 1
|
||||
assert len(deck.findCards("deck:default -deck:default::*")) == 6
|
||||
|
@ -166,7 +178,9 @@ def test_findCards():
|
|||
id = deck.db.scalar("select id from cards limit 1")
|
||||
deck.db.execute(
|
||||
"update cards set queue=2, ivl=10, reps=20, due=30, factor=2200 "
|
||||
"where id = ?", id)
|
||||
"where id = ?",
|
||||
id,
|
||||
)
|
||||
assert len(deck.findCards("prop:ivl>5")) == 1
|
||||
assert len(deck.findCards("prop:ivl<5")) > 1
|
||||
assert len(deck.findCards("prop:ivl>=5")) == 1
|
||||
|
@ -205,8 +219,8 @@ def test_findCards():
|
|||
# empty field
|
||||
assert len(deck.findCards("front:")) == 0
|
||||
f = deck.newNote()
|
||||
f['Front'] = ''
|
||||
f['Back'] = 'abc2'
|
||||
f["Front"] = ""
|
||||
f["Back"] = "abc2"
|
||||
assert deck.addNote(f) == 1
|
||||
assert len(deck.findCards("front:")) == 1
|
||||
# OR searches and nesting
|
||||
|
@ -220,60 +234,67 @@ def test_findCards():
|
|||
assert len(deck.findCards("(()")) == 0
|
||||
# added
|
||||
assert len(deck.findCards("added:0")) == 0
|
||||
deck.db.execute("update cards set id = id - 86400*1000 where id = ?",
|
||||
id)
|
||||
deck.db.execute("update cards set id = id - 86400*1000 where id = ?", id)
|
||||
assert len(deck.findCards("added:1")) == deck.cardCount() - 1
|
||||
assert len(deck.findCards("added:2")) == deck.cardCount()
|
||||
# flag
|
||||
with assert_raises(Exception):
|
||||
with helper.assertRaises(Exception):
|
||||
deck.findCards("flag:01")
|
||||
with assert_raises(Exception):
|
||||
with helper.assertRaises(Exception):
|
||||
deck.findCards("flag:12")
|
||||
|
||||
|
||||
def test_findReplace():
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = 'foo'
|
||||
f['Back'] = 'bar'
|
||||
f["Front"] = "foo"
|
||||
f["Back"] = "bar"
|
||||
deck.addNote(f)
|
||||
f2 = deck.newNote()
|
||||
f2['Front'] = 'baz'
|
||||
f2['Back'] = 'foo'
|
||||
f2["Front"] = "baz"
|
||||
f2["Back"] = "foo"
|
||||
deck.addNote(f2)
|
||||
nids = [f.id, f2.id]
|
||||
# should do nothing
|
||||
assert deck.findReplace(nids, "abc", "123") == 0
|
||||
# global replace
|
||||
assert deck.findReplace(nids, "foo", "qux") == 2
|
||||
f.load(); assert f['Front'] == "qux"
|
||||
f2.load(); assert f2['Back'] == "qux"
|
||||
f.load()
|
||||
assert f["Front"] == "qux"
|
||||
f2.load()
|
||||
assert f2["Back"] == "qux"
|
||||
# single field replace
|
||||
assert deck.findReplace(nids, "qux", "foo", field="Front") == 1
|
||||
f.load(); assert f['Front'] == "foo"
|
||||
f2.load(); assert f2['Back'] == "qux"
|
||||
f.load()
|
||||
assert f["Front"] == "foo"
|
||||
f2.load()
|
||||
assert f2["Back"] == "qux"
|
||||
# regex replace
|
||||
assert deck.findReplace(nids, "B.r", "reg") == 0
|
||||
f.load(); assert f['Back'] != "reg"
|
||||
f.load()
|
||||
assert f["Back"] != "reg"
|
||||
assert deck.findReplace(nids, "B.r", "reg", regex=True) == 1
|
||||
f.load(); assert f['Back'] == "reg"
|
||||
f.load()
|
||||
assert f["Back"] == "reg"
|
||||
|
||||
|
||||
def test_findDupes():
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = 'foo'
|
||||
f['Back'] = 'bar'
|
||||
f["Front"] = "foo"
|
||||
f["Back"] = "bar"
|
||||
deck.addNote(f)
|
||||
f2 = deck.newNote()
|
||||
f2['Front'] = 'baz'
|
||||
f2['Back'] = 'bar'
|
||||
f2["Front"] = "baz"
|
||||
f2["Back"] = "bar"
|
||||
deck.addNote(f2)
|
||||
f3 = deck.newNote()
|
||||
f3['Front'] = 'quux'
|
||||
f3['Back'] = 'bar'
|
||||
f3["Front"] = "quux"
|
||||
f3["Back"] = "bar"
|
||||
deck.addNote(f3)
|
||||
f4 = deck.newNote()
|
||||
f4['Front'] = 'quuux'
|
||||
f4['Back'] = 'nope'
|
||||
f4["Front"] = "quuux"
|
||||
f4["Back"] = "nope"
|
||||
deck.addNote(f4)
|
||||
r = deck.findDupes("Back")
|
||||
assert r[0][0] == "bar"
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from tests.shared import assertException, getEmptyCol
|
||||
|
||||
|
||||
def test_flags():
|
||||
col = getEmptyCol()
|
||||
n = col.newNote()
|
||||
n['Front'] = "one"; n['Back'] = "two"
|
||||
n["Front"] = "one"
|
||||
n["Back"] = "two"
|
||||
cnt = col.addNote(n)
|
||||
c = n.cards()[0]
|
||||
# make sure higher bits are preserved
|
||||
|
|
|
@ -1,22 +1,29 @@
|
|||
# coding: utf-8
|
||||
|
||||
import os
|
||||
from tests.shared import getUpgradeDeckPath, getEmptyCol
|
||||
import os
|
||||
|
||||
from anki.importing import (
|
||||
Anki2Importer,
|
||||
AnkiPackageImporter,
|
||||
MnemosyneImporter,
|
||||
SupermemoXmlImporter,
|
||||
TextImporter,
|
||||
)
|
||||
from anki.utils import ids2str
|
||||
from anki.importing import Anki2Importer, TextImporter, \
|
||||
SupermemoXmlImporter, MnemosyneImporter, AnkiPackageImporter
|
||||
from tests.shared import getEmptyCol, getUpgradeDeckPath
|
||||
|
||||
testDir = os.path.dirname(__file__)
|
||||
|
||||
srcNotes=None
|
||||
srcCards=None
|
||||
srcNotes = None
|
||||
srcCards = None
|
||||
|
||||
|
||||
def test_anki2_mediadupes():
|
||||
tmp = getEmptyCol()
|
||||
# add a note that references a sound
|
||||
n = tmp.newNote()
|
||||
n['Front'] = "[sound:foo.mp3]"
|
||||
mid = n.model()['id']
|
||||
n["Front"] = "[sound:foo.mp3]"
|
||||
mid = n.model()["id"]
|
||||
tmp.addNote(n)
|
||||
# add that sound to media folder
|
||||
with open(os.path.join(tmp.media.dir(), "foo.mp3"), "w") as f:
|
||||
|
@ -41,8 +48,7 @@ def test_anki2_mediadupes():
|
|||
f.write("bar")
|
||||
imp = Anki2Importer(empty, tmp.path)
|
||||
imp.run()
|
||||
assert sorted(os.listdir(empty.media.dir())) == [
|
||||
"foo.mp3", "foo_%s.mp3" % mid]
|
||||
assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
|
||||
n = empty.getNote(empty.db.scalar("select id from notes"))
|
||||
assert "_" in n.fields[0]
|
||||
# if the localized media file already exists, we rewrite the note and
|
||||
|
@ -52,25 +58,24 @@ def test_anki2_mediadupes():
|
|||
f.write("bar")
|
||||
imp = Anki2Importer(empty, tmp.path)
|
||||
imp.run()
|
||||
assert sorted(os.listdir(empty.media.dir())) == [
|
||||
"foo.mp3", "foo_%s.mp3" % mid]
|
||||
assert sorted(os.listdir(empty.media.dir())) == [
|
||||
"foo.mp3", "foo_%s.mp3" % mid]
|
||||
assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
|
||||
assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
|
||||
n = empty.getNote(empty.db.scalar("select id from notes"))
|
||||
assert "_" in n.fields[0]
|
||||
|
||||
|
||||
def test_apkg():
|
||||
tmp = getEmptyCol()
|
||||
apkg = str(os.path.join(testDir, "support/media.apkg"))
|
||||
imp = AnkiPackageImporter(tmp, apkg)
|
||||
assert os.listdir(tmp.media.dir()) == []
|
||||
imp.run()
|
||||
assert os.listdir(tmp.media.dir()) == ['foo.wav']
|
||||
assert os.listdir(tmp.media.dir()) == ["foo.wav"]
|
||||
# importing again should be idempotent in terms of media
|
||||
tmp.remCards(tmp.db.list("select id from cards"))
|
||||
imp = AnkiPackageImporter(tmp, apkg)
|
||||
imp.run()
|
||||
assert os.listdir(tmp.media.dir()) == ['foo.wav']
|
||||
assert os.listdir(tmp.media.dir()) == ["foo.wav"]
|
||||
# but if the local file has different data, it will rename
|
||||
tmp.remCards(tmp.db.list("select id from cards"))
|
||||
with open(os.path.join(tmp.media.dir(), "foo.wav"), "w") as f:
|
||||
|
@ -79,6 +84,7 @@ def test_apkg():
|
|||
imp.run()
|
||||
assert len(os.listdir(tmp.media.dir())) == 2
|
||||
|
||||
|
||||
def test_anki2_diffmodel_templates():
|
||||
# different from the above as this one tests only the template text being
|
||||
# changed, not the number of cards/fields
|
||||
|
@ -94,11 +100,12 @@ def test_anki2_diffmodel_templates():
|
|||
imp.dupeOnSchemaChange = True
|
||||
imp.run()
|
||||
# collection should contain the note we imported
|
||||
assert(dst.noteCount() == 1)
|
||||
assert dst.noteCount() == 1
|
||||
# the front template should contain the text added in the 2nd package
|
||||
tcid = dst.findCards("")[0] # only 1 note in collection
|
||||
tcid = dst.findCards("")[0] # only 1 note in collection
|
||||
tnote = dst.getCard(tcid).note()
|
||||
assert("Changed Front Template" in dst.findTemplates(tnote)[0]['qfmt'])
|
||||
assert "Changed Front Template" in dst.findTemplates(tnote)[0]["qfmt"]
|
||||
|
||||
|
||||
def test_anki2_updates():
|
||||
# create a new empty deck
|
||||
|
@ -127,6 +134,7 @@ def test_anki2_updates():
|
|||
assert dst.noteCount() == 1
|
||||
assert dst.db.scalar("select flds from notes").startswith("goodbye")
|
||||
|
||||
|
||||
def test_csv():
|
||||
deck = getEmptyCol()
|
||||
file = str(os.path.join(testDir, "support/text-2fields.txt"))
|
||||
|
@ -147,7 +155,7 @@ def test_csv():
|
|||
n.flush()
|
||||
i.run()
|
||||
n.load()
|
||||
assert n.tags == ['test']
|
||||
assert n.tags == ["test"]
|
||||
# if add-only mode, count will be 0
|
||||
i.importMode = 1
|
||||
i.run()
|
||||
|
@ -161,6 +169,7 @@ def test_csv():
|
|||
assert deck.cardCount() == 11
|
||||
deck.close()
|
||||
|
||||
|
||||
def test_csv2():
|
||||
deck = getEmptyCol()
|
||||
mm = deck.models
|
||||
|
@ -169,9 +178,9 @@ def test_csv2():
|
|||
mm.addField(m, f)
|
||||
mm.save(m)
|
||||
n = deck.newNote()
|
||||
n['Front'] = "1"
|
||||
n['Back'] = "2"
|
||||
n['Three'] = "3"
|
||||
n["Front"] = "1"
|
||||
n["Back"] = "2"
|
||||
n["Three"] = "3"
|
||||
deck.addNote(n)
|
||||
# an update with unmapped fields should not clobber those fields
|
||||
file = str(os.path.join(testDir, "support/text-update.txt"))
|
||||
|
@ -179,16 +188,17 @@ def test_csv2():
|
|||
i.initMapping()
|
||||
i.run()
|
||||
n.load()
|
||||
assert n['Front'] == "1"
|
||||
assert n['Back'] == "x"
|
||||
assert n['Three'] == "3"
|
||||
assert n["Front"] == "1"
|
||||
assert n["Back"] == "x"
|
||||
assert n["Three"] == "3"
|
||||
deck.close()
|
||||
|
||||
|
||||
def test_supermemo_xml_01_unicode():
|
||||
deck = getEmptyCol()
|
||||
file = str(os.path.join(testDir, "support/supermemo1.xml"))
|
||||
i = SupermemoXmlImporter(deck, file)
|
||||
#i.META.logToStdOutput = True
|
||||
# i.META.logToStdOutput = True
|
||||
i.run()
|
||||
assert i.total == 1
|
||||
cid = deck.db.scalar("select id from cards")
|
||||
|
@ -198,6 +208,7 @@ def test_supermemo_xml_01_unicode():
|
|||
assert c.reps == 7
|
||||
deck.close()
|
||||
|
||||
|
||||
def test_mnemo():
|
||||
deck = getEmptyCol()
|
||||
file = str(os.path.join(testDir, "support/mnemo.db"))
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
# coding: utf-8
|
||||
|
||||
import os
|
||||
|
||||
import shutil
|
||||
|
||||
from tests.shared import getEmptyCol
|
||||
from anki.utils import stripHTML
|
||||
from tests.shared import getEmptyCol
|
||||
|
||||
|
||||
def test_latex():
|
||||
d = getEmptyCol()
|
||||
# change latex cmd to simulate broken build
|
||||
import anki.latex
|
||||
|
||||
anki.latex.pngCommands[0][0] = "nolatex"
|
||||
# add a note with latex
|
||||
f = d.newNote()
|
||||
f['Front'] = "[latex]hello[/latex]"
|
||||
f["Front"] = "[latex]hello[/latex]"
|
||||
d.addNote(f)
|
||||
# but since latex couldn't run, there's nothing there
|
||||
assert len(os.listdir(d.media.dir())) == 0
|
||||
|
@ -34,13 +35,13 @@ def test_latex():
|
|||
assert ".png" in f.cards()[0].q()
|
||||
# adding new notes should cause generation on question display
|
||||
f = d.newNote()
|
||||
f['Front'] = "[latex]world[/latex]"
|
||||
f["Front"] = "[latex]world[/latex]"
|
||||
d.addNote(f)
|
||||
f.cards()[0].q()
|
||||
assert len(os.listdir(d.media.dir())) == 2
|
||||
# another note with the same media should reuse
|
||||
f = d.newNote()
|
||||
f['Front'] = " [latex]world[/latex]"
|
||||
f["Front"] = " [latex]world[/latex]"
|
||||
d.addNote(f)
|
||||
assert len(os.listdir(d.media.dir())) == 2
|
||||
oldcard = f.cards()[0]
|
||||
|
@ -49,7 +50,7 @@ def test_latex():
|
|||
# missing media will show the latex
|
||||
anki.latex.build = False
|
||||
f = d.newNote()
|
||||
f['Front'] = "[latex]foo[/latex]"
|
||||
f["Front"] = "[latex]foo[/latex]"
|
||||
d.addNote(f)
|
||||
assert len(os.listdir(d.media.dir())) == 2
|
||||
assert stripHTML(f.cards()[0].q()) == "[latex]foo[/latex]"
|
||||
|
@ -86,10 +87,11 @@ def test_latex():
|
|||
(result, msg) = _test_includes_bad_command("\\emph")
|
||||
assert not result, msg
|
||||
|
||||
|
||||
def _test_includes_bad_command(bad):
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = '[latex]%s[/latex]' % bad
|
||||
f["Front"] = "[latex]%s[/latex]" % bad
|
||||
d.addNote(f)
|
||||
q = f.cards()[0].q()
|
||||
return ("'%s' is not allowed on cards" % bad in q, "Card content: %s" % q)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# coding: utf-8
|
||||
|
||||
import tempfile
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from .shared import getEmptyCol, testDir
|
||||
|
@ -23,6 +23,7 @@ def test_add():
|
|||
f.write("world")
|
||||
assert d.media.addFile(path) == "foo (1).jpg"
|
||||
|
||||
|
||||
def test_strings():
|
||||
d = getEmptyCol()
|
||||
mf = d.media.filesInStr
|
||||
|
@ -31,12 +32,16 @@ def test_strings():
|
|||
assert mf(mid, "aoeu<img src='foo.jpg'>ao") == ["foo.jpg"]
|
||||
assert mf(mid, "aoeu<img src='foo.jpg' style='test'>ao") == ["foo.jpg"]
|
||||
assert mf(mid, "aoeu<img src='foo.jpg'><img src=\"bar.jpg\">ao") == [
|
||||
"foo.jpg", "bar.jpg"]
|
||||
"foo.jpg",
|
||||
"bar.jpg",
|
||||
]
|
||||
assert mf(mid, "aoeu<img src=foo.jpg style=bar>ao") == ["foo.jpg"]
|
||||
assert mf(mid, "<img src=one><img src=two>") == ["one", "two"]
|
||||
assert mf(mid, "aoeu<img src=\"foo.jpg\">ao") == ["foo.jpg"]
|
||||
assert mf(mid, "aoeu<img src=\"foo.jpg\"><img class=yo src=fo>ao") == [
|
||||
"foo.jpg", "fo"]
|
||||
assert mf(mid, 'aoeu<img src="foo.jpg">ao') == ["foo.jpg"]
|
||||
assert mf(mid, 'aoeu<img src="foo.jpg"><img class=yo src=fo>ao') == [
|
||||
"foo.jpg",
|
||||
"fo",
|
||||
]
|
||||
assert mf(mid, "aou[sound:foo.mp3]aou") == ["foo.mp3"]
|
||||
sp = d.media.strip
|
||||
assert sp("aoeu") == "aoeu"
|
||||
|
@ -47,6 +52,7 @@ def test_strings():
|
|||
assert es("<img src='http://foo.com'>") == "<img src='http://foo.com'>"
|
||||
assert es('<img src="foo bar.jpg">') == '<img src="foo%20bar.jpg">'
|
||||
|
||||
|
||||
def test_deckIntegration():
|
||||
d = getEmptyCol()
|
||||
# create a media dir
|
||||
|
@ -56,11 +62,13 @@ def test_deckIntegration():
|
|||
d.media.addFile(file)
|
||||
# add a note which references it
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "<img src='fake.png'>"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "<img src='fake.png'>"
|
||||
d.addNote(f)
|
||||
# and one which references a non-existent file
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "<img src='fake2.png'>"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "<img src='fake2.png'>"
|
||||
d.addNote(f)
|
||||
# and add another file which isn't used
|
||||
with open(os.path.join(d.media.dir(), "foo.jpg"), "w") as f:
|
||||
|
@ -70,12 +78,16 @@ def test_deckIntegration():
|
|||
assert ret[0] == ["fake2.png"]
|
||||
assert ret[1] == ["foo.jpg"]
|
||||
|
||||
|
||||
def test_changes():
|
||||
d = getEmptyCol()
|
||||
|
||||
def added():
|
||||
return d.media.db.execute("select fname from media where csum is not null")
|
||||
|
||||
def removed():
|
||||
return d.media.db.execute("select fname from media where csum is null")
|
||||
|
||||
assert not list(added())
|
||||
assert not list(removed())
|
||||
# add a file
|
||||
|
@ -97,26 +109,27 @@ def test_changes():
|
|||
assert not list(removed())
|
||||
# but if we add another file, it will
|
||||
time.sleep(1)
|
||||
with open(path+"2", "w") as f:
|
||||
with open(path + "2", "w") as f:
|
||||
f.write("yo")
|
||||
d.media.findChanges()
|
||||
assert len(list(added())) == 2
|
||||
assert not list(removed())
|
||||
# deletions should get noticed too
|
||||
time.sleep(1)
|
||||
os.unlink(path+"2")
|
||||
os.unlink(path + "2")
|
||||
d.media.findChanges()
|
||||
assert len(list(added())) == 1
|
||||
assert len(list(removed())) == 1
|
||||
|
||||
|
||||
def test_illegal():
|
||||
d = getEmptyCol()
|
||||
aString = "a:b|cd\\e/f\0g*h"
|
||||
good = "abcdefgh"
|
||||
assert d.media.stripIllegal(aString) == good
|
||||
for c in aString:
|
||||
bad = d.media.hasIllegal("somestring"+c+"morestring")
|
||||
bad = d.media.hasIllegal("somestring" + c + "morestring")
|
||||
if bad:
|
||||
assert(c not in good)
|
||||
assert c not in good
|
||||
else:
|
||||
assert(c in good)
|
||||
assert c in good
|
||||
|
|
|
@ -1,43 +1,47 @@
|
|||
# coding: utf-8
|
||||
import time
|
||||
|
||||
from tests.shared import getEmptyCol
|
||||
from anki.consts import MODEL_CLOZE
|
||||
from anki.utils import stripHTML, joinFields
|
||||
import anki.template
|
||||
from anki.consts import MODEL_CLOZE
|
||||
from anki.utils import isWin, joinFields, stripHTML
|
||||
from tests.shared import getEmptyCol
|
||||
|
||||
|
||||
def test_modelDelete():
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = '1'
|
||||
f['Back'] = '2'
|
||||
f["Front"] = "1"
|
||||
f["Back"] = "2"
|
||||
deck.addNote(f)
|
||||
assert deck.cardCount() == 1
|
||||
deck.models.rem(deck.models.current())
|
||||
assert deck.cardCount() == 0
|
||||
|
||||
|
||||
def test_modelCopy():
|
||||
deck = getEmptyCol()
|
||||
m = deck.models.current()
|
||||
m2 = deck.models.copy(m)
|
||||
assert m2['name'] == "Basic copy"
|
||||
assert m2['id'] != m['id']
|
||||
assert len(m2['flds']) == 2
|
||||
assert len(m['flds']) == 2
|
||||
assert len(m2['flds']) == len(m['flds'])
|
||||
assert len(m['tmpls']) == 1
|
||||
assert len(m2['tmpls']) == 1
|
||||
assert m2["name"] == "Basic copy"
|
||||
assert m2["id"] != m["id"]
|
||||
assert len(m2["flds"]) == 2
|
||||
assert len(m["flds"]) == 2
|
||||
assert len(m2["flds"]) == len(m["flds"])
|
||||
assert len(m["tmpls"]) == 1
|
||||
assert len(m2["tmpls"]) == 1
|
||||
assert deck.models.scmhash(m) == deck.models.scmhash(m2)
|
||||
|
||||
|
||||
def test_fields():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = '1'
|
||||
f['Back'] = '2'
|
||||
f["Front"] = "1"
|
||||
f["Back"] = "2"
|
||||
d.addNote(f)
|
||||
m = d.models.current()
|
||||
# make sure renaming a field updates the templates
|
||||
d.models.renameField(m, m['flds'][0], "NewFront")
|
||||
assert "{{NewFront}}" in m['tmpls'][0]['qfmt']
|
||||
d.models.renameField(m, m["flds"][0], "NewFront")
|
||||
assert "{{NewFront}}" in m["tmpls"][0]["qfmt"]
|
||||
h = d.models.scmhash(m)
|
||||
# add a field
|
||||
f = d.models.newField("foo")
|
||||
|
@ -46,44 +50,46 @@ def test_fields():
|
|||
assert d.models.scmhash(m) != h
|
||||
# rename it
|
||||
d.models.renameField(m, f, "bar")
|
||||
assert d.getNote(d.models.nids(m)[0])['bar'] == ''
|
||||
assert d.getNote(d.models.nids(m)[0])["bar"] == ""
|
||||
# delete back
|
||||
d.models.remField(m, m['flds'][1])
|
||||
d.models.remField(m, m["flds"][1])
|
||||
assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""]
|
||||
# move 0 -> 1
|
||||
d.models.moveField(m, m['flds'][0], 1)
|
||||
d.models.moveField(m, m["flds"][0], 1)
|
||||
assert d.getNote(d.models.nids(m)[0]).fields == ["", "1"]
|
||||
# move 1 -> 0
|
||||
d.models.moveField(m, m['flds'][1], 0)
|
||||
d.models.moveField(m, m["flds"][1], 0)
|
||||
assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""]
|
||||
# add another and put in middle
|
||||
f = d.models.newField("baz")
|
||||
d.models.addField(m, f)
|
||||
f = d.getNote(d.models.nids(m)[0])
|
||||
f['baz'] = "2"
|
||||
f["baz"] = "2"
|
||||
f.flush()
|
||||
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "", "2"]
|
||||
# move 2 -> 1
|
||||
d.models.moveField(m, m['flds'][2], 1)
|
||||
d.models.moveField(m, m["flds"][2], 1)
|
||||
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "2", ""]
|
||||
# move 0 -> 2
|
||||
d.models.moveField(m, m['flds'][0], 2)
|
||||
d.models.moveField(m, m["flds"][0], 2)
|
||||
assert d.getNote(d.models.nids(m)[0]).fields == ["2", "", "1"]
|
||||
# move 0 -> 1
|
||||
d.models.moveField(m, m['flds'][0], 1)
|
||||
d.models.moveField(m, m["flds"][0], 1)
|
||||
assert d.getNote(d.models.nids(m)[0]).fields == ["", "2", "1"]
|
||||
|
||||
|
||||
def test_templates():
|
||||
d = getEmptyCol()
|
||||
m = d.models.current(); mm = d.models
|
||||
m = d.models.current()
|
||||
mm = d.models
|
||||
t = mm.newTemplate("Reverse")
|
||||
t['qfmt'] = "{{Back}}"
|
||||
t['afmt'] = "{{Front}}"
|
||||
t["qfmt"] = "{{Back}}"
|
||||
t["afmt"] = "{{Front}}"
|
||||
mm.addTemplate(m, t)
|
||||
mm.save(m)
|
||||
f = d.newNote()
|
||||
f['Front'] = '1'
|
||||
f['Back'] = '2'
|
||||
f["Front"] = "1"
|
||||
f["Back"] = "2"
|
||||
d.addNote(f)
|
||||
assert d.cardCount() == 2
|
||||
(c, c2) = f.cards()
|
||||
|
@ -92,11 +98,12 @@ def test_templates():
|
|||
assert c2.ord == 1
|
||||
# switch templates
|
||||
d.models.moveTemplate(m, c.template(), 1)
|
||||
c.load(); c2.load()
|
||||
c.load()
|
||||
c2.load()
|
||||
assert c.ord == 1
|
||||
assert c2.ord == 0
|
||||
# removing a template should delete its cards
|
||||
assert d.models.remTemplate(m, m['tmpls'][0])
|
||||
assert d.models.remTemplate(m, m["tmpls"][0])
|
||||
assert d.cardCount() == 1
|
||||
# and should have updated the other cards' ordinals
|
||||
c = f.cards()[0]
|
||||
|
@ -105,64 +112,67 @@ def test_templates():
|
|||
# it shouldn't be possible to orphan notes by removing templates
|
||||
t = mm.newTemplate("template name")
|
||||
mm.addTemplate(m, t)
|
||||
assert not d.models.remTemplate(m, m['tmpls'][0])
|
||||
assert not d.models.remTemplate(m, m["tmpls"][0])
|
||||
|
||||
|
||||
def test_cloze_ordinals():
|
||||
d = getEmptyCol()
|
||||
d.models.setCurrent(d.models.byName("Cloze"))
|
||||
m = d.models.current(); mm = d.models
|
||||
|
||||
#We replace the default Cloze template
|
||||
m = d.models.current()
|
||||
mm = d.models
|
||||
|
||||
# We replace the default Cloze template
|
||||
t = mm.newTemplate("ChainedCloze")
|
||||
t['qfmt'] = "{{text:cloze:Text}}"
|
||||
t['afmt'] = "{{text:cloze:Text}}"
|
||||
t["qfmt"] = "{{text:cloze:Text}}"
|
||||
t["afmt"] = "{{text:cloze:Text}}"
|
||||
mm.addTemplate(m, t)
|
||||
mm.save(m)
|
||||
d.models.remTemplate(m, m['tmpls'][0])
|
||||
|
||||
d.models.remTemplate(m, m["tmpls"][0])
|
||||
|
||||
f = d.newNote()
|
||||
f['Text'] = '{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}'
|
||||
f["Text"] = "{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}"
|
||||
d.addNote(f)
|
||||
assert d.cardCount() == 2
|
||||
(c, c2) = f.cards()
|
||||
# first card should have first ord
|
||||
assert c.ord == 0
|
||||
assert c2.ord == 1
|
||||
|
||||
|
||||
|
||||
def test_text():
|
||||
d = getEmptyCol()
|
||||
m = d.models.current()
|
||||
m['tmpls'][0]['qfmt'] = "{{text:Front}}"
|
||||
m["tmpls"][0]["qfmt"] = "{{text:Front}}"
|
||||
d.models.save(m)
|
||||
f = d.newNote()
|
||||
f['Front'] = 'hello<b>world'
|
||||
f["Front"] = "hello<b>world"
|
||||
d.addNote(f)
|
||||
assert "helloworld" in f.cards()[0].q()
|
||||
|
||||
|
||||
def test_cloze():
|
||||
d = getEmptyCol()
|
||||
d.models.setCurrent(d.models.byName("Cloze"))
|
||||
f = d.newNote()
|
||||
assert f.model()['name'] == "Cloze"
|
||||
assert f.model()["name"] == "Cloze"
|
||||
# a cloze model with no clozes is not empty
|
||||
f['Text'] = 'nothing'
|
||||
f["Text"] = "nothing"
|
||||
assert d.addNote(f)
|
||||
# try with one cloze
|
||||
f = d.newNote()
|
||||
f['Text'] = "hello {{c1::world}}"
|
||||
f["Text"] = "hello {{c1::world}}"
|
||||
assert d.addNote(f) == 1
|
||||
assert "hello <span class=cloze>[...]</span>" in f.cards()[0].q()
|
||||
assert "hello <span class=cloze>world</span>" in f.cards()[0].a()
|
||||
# and with a comment
|
||||
f = d.newNote()
|
||||
f['Text'] = "hello {{c1::world::typical}}"
|
||||
f["Text"] = "hello {{c1::world::typical}}"
|
||||
assert d.addNote(f) == 1
|
||||
assert "<span class=cloze>[typical]</span>" in f.cards()[0].q()
|
||||
assert "<span class=cloze>world</span>" in f.cards()[0].a()
|
||||
# and with 2 clozes
|
||||
f = d.newNote()
|
||||
f['Text'] = "hello {{c1::world}} {{c2::bar}}"
|
||||
f["Text"] = "hello {{c1::world}} {{c2::bar}}"
|
||||
assert d.addNote(f) == 2
|
||||
(c1, c2) = f.cards()
|
||||
assert "<span class=cloze>[...]</span> bar" in c1.q()
|
||||
|
@ -172,25 +182,27 @@ def test_cloze():
|
|||
# if there are multiple answers for a single cloze, they are given in a
|
||||
# list
|
||||
f = d.newNote()
|
||||
f['Text'] = "a {{c1::b}} {{c1::c}}"
|
||||
f["Text"] = "a {{c1::b}} {{c1::c}}"
|
||||
assert d.addNote(f) == 1
|
||||
assert "<span class=cloze>b</span> <span class=cloze>c</span>" in (
|
||||
f.cards()[0].a())
|
||||
assert "<span class=cloze>b</span> <span class=cloze>c</span>" in (f.cards()[0].a())
|
||||
# if we add another cloze, a card should be generated
|
||||
cnt = d.cardCount()
|
||||
f['Text'] = "{{c2::hello}} {{c1::foo}}"
|
||||
f["Text"] = "{{c2::hello}} {{c1::foo}}"
|
||||
f.flush()
|
||||
assert d.cardCount() == cnt + 1
|
||||
# 0 or negative indices are not supported
|
||||
f['Text'] += "{{c0::zero}} {{c-1:foo}}"
|
||||
f["Text"] += "{{c0::zero}} {{c-1:foo}}"
|
||||
f.flush()
|
||||
assert len(f.cards()) == 2
|
||||
|
||||
|
||||
def test_cloze_mathjax():
|
||||
d = getEmptyCol()
|
||||
d.models.setCurrent(d.models.byName("Cloze"))
|
||||
f = d.newNote()
|
||||
f['Text'] = r'{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) {{c4::blah}} {{c5::text with \(x^2\) jax}}'
|
||||
f[
|
||||
"Text"
|
||||
] = r"{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) {{c4::blah}} {{c5::text with \(x^2\) jax}}"
|
||||
assert d.addNote(f)
|
||||
assert len(f.cards()) == 5
|
||||
assert "class=cloze" in f.cards()[0].q()
|
||||
|
@ -200,56 +212,70 @@ def test_cloze_mathjax():
|
|||
assert "class=cloze" in f.cards()[4].q()
|
||||
|
||||
f = d.newNote()
|
||||
f['Text'] = r'\(a\) {{c1::b}} \[ {{c1::c}} \]'
|
||||
f["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
|
||||
assert d.addNote(f)
|
||||
assert len(f.cards()) == 1
|
||||
assert f.cards()[0].q().endswith('\(a\) <span class=cloze>[...]</span> \[ [...] \]')
|
||||
assert f.cards()[0].q().endswith("\(a\) <span class=cloze>[...]</span> \[ [...] \]")
|
||||
|
||||
|
||||
def test_chained_mods():
|
||||
d = getEmptyCol()
|
||||
d.models.setCurrent(d.models.byName("Cloze"))
|
||||
m = d.models.current(); mm = d.models
|
||||
|
||||
#We replace the default Cloze template
|
||||
m = d.models.current()
|
||||
mm = d.models
|
||||
|
||||
# We replace the default Cloze template
|
||||
t = mm.newTemplate("ChainedCloze")
|
||||
t['qfmt'] = "{{cloze:text:Text}}"
|
||||
t['afmt'] = "{{cloze:text:Text}}"
|
||||
t["qfmt"] = "{{cloze:text:Text}}"
|
||||
t["afmt"] = "{{cloze:text:Text}}"
|
||||
mm.addTemplate(m, t)
|
||||
mm.save(m)
|
||||
d.models.remTemplate(m, m['tmpls'][0])
|
||||
|
||||
d.models.remTemplate(m, m["tmpls"][0])
|
||||
|
||||
f = d.newNote()
|
||||
q1 = '<span style=\"color:red\">phrase</span>'
|
||||
a1 = '<b>sentence</b>'
|
||||
q2 = '<span style=\"color:red\">en chaine</span>'
|
||||
a2 = '<i>chained</i>'
|
||||
f['Text'] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (q1,a1,q2,a2)
|
||||
q1 = '<span style="color:red">phrase</span>'
|
||||
a1 = "<b>sentence</b>"
|
||||
q2 = '<span style="color:red">en chaine</span>'
|
||||
a2 = "<i>chained</i>"
|
||||
f["Text"] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (
|
||||
q1,
|
||||
a1,
|
||||
q2,
|
||||
a2,
|
||||
)
|
||||
assert d.addNote(f) == 1
|
||||
assert "This <span class=cloze>[sentence]</span> demonstrates <span class=cloze>[chained]</span> clozes." in f.cards()[0].q()
|
||||
assert "This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes." in f.cards()[0].a()
|
||||
assert (
|
||||
"This <span class=cloze>[sentence]</span> demonstrates <span class=cloze>[chained]</span> clozes."
|
||||
in f.cards()[0].q()
|
||||
)
|
||||
assert (
|
||||
"This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes."
|
||||
in f.cards()[0].a()
|
||||
)
|
||||
|
||||
|
||||
def test_modelChange():
|
||||
deck = getEmptyCol()
|
||||
basic = deck.models.byName("Basic")
|
||||
cloze = deck.models.byName("Cloze")
|
||||
# enable second template and add a note
|
||||
m = deck.models.current(); mm = deck.models
|
||||
m = deck.models.current()
|
||||
mm = deck.models
|
||||
t = mm.newTemplate("Reverse")
|
||||
t['qfmt'] = "{{Back}}"
|
||||
t['afmt'] = "{{Front}}"
|
||||
t["qfmt"] = "{{Back}}"
|
||||
t["afmt"] = "{{Front}}"
|
||||
mm.addTemplate(m, t)
|
||||
mm.save(m)
|
||||
f = deck.newNote()
|
||||
f['Front'] = 'f'
|
||||
f['Back'] = 'b123'
|
||||
f["Front"] = "f"
|
||||
f["Back"] = "b123"
|
||||
deck.addNote(f)
|
||||
# switch fields
|
||||
map = {0: 1, 1: 0}
|
||||
deck.models.change(basic, [f.id], basic, map, None)
|
||||
f.load()
|
||||
assert f['Front'] == 'b123'
|
||||
assert f['Back'] == 'f'
|
||||
assert f["Front"] == "b123"
|
||||
assert f["Back"] == "f"
|
||||
# switch cards
|
||||
c0 = f.cards()[0]
|
||||
c1 = f.cards()[1]
|
||||
|
@ -258,7 +284,9 @@ def test_modelChange():
|
|||
assert c0.ord == 0
|
||||
assert c1.ord == 1
|
||||
deck.models.change(basic, [f.id], basic, None, map)
|
||||
f.load(); c0.load(); c1.load()
|
||||
f.load()
|
||||
c0.load()
|
||||
c1.load()
|
||||
assert "f" in c0.q()
|
||||
assert "b123" in c1.q()
|
||||
assert c0.ord == 1
|
||||
|
@ -267,6 +295,9 @@ def test_modelChange():
|
|||
assert f.cards()[0].id == c1.id
|
||||
# delete first card
|
||||
map = {0: None, 1: 1}
|
||||
if isWin:
|
||||
# The low precision timer on Windows reveals a race condition
|
||||
time.sleep(0.05)
|
||||
deck.models.change(basic, [f.id], basic, None, map)
|
||||
f.load()
|
||||
c0.load()
|
||||
|
@ -279,30 +310,31 @@ def test_modelChange():
|
|||
# but we have two cards, as a new one was generated
|
||||
assert len(f.cards()) == 2
|
||||
# an unmapped field becomes blank
|
||||
assert f['Front'] == 'b123'
|
||||
assert f['Back'] == 'f'
|
||||
assert f["Front"] == "b123"
|
||||
assert f["Back"] == "f"
|
||||
deck.models.change(basic, [f.id], basic, map, None)
|
||||
f.load()
|
||||
assert f['Front'] == ''
|
||||
assert f['Back'] == 'f'
|
||||
assert f["Front"] == ""
|
||||
assert f["Back"] == "f"
|
||||
# another note to try model conversion
|
||||
f = deck.newNote()
|
||||
f['Front'] = 'f2'
|
||||
f['Back'] = 'b2'
|
||||
f["Front"] = "f2"
|
||||
f["Back"] = "b2"
|
||||
deck.addNote(f)
|
||||
assert deck.models.useCount(basic) == 2
|
||||
assert deck.models.useCount(cloze) == 0
|
||||
map = {0: 0, 1: 1}
|
||||
deck.models.change(basic, [f.id], cloze, map, map)
|
||||
f.load()
|
||||
assert f['Text'] == "f2"
|
||||
assert f["Text"] == "f2"
|
||||
assert len(f.cards()) == 2
|
||||
# back the other way, with deletion of second ord
|
||||
deck.models.remTemplate(basic, basic['tmpls'][1])
|
||||
deck.models.remTemplate(basic, basic["tmpls"][1])
|
||||
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 2
|
||||
deck.models.change(cloze, [f.id], basic, map, map)
|
||||
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1
|
||||
|
||||
|
||||
def test_templates():
|
||||
d = dict(Foo="x", Bar="y")
|
||||
assert anki.template.render("{{Foo}}", d) == "x"
|
||||
|
@ -311,68 +343,83 @@ def test_templates():
|
|||
assert anki.template.render("{{#Bar}}{{#Foo}}{{Foo}}{{/Foo}}{{/Bar}}", d) == "x"
|
||||
assert anki.template.render("{{#Baz}}{{#Foo}}{{Foo}}{{/Foo}}{{/Baz}}", d) == ""
|
||||
|
||||
|
||||
def test_availOrds():
|
||||
d = getEmptyCol()
|
||||
m = d.models.current(); mm = d.models
|
||||
t = m['tmpls'][0]
|
||||
m = d.models.current()
|
||||
mm = d.models
|
||||
t = m["tmpls"][0]
|
||||
f = d.newNote()
|
||||
f['Front'] = "1"
|
||||
f["Front"] = "1"
|
||||
# simple templates
|
||||
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
||||
t['qfmt'] = "{{Back}}"
|
||||
t["qfmt"] = "{{Back}}"
|
||||
mm.save(m, templates=True)
|
||||
assert not mm.availOrds(m, joinFields(f.fields))
|
||||
# AND
|
||||
t['qfmt'] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
|
||||
t["qfmt"] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
|
||||
mm.save(m, templates=True)
|
||||
assert not mm.availOrds(m, joinFields(f.fields))
|
||||
t['qfmt'] = "{{#Front}}\n{{#Back}}\n{{Front}}\n{{/Back}}\n{{/Front}}"
|
||||
t["qfmt"] = "{{#Front}}\n{{#Back}}\n{{Front}}\n{{/Back}}\n{{/Front}}"
|
||||
mm.save(m, templates=True)
|
||||
assert not mm.availOrds(m, joinFields(f.fields))
|
||||
# OR
|
||||
t['qfmt'] = "{{Front}}\n{{Back}}"
|
||||
t["qfmt"] = "{{Front}}\n{{Back}}"
|
||||
mm.save(m, templates=True)
|
||||
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
||||
t['Front'] = ""
|
||||
t['Back'] = "1"
|
||||
t["Front"] = ""
|
||||
t["Back"] = "1"
|
||||
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
||||
|
||||
|
||||
def test_req():
|
||||
def reqSize(model):
|
||||
if model['type'] == MODEL_CLOZE:
|
||||
if model["type"] == MODEL_CLOZE:
|
||||
return
|
||||
assert (len(model['tmpls']) == len(model['req']))
|
||||
assert len(model["tmpls"]) == len(model["req"])
|
||||
|
||||
d = getEmptyCol()
|
||||
mm = d.models
|
||||
basic = mm.byName("Basic")
|
||||
assert 'req' in basic
|
||||
assert "req" in basic
|
||||
reqSize(basic)
|
||||
assert basic['req'][0] == [0, 'all', [0]]
|
||||
r = basic["req"][0]
|
||||
assert r[0] == 0
|
||||
assert r[1] in ("any", "all")
|
||||
assert r[2] == [0]
|
||||
opt = mm.byName("Basic (optional reversed card)")
|
||||
reqSize(opt)
|
||||
assert opt['req'][0] == [0, 'all', [0]]
|
||||
assert opt['req'][1] == [1, 'all', [1, 2]]
|
||||
#testing any
|
||||
opt['tmpls'][1]['qfmt'] = "{{Back}}{{Add Reverse}}"
|
||||
r = opt["req"][0]
|
||||
assert r[1] in ("any", "all")
|
||||
assert r[2] == [0]
|
||||
assert opt["req"][1] == [1, "all", [1, 2]]
|
||||
# testing any
|
||||
opt["tmpls"][1]["qfmt"] = "{{Back}}{{Add Reverse}}"
|
||||
mm.save(opt, templates=True)
|
||||
assert opt['req'][1] == [1, 'any', [1, 2]]
|
||||
#testing None
|
||||
opt['tmpls'][1]['qfmt'] = "{{^Add Reverse}}{{Back}}{{/Add Reverse}}"
|
||||
assert opt["req"][1] == [1, "any", [1, 2]]
|
||||
# testing None
|
||||
opt["tmpls"][1]["qfmt"] = "{{^Add Reverse}}{{Back}}{{/Add Reverse}}"
|
||||
mm.save(opt, templates=True)
|
||||
assert opt['req'][1] == [1, 'none', []]
|
||||
assert opt["req"][1] == [1, "none", []]
|
||||
|
||||
def test_updatereqs_performance():
|
||||
import time
|
||||
d = getEmptyCol()
|
||||
mm = d.models
|
||||
m = mm.byName("Basic")
|
||||
for i in range(100):
|
||||
fld = mm.newField(f"field{i}")
|
||||
mm.addField(m, fld)
|
||||
tmpl = mm.newTemplate(f"template{i}")
|
||||
tmpl['qfmt'] = "{{field%s}}" % i
|
||||
mm.addTemplate(m, tmpl)
|
||||
t = time.time()
|
||||
mm.save(m, templates=True)
|
||||
print("took", (time.time()-t)*100)
|
||||
opt = mm.byName("Basic (type in the answer)")
|
||||
reqSize(opt)
|
||||
r = opt["req"][0]
|
||||
assert r[1] in ("any", "all")
|
||||
assert r[2] == [0]
|
||||
|
||||
|
||||
# def test_updatereqs_performance():
|
||||
# import time
|
||||
# d = getEmptyCol()
|
||||
# mm = d.models
|
||||
# m = mm.byName("Basic")
|
||||
# for i in range(100):
|
||||
# fld = mm.newField(f"field{i}")
|
||||
# mm.addField(m, fld)
|
||||
# tmpl = mm.newTemplate(f"template{i}")
|
||||
# tmpl['qfmt'] = "{{field%s}}" % i
|
||||
# mm.addTemplate(m, tmpl)
|
||||
# t = time.time()
|
||||
# mm.save(m, templates=True)
|
||||
# print("took", (time.time()-t)*100)
|
||||
|
|
|
@ -1,39 +1,45 @@
|
|||
# coding: utf-8
|
||||
|
||||
import time
|
||||
import copy
|
||||
import time
|
||||
|
||||
from anki.consts import STARTING_FACTOR
|
||||
from tests.shared import getEmptyCol as getEmptyColOrig
|
||||
from anki.utils import intTime
|
||||
from anki.hooks import addHook
|
||||
from anki.utils import intTime
|
||||
from tests.shared import getEmptyCol as getEmptyColOrig
|
||||
|
||||
|
||||
def getEmptyCol():
|
||||
col = getEmptyColOrig()
|
||||
col.changeSchedulerVer(1)
|
||||
return col
|
||||
|
||||
|
||||
def test_clock():
|
||||
d = getEmptyCol()
|
||||
if (d.sched.dayCutoff - intTime()) < 10*60:
|
||||
if (d.sched.dayCutoff - intTime()) < 10 * 60:
|
||||
raise Exception("Unit tests will fail around the day rollover.")
|
||||
|
||||
|
||||
def checkRevIvl(d, c, targetIvl):
|
||||
min, max = d.sched._fuzzIvlRange(targetIvl)
|
||||
return min <= c.ivl <= max
|
||||
|
||||
|
||||
def test_basics():
|
||||
d = getEmptyCol()
|
||||
d.reset()
|
||||
assert not d.sched.getCard()
|
||||
|
||||
|
||||
def test_new():
|
||||
d = getEmptyCol()
|
||||
d.reset()
|
||||
assert d.sched.newCount == 0
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
assert d.sched.newCount == 1
|
||||
|
@ -71,15 +77,16 @@ def test_new():
|
|||
# assert qs[n] in c.q()
|
||||
# d.sched.answerCard(c, 2)
|
||||
|
||||
|
||||
def test_newLimits():
|
||||
d = getEmptyCol()
|
||||
# add some notes
|
||||
g2 = d.decks.id("Default::foo")
|
||||
for i in range(30):
|
||||
f = d.newNote()
|
||||
f['Front'] = str(i)
|
||||
f["Front"] = str(i)
|
||||
if i > 4:
|
||||
f.model()['did'] = g2
|
||||
f.model()["did"] = g2
|
||||
d.addNote(f)
|
||||
# give the child deck a different configuration
|
||||
c2 = d.decks.confId("new conf")
|
||||
|
@ -92,33 +99,36 @@ def test_newLimits():
|
|||
assert c.did == 1
|
||||
# limit the parent to 10 cards, meaning we get 10 in total
|
||||
conf1 = d.decks.confForDid(1)
|
||||
conf1['new']['perDay'] = 10
|
||||
conf1["new"]["perDay"] = 10
|
||||
d.reset()
|
||||
assert d.sched.newCount == 10
|
||||
# if we limit child to 4, we should get 9
|
||||
conf2 = d.decks.confForDid(g2)
|
||||
conf2['new']['perDay'] = 4
|
||||
conf2["new"]["perDay"] = 4
|
||||
d.reset()
|
||||
assert d.sched.newCount == 9
|
||||
|
||||
|
||||
def test_newBoxes():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
c = d.sched.getCard()
|
||||
d.sched._cardConf(c)['new']['delays'] = [1,2,3,4,5]
|
||||
d.sched._cardConf(c)["new"]["delays"] = [1, 2, 3, 4, 5]
|
||||
d.sched.answerCard(c, 2)
|
||||
# should handle gracefully
|
||||
d.sched._cardConf(c)['new']['delays'] = [1]
|
||||
d.sched._cardConf(c)["new"]["delays"] = [1]
|
||||
d.sched.answerCard(c, 2)
|
||||
|
||||
|
||||
def test_learn():
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
f = d.addNote(f)
|
||||
# set as a learn card and rebuild queues
|
||||
d.db.execute("update cards set queue=0, type=0")
|
||||
|
@ -126,12 +136,12 @@ def test_learn():
|
|||
# sched.getCard should return it, since it's due in the past
|
||||
c = d.sched.getCard()
|
||||
assert c
|
||||
d.sched._cardConf(c)['new']['delays'] = [0.5, 3, 10]
|
||||
d.sched._cardConf(c)["new"]["delays"] = [0.5, 3, 10]
|
||||
# fail it
|
||||
d.sched.answerCard(c, 1)
|
||||
# it should have three reps left to graduation
|
||||
assert c.left%1000 == 3
|
||||
assert c.left//1000 == 3
|
||||
assert c.left % 1000 == 3
|
||||
assert c.left // 1000 == 3
|
||||
# it should by due in 30 seconds
|
||||
t = round(c.due - time.time())
|
||||
assert t >= 25 and t <= 40
|
||||
|
@ -139,8 +149,8 @@ def test_learn():
|
|||
d.sched.answerCard(c, 2)
|
||||
# it should by due in 3 minutes
|
||||
assert round(c.due - time.time()) in (179, 180)
|
||||
assert c.left%1000 == 2
|
||||
assert c.left//1000 == 2
|
||||
assert c.left % 1000 == 2
|
||||
assert c.left // 1000 == 2
|
||||
# check log is accurate
|
||||
log = d.db.first("select * from revlog order by id desc")
|
||||
assert log[3] == 2
|
||||
|
@ -150,8 +160,8 @@ def test_learn():
|
|||
d.sched.answerCard(c, 2)
|
||||
# it should by due in 10 minutes
|
||||
assert round(c.due - time.time()) in (599, 600)
|
||||
assert c.left%1000 == 1
|
||||
assert c.left//1000 == 1
|
||||
assert c.left % 1000 == 1
|
||||
assert c.left // 1000 == 1
|
||||
# the next pass should graduate the card
|
||||
assert c.queue == 1
|
||||
assert c.type == 1
|
||||
|
@ -159,7 +169,7 @@ def test_learn():
|
|||
assert c.queue == 2
|
||||
assert c.type == 2
|
||||
# should be due tomorrow, with an interval of 1
|
||||
assert c.due == d.sched.today+1
|
||||
assert c.due == d.sched.today + 1
|
||||
assert c.ivl == 1
|
||||
# or normal removal
|
||||
c.type = 0
|
||||
|
@ -188,14 +198,15 @@ def test_learn():
|
|||
assert c.queue == 2
|
||||
assert c.due == 321
|
||||
|
||||
|
||||
def test_learn_collapsed():
|
||||
d = getEmptyCol()
|
||||
# add 2 notes
|
||||
f = d.newNote()
|
||||
f['Front'] = "1"
|
||||
f["Front"] = "1"
|
||||
f = d.addNote(f)
|
||||
f = d.newNote()
|
||||
f['Front'] = "2"
|
||||
f["Front"] = "2"
|
||||
f = d.addNote(f)
|
||||
# set as a learn card and rebuild queues
|
||||
d.db.execute("update cards set queue=0, type=0")
|
||||
|
@ -214,27 +225,28 @@ def test_learn_collapsed():
|
|||
c = d.sched.getCard()
|
||||
assert not c.q().endswith("2")
|
||||
|
||||
|
||||
def test_learn_day():
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
f = d.addNote(f)
|
||||
d.sched.reset()
|
||||
c = d.sched.getCard()
|
||||
d.sched._cardConf(c)['new']['delays'] = [1, 10, 1440, 2880]
|
||||
d.sched._cardConf(c)["new"]["delays"] = [1, 10, 1440, 2880]
|
||||
# pass it
|
||||
d.sched.answerCard(c, 2)
|
||||
# two reps to graduate, 1 more today
|
||||
assert c.left%1000 == 3
|
||||
assert c.left//1000 == 1
|
||||
assert c.left % 1000 == 3
|
||||
assert c.left // 1000 == 1
|
||||
assert d.sched.counts() == (0, 1, 0)
|
||||
c = d.sched.getCard()
|
||||
ni = d.sched.nextIvl
|
||||
assert ni(c, 2) == 86400
|
||||
# answering it will place it in queue 3
|
||||
d.sched.answerCard(c, 2)
|
||||
assert c.due == d.sched.today+1
|
||||
assert c.due == d.sched.today + 1
|
||||
assert c.queue == 3
|
||||
assert not d.sched.getCard()
|
||||
# for testing, move it back a day
|
||||
|
@ -244,7 +256,7 @@ def test_learn_day():
|
|||
assert d.sched.counts() == (0, 1, 0)
|
||||
c = d.sched.getCard()
|
||||
# nextIvl should work
|
||||
assert ni(c, 2) == 86400*2
|
||||
assert ni(c, 2) == 86400 * 2
|
||||
# if we fail it, it should be back in the correct queue
|
||||
d.sched.answerCard(c, 1)
|
||||
assert c.queue == 1
|
||||
|
@ -266,17 +278,19 @@ def test_learn_day():
|
|||
c.flush()
|
||||
d.reset()
|
||||
assert d.sched.counts() == (0, 0, 1)
|
||||
d.sched._cardConf(c)['lapse']['delays'] = [1440]
|
||||
d.sched._cardConf(c)["lapse"]["delays"] = [1440]
|
||||
c = d.sched.getCard()
|
||||
d.sched.answerCard(c, 1)
|
||||
assert c.queue == 3
|
||||
assert d.sched.counts() == (0, 0, 0)
|
||||
|
||||
|
||||
def test_reviews():
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
d.addNote(f)
|
||||
# set the card up as a review card, due 8 days ago
|
||||
c = f.cards()[0]
|
||||
|
@ -295,7 +309,7 @@ def test_reviews():
|
|||
##################################################
|
||||
# different delay to new
|
||||
d.reset()
|
||||
d.sched._cardConf(c)['lapse']['delays'] = [2, 20]
|
||||
d.sched._cardConf(c)["lapse"]["delays"] = [2, 20]
|
||||
d.sched.answerCard(c, 1)
|
||||
assert c.queue == 1
|
||||
# it should be due tomorrow, with an interval of 1
|
||||
|
@ -313,7 +327,7 @@ def test_reviews():
|
|||
# check ests.
|
||||
ni = d.sched.nextIvl
|
||||
assert ni(c, 1) == 120
|
||||
assert ni(c, 2) == 20*60
|
||||
assert ni(c, 2) == 20 * 60
|
||||
# try again with an ease of 2 instead
|
||||
##################################################
|
||||
c = copy.copy(cardcopy)
|
||||
|
@ -355,8 +369,10 @@ def test_reviews():
|
|||
c.flush()
|
||||
# steup hook
|
||||
hooked = []
|
||||
|
||||
def onLeech(card):
|
||||
hooked.append(1)
|
||||
|
||||
addHook("leech", onLeech)
|
||||
d.sched.answerCard(c, 1)
|
||||
assert hooked
|
||||
|
@ -364,10 +380,11 @@ def test_reviews():
|
|||
c.load()
|
||||
assert c.queue == -1
|
||||
|
||||
|
||||
def test_button_spacing():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
# 1 day ivl review card due now
|
||||
c = f.cards()[0]
|
||||
|
@ -384,13 +401,14 @@ def test_button_spacing():
|
|||
assert ni(c, 3) == "3 days"
|
||||
assert ni(c, 4) == "4 days"
|
||||
|
||||
|
||||
def test_overdue_lapse():
|
||||
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2
|
||||
return
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
# simulate a review that was lapsed and is now due for its normal review
|
||||
c = f.cards()[0]
|
||||
|
@ -419,13 +437,15 @@ def test_overdue_lapse():
|
|||
d.sched.reset()
|
||||
assert d.sched.counts() == (0, 0, 1)
|
||||
|
||||
|
||||
def test_finished():
|
||||
d = getEmptyCol()
|
||||
# nothing due
|
||||
assert "Congratulations" in d.sched.finishedMsg()
|
||||
assert "limit" not in d.sched.finishedMsg()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
d.addNote(f)
|
||||
# have a new card
|
||||
assert "new cards available" in d.sched.finishedMsg()
|
||||
|
@ -438,44 +458,46 @@ def test_finished():
|
|||
assert "Congratulations" in d.sched.finishedMsg()
|
||||
assert "limit" not in d.sched.finishedMsg()
|
||||
|
||||
|
||||
def test_nextIvl():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
conf = d.decks.confForDid(1)
|
||||
conf['new']['delays'] = [0.5, 3, 10]
|
||||
conf['lapse']['delays'] = [1, 5, 9]
|
||||
conf["new"]["delays"] = [0.5, 3, 10]
|
||||
conf["lapse"]["delays"] = [1, 5, 9]
|
||||
c = d.sched.getCard()
|
||||
# new cards
|
||||
##################################################
|
||||
ni = d.sched.nextIvl
|
||||
assert ni(c, 1) == 30
|
||||
assert ni(c, 2) == 180
|
||||
assert ni(c, 3) == 4*86400
|
||||
assert ni(c, 3) == 4 * 86400
|
||||
d.sched.answerCard(c, 1)
|
||||
# cards in learning
|
||||
##################################################
|
||||
assert ni(c, 1) == 30
|
||||
assert ni(c, 2) == 180
|
||||
assert ni(c, 3) == 4*86400
|
||||
assert ni(c, 3) == 4 * 86400
|
||||
d.sched.answerCard(c, 2)
|
||||
assert ni(c, 1) == 30
|
||||
assert ni(c, 2) == 600
|
||||
assert ni(c, 3) == 4*86400
|
||||
assert ni(c, 3) == 4 * 86400
|
||||
d.sched.answerCard(c, 2)
|
||||
# normal graduation is tomorrow
|
||||
assert ni(c, 2) == 1*86400
|
||||
assert ni(c, 3) == 4*86400
|
||||
assert ni(c, 2) == 1 * 86400
|
||||
assert ni(c, 3) == 4 * 86400
|
||||
# lapsed cards
|
||||
##################################################
|
||||
c.type = 2
|
||||
c.ivl = 100
|
||||
c.factor = STARTING_FACTOR
|
||||
assert ni(c, 1) == 60
|
||||
assert ni(c, 2) == 100*86400
|
||||
assert ni(c, 3) == 100*86400
|
||||
assert ni(c, 2) == 100 * 86400
|
||||
assert ni(c, 3) == 100 * 86400
|
||||
# review cards
|
||||
##################################################
|
||||
c.queue = 2
|
||||
|
@ -484,8 +506,8 @@ def test_nextIvl():
|
|||
# failing it should put it at 60s
|
||||
assert ni(c, 1) == 60
|
||||
# or 1 day if relearn is false
|
||||
d.sched._cardConf(c)['lapse']['delays']=[]
|
||||
assert ni(c, 1) == 1*86400
|
||||
d.sched._cardConf(c)["lapse"]["delays"] = []
|
||||
assert ni(c, 1) == 1 * 86400
|
||||
# (* 100 1.2 86400)10368000.0
|
||||
assert ni(c, 2) == 10368000
|
||||
# (* 100 2.5 86400)21600000.0
|
||||
|
@ -494,10 +516,11 @@ def test_nextIvl():
|
|||
assert ni(c, 4) == 28080000
|
||||
assert d.sched.nextIvlStr(c, 4) == "10.8 months"
|
||||
|
||||
|
||||
def test_misc():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
# burying
|
||||
|
@ -508,10 +531,11 @@ def test_misc():
|
|||
d.reset()
|
||||
assert d.sched.getCard()
|
||||
|
||||
|
||||
def test_suspend():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
# suspending
|
||||
|
@ -525,7 +549,11 @@ def test_suspend():
|
|||
d.reset()
|
||||
assert d.sched.getCard()
|
||||
# should cope with rev cards being relearnt
|
||||
c.due = 0; c.ivl = 100; c.type = 2; c.queue = 2; c.flush()
|
||||
c.due = 0
|
||||
c.ivl = 100
|
||||
c.type = 2
|
||||
c.queue = 2
|
||||
c.flush()
|
||||
d.reset()
|
||||
c = d.sched.getCard()
|
||||
d.sched.answerCard(c, 1)
|
||||
|
@ -551,10 +579,11 @@ def test_suspend():
|
|||
assert c.due == 1
|
||||
assert c.did == 1
|
||||
|
||||
|
||||
def test_cram():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.ivl = 100
|
||||
|
@ -566,7 +595,7 @@ def test_cram():
|
|||
c.startTimer()
|
||||
c.flush()
|
||||
d.reset()
|
||||
assert d.sched.counts() == (0,0,0)
|
||||
assert d.sched.counts() == (0, 0, 0)
|
||||
cardcopy = copy.copy(c)
|
||||
# create a dynamic deck and refresh it
|
||||
did = d.decks.newDyn("Cram")
|
||||
|
@ -575,18 +604,18 @@ def test_cram():
|
|||
# should appear as new in the deck list
|
||||
assert sorted(d.sched.deckDueList())[0][4] == 1
|
||||
# and should appear in the counts
|
||||
assert d.sched.counts() == (1,0,0)
|
||||
assert d.sched.counts() == (1, 0, 0)
|
||||
# grab it and check estimates
|
||||
c = d.sched.getCard()
|
||||
assert d.sched.answerButtons(c) == 2
|
||||
assert d.sched.nextIvl(c, 1) == 600
|
||||
assert d.sched.nextIvl(c, 2) == 138*60*60*24
|
||||
assert d.sched.nextIvl(c, 2) == 138 * 60 * 60 * 24
|
||||
cram = d.decks.get(did)
|
||||
cram['delays'] = [1, 10]
|
||||
cram["delays"] = [1, 10]
|
||||
assert d.sched.answerButtons(c) == 3
|
||||
assert d.sched.nextIvl(c, 1) == 60
|
||||
assert d.sched.nextIvl(c, 2) == 600
|
||||
assert d.sched.nextIvl(c, 3) == 138*60*60*24
|
||||
assert d.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24
|
||||
d.sched.answerCard(c, 2)
|
||||
# elapsed time was 75 days
|
||||
# factor = 2.5+1.2/2 = 1.85
|
||||
|
@ -595,12 +624,11 @@ def test_cram():
|
|||
assert c.odue == 138
|
||||
assert c.queue == 1
|
||||
# should be logged as a cram rep
|
||||
assert d.db.scalar(
|
||||
"select type from revlog order by id desc limit 1") == 3
|
||||
assert d.db.scalar("select type from revlog order by id desc limit 1") == 3
|
||||
# check ivls again
|
||||
assert d.sched.nextIvl(c, 1) == 60
|
||||
assert d.sched.nextIvl(c, 2) == 138*60*60*24
|
||||
assert d.sched.nextIvl(c, 3) == 138*60*60*24
|
||||
assert d.sched.nextIvl(c, 2) == 138 * 60 * 60 * 24
|
||||
assert d.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24
|
||||
# when it graduates, due is updated
|
||||
c = d.sched.getCard()
|
||||
d.sched.answerCard(c, 2)
|
||||
|
@ -616,7 +644,7 @@ def test_cram():
|
|||
# check ivls again - passing should be idempotent
|
||||
assert d.sched.nextIvl(c, 1) == 60
|
||||
assert d.sched.nextIvl(c, 2) == 600
|
||||
assert d.sched.nextIvl(c, 3) == 138*60*60*24
|
||||
assert d.sched.nextIvl(c, 3) == 138 * 60 * 60 * 24
|
||||
d.sched.answerCard(c, 2)
|
||||
assert c.ivl == 138
|
||||
assert c.odue == 138
|
||||
|
@ -630,20 +658,20 @@ def test_cram():
|
|||
assert len(d.sched.deckDueList()) == 1
|
||||
c.load()
|
||||
assert c.ivl == 1
|
||||
assert c.due == d.sched.today+1
|
||||
assert c.due == d.sched.today + 1
|
||||
# make it due
|
||||
d.reset()
|
||||
assert d.sched.counts() == (0,0,0)
|
||||
assert d.sched.counts() == (0, 0, 0)
|
||||
c.due = -5
|
||||
c.ivl = 100
|
||||
c.flush()
|
||||
d.reset()
|
||||
assert d.sched.counts() == (0,0,1)
|
||||
assert d.sched.counts() == (0, 0, 1)
|
||||
# cram again
|
||||
did = d.decks.newDyn("Cram")
|
||||
d.sched.rebuildDyn(did)
|
||||
d.reset()
|
||||
assert d.sched.counts() == (0,0,1)
|
||||
assert d.sched.counts() == (0, 0, 1)
|
||||
c.load()
|
||||
assert d.sched.answerButtons(c) == 4
|
||||
# add a sibling so we can test minSpace, etc
|
||||
|
@ -661,10 +689,11 @@ def test_cram():
|
|||
# it should have been moved back to the original deck
|
||||
assert c.did == 1
|
||||
|
||||
|
||||
def test_cram_rem():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
oldDue = f.cards()[0].due
|
||||
did = d.decks.newDyn("Cram")
|
||||
|
@ -681,16 +710,17 @@ def test_cram_rem():
|
|||
assert c.type == c.queue == 0
|
||||
assert c.due == oldDue
|
||||
|
||||
|
||||
def test_cram_resched():
|
||||
# add card
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
# cram deck
|
||||
did = d.decks.newDyn("Cram")
|
||||
cram = d.decks.get(did)
|
||||
cram['resched'] = False
|
||||
cram["resched"] = False
|
||||
d.sched.rebuildDyn(did)
|
||||
d.reset()
|
||||
# graduate should return it to new
|
||||
|
@ -786,22 +816,25 @@ def test_cram_resched():
|
|||
# d.sched.answerCard(c, 2)
|
||||
# print c.__dict__
|
||||
|
||||
|
||||
def test_ordcycle():
|
||||
d = getEmptyCol()
|
||||
# add two more templates and set second active
|
||||
m = d.models.current(); mm = d.models
|
||||
m = d.models.current()
|
||||
mm = d.models
|
||||
t = mm.newTemplate("Reverse")
|
||||
t['qfmt'] = "{{Back}}"
|
||||
t['afmt'] = "{{Front}}"
|
||||
t["qfmt"] = "{{Back}}"
|
||||
t["afmt"] = "{{Front}}"
|
||||
mm.addTemplate(m, t)
|
||||
t = mm.newTemplate("f2")
|
||||
t['qfmt'] = "{{Front}}"
|
||||
t['afmt'] = "{{Back}}"
|
||||
t["qfmt"] = "{{Front}}"
|
||||
t["afmt"] = "{{Back}}"
|
||||
mm.addTemplate(m, t)
|
||||
mm.save(m)
|
||||
# create a new note; it should have 3 cards
|
||||
f = d.newNote()
|
||||
f['Front'] = "1"; f['Back'] = "1"
|
||||
f["Front"] = "1"
|
||||
f["Back"] = "1"
|
||||
d.addNote(f)
|
||||
assert d.cardCount() == 3
|
||||
d.reset()
|
||||
|
@ -810,10 +843,12 @@ def test_ordcycle():
|
|||
assert d.sched.getCard().ord == 1
|
||||
assert d.sched.getCard().ord == 2
|
||||
|
||||
|
||||
def test_counts_idx():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
assert d.sched.counts() == (1, 0, 0)
|
||||
|
@ -832,10 +867,11 @@ def test_counts_idx():
|
|||
d.sched.answerCard(c, 1)
|
||||
assert d.sched.counts() == (0, 2, 0)
|
||||
|
||||
|
||||
def test_repCounts():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
# lrnReps should be accurate on pass/fail
|
||||
|
@ -853,7 +889,7 @@ def test_repCounts():
|
|||
d.sched.answerCard(d.sched.getCard(), 2)
|
||||
assert d.sched.counts() == (0, 0, 0)
|
||||
f = d.newNote()
|
||||
f['Front'] = "two"
|
||||
f["Front"] = "two"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
# initial pass should be correct too
|
||||
|
@ -865,14 +901,14 @@ def test_repCounts():
|
|||
assert d.sched.counts() == (0, 0, 0)
|
||||
# immediate graduate should work
|
||||
f = d.newNote()
|
||||
f['Front'] = "three"
|
||||
f["Front"] = "three"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
d.sched.answerCard(d.sched.getCard(), 3)
|
||||
assert d.sched.counts() == (0, 0, 0)
|
||||
# and failing a review should too
|
||||
f = d.newNote()
|
||||
f['Front'] = "three"
|
||||
f["Front"] = "three"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.type = 2
|
||||
|
@ -884,12 +920,13 @@ def test_repCounts():
|
|||
d.sched.answerCard(d.sched.getCard(), 1)
|
||||
assert d.sched.counts() == (0, 1, 0)
|
||||
|
||||
|
||||
def test_timing():
|
||||
d = getEmptyCol()
|
||||
# add a few review cards, due today
|
||||
for i in range(5):
|
||||
f = d.newNote()
|
||||
f['Front'] = "num"+str(i)
|
||||
f["Front"] = "num" + str(i)
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.type = 2
|
||||
|
@ -900,7 +937,7 @@ def test_timing():
|
|||
d.reset()
|
||||
c = d.sched.getCard()
|
||||
# set a a fail delay of 1 second so we don't have to wait
|
||||
d.sched._cardConf(c)['lapse']['delays'][0] = 1/60.0
|
||||
d.sched._cardConf(c)["lapse"]["delays"][0] = 1 / 60.0
|
||||
d.sched.answerCard(c, 1)
|
||||
# the next card should be another review
|
||||
c = d.sched.getCard()
|
||||
|
@ -910,11 +947,12 @@ def test_timing():
|
|||
c = d.sched.getCard()
|
||||
assert c.queue == 1
|
||||
|
||||
|
||||
def test_collapse():
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
# test collapsing
|
||||
|
@ -924,16 +962,17 @@ def test_collapse():
|
|||
d.sched.answerCard(c, 3)
|
||||
assert not d.sched.getCard()
|
||||
|
||||
|
||||
def test_deckDue():
|
||||
d = getEmptyCol()
|
||||
# add a note with default deck
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
# and one that's a child
|
||||
f = d.newNote()
|
||||
f['Front'] = "two"
|
||||
default1 = f.model()['did'] = d.decks.id("Default::1")
|
||||
f["Front"] = "two"
|
||||
default1 = f.model()["did"] = d.decks.id("Default::1")
|
||||
d.addNote(f)
|
||||
# make it a review card
|
||||
c = f.cards()[0]
|
||||
|
@ -942,13 +981,13 @@ def test_deckDue():
|
|||
c.flush()
|
||||
# add one more with a new deck
|
||||
f = d.newNote()
|
||||
f['Front'] = "two"
|
||||
foobar = f.model()['did'] = d.decks.id("foo::bar")
|
||||
f["Front"] = "two"
|
||||
foobar = f.model()["did"] = d.decks.id("foo::bar")
|
||||
d.addNote(f)
|
||||
# and one that's a sibling
|
||||
f = d.newNote()
|
||||
f['Front'] = "three"
|
||||
foobaz = f.model()['did'] = d.decks.id("foo::baz")
|
||||
f["Front"] = "three"
|
||||
foobaz = f.model()["did"] = d.decks.id("foo::baz")
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
assert len(d.decks.decks) == 5
|
||||
|
@ -970,10 +1009,12 @@ def test_deckDue():
|
|||
assert tree[0][5][0][2] == 1
|
||||
assert tree[0][5][0][4] == 0
|
||||
# code should not fail if a card has an invalid deck
|
||||
c.did = 12345; c.flush()
|
||||
c.did = 12345
|
||||
c.flush()
|
||||
d.sched.deckDueList()
|
||||
d.sched.deckDueTree()
|
||||
|
||||
|
||||
def test_deckTree():
|
||||
d = getEmptyCol()
|
||||
d.decks.id("new::b::c")
|
||||
|
@ -983,75 +1024,80 @@ def test_deckTree():
|
|||
names.remove("new")
|
||||
assert "new" not in names
|
||||
|
||||
|
||||
def test_deckFlow():
|
||||
d = getEmptyCol()
|
||||
# add a note with default deck
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
# and one that's a child
|
||||
f = d.newNote()
|
||||
f['Front'] = "two"
|
||||
default1 = f.model()['did'] = d.decks.id("Default::2")
|
||||
f["Front"] = "two"
|
||||
default1 = f.model()["did"] = d.decks.id("Default::2")
|
||||
d.addNote(f)
|
||||
# and another that's higher up
|
||||
f = d.newNote()
|
||||
f['Front'] = "three"
|
||||
default1 = f.model()['did'] = d.decks.id("Default::1")
|
||||
f["Front"] = "three"
|
||||
default1 = f.model()["did"] = d.decks.id("Default::1")
|
||||
d.addNote(f)
|
||||
# should get top level one first, then ::1, then ::2
|
||||
d.reset()
|
||||
assert d.sched.counts() == (3,0,0)
|
||||
assert d.sched.counts() == (3, 0, 0)
|
||||
for i in "one", "three", "two":
|
||||
c = d.sched.getCard()
|
||||
assert c.note()['Front'] == i
|
||||
assert c.note()["Front"] == i
|
||||
d.sched.answerCard(c, 2)
|
||||
|
||||
|
||||
def test_reorder():
|
||||
d = getEmptyCol()
|
||||
# add a note with default deck
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
f2 = d.newNote()
|
||||
f2['Front'] = "two"
|
||||
f2["Front"] = "two"
|
||||
d.addNote(f2)
|
||||
assert f2.cards()[0].due == 2
|
||||
found=False
|
||||
found = False
|
||||
# 50/50 chance of being reordered
|
||||
for i in range(20):
|
||||
d.sched.randomizeCards(1)
|
||||
if f.cards()[0].due != f.id:
|
||||
found=True
|
||||
found = True
|
||||
break
|
||||
assert found
|
||||
d.sched.orderCards(1)
|
||||
assert f.cards()[0].due == 1
|
||||
# shifting
|
||||
f3 = d.newNote()
|
||||
f3['Front'] = "three"
|
||||
f3["Front"] = "three"
|
||||
d.addNote(f3)
|
||||
f4 = d.newNote()
|
||||
f4['Front'] = "four"
|
||||
f4["Front"] = "four"
|
||||
d.addNote(f4)
|
||||
assert f.cards()[0].due == 1
|
||||
assert f2.cards()[0].due == 2
|
||||
assert f3.cards()[0].due == 3
|
||||
assert f4.cards()[0].due == 4
|
||||
d.sched.sortCards([
|
||||
f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
|
||||
d.sched.sortCards([f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
|
||||
assert f.cards()[0].due == 3
|
||||
assert f2.cards()[0].due == 4
|
||||
assert f3.cards()[0].due == 1
|
||||
assert f4.cards()[0].due == 2
|
||||
|
||||
|
||||
def test_forget():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.queue = 2; c.type = 2; c.ivl = 100; c.due = 0
|
||||
c.queue = 2
|
||||
c.type = 2
|
||||
c.ivl = 100
|
||||
c.due = 0
|
||||
c.flush()
|
||||
d.reset()
|
||||
assert d.sched.counts() == (0, 0, 1)
|
||||
|
@ -1059,10 +1105,11 @@ def test_forget():
|
|||
d.reset()
|
||||
assert d.sched.counts() == (1, 0, 0)
|
||||
|
||||
|
||||
def test_resched():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
d.sched.reschedCards([c.id], 0, 0)
|
||||
|
@ -1072,14 +1119,15 @@ def test_resched():
|
|||
assert c.queue == c.type == 2
|
||||
d.sched.reschedCards([c.id], 1, 1)
|
||||
c.load()
|
||||
assert c.due == d.sched.today+1
|
||||
assert c.due == d.sched.today + 1
|
||||
assert c.ivl == +1
|
||||
|
||||
|
||||
def test_norelearn():
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.type = 2
|
||||
|
@ -1093,13 +1141,15 @@ def test_norelearn():
|
|||
c.flush()
|
||||
d.reset()
|
||||
d.sched.answerCard(c, 1)
|
||||
d.sched._cardConf(c)['lapse']['delays'] = []
|
||||
d.sched._cardConf(c)["lapse"]["delays"] = []
|
||||
d.sched.answerCard(c, 1)
|
||||
|
||||
|
||||
def test_failmult():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.type = 2
|
||||
|
@ -1111,7 +1161,7 @@ def test_failmult():
|
|||
c.lapses = 1
|
||||
c.startTimer()
|
||||
c.flush()
|
||||
d.sched._cardConf(c)['lapse']['mult'] = 0.5
|
||||
d.sched._cardConf(c)["lapse"]["mult"] = 0.5
|
||||
c = d.sched.getCard()
|
||||
d.sched.answerCard(c, 1)
|
||||
assert c.ivl == 50
|
||||
|
|
|
@ -1,34 +1,49 @@
|
|||
# coding: utf-8
|
||||
|
||||
import time
|
||||
import copy
|
||||
import time
|
||||
|
||||
from anki.consts import STARTING_FACTOR
|
||||
from tests.shared import getEmptyCol
|
||||
from anki.utils import intTime
|
||||
from anki.hooks import addHook
|
||||
from anki.utils import intTime
|
||||
from tests.shared import getEmptyCol
|
||||
|
||||
# Between 2-4AM, shift the time back so test assumptions hold.
|
||||
lt = time.localtime()
|
||||
if lt.tm_hour > 2 and lt.tm_hour < 4:
|
||||
orig_time = time.time
|
||||
|
||||
def adjusted_time():
|
||||
return orig_time() - 60 * 60 * 2
|
||||
|
||||
time.time = adjusted_time
|
||||
|
||||
|
||||
def test_clock():
|
||||
d = getEmptyCol()
|
||||
if (d.sched.dayCutoff - intTime()) < 10*60:
|
||||
if (d.sched.dayCutoff - intTime()) < 10 * 60:
|
||||
raise Exception("Unit tests will fail around the day rollover.")
|
||||
|
||||
|
||||
def checkRevIvl(d, c, targetIvl):
|
||||
min, max = d.sched._fuzzIvlRange(targetIvl)
|
||||
return min <= c.ivl <= max
|
||||
|
||||
|
||||
def test_basics():
|
||||
d = getEmptyCol()
|
||||
d.reset()
|
||||
assert not d.sched.getCard()
|
||||
|
||||
|
||||
def test_new():
|
||||
d = getEmptyCol()
|
||||
d.reset()
|
||||
assert d.sched.newCount == 0
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
assert d.sched.newCount == 1
|
||||
|
@ -66,15 +81,16 @@ def test_new():
|
|||
# assert qs[n] in c.q()
|
||||
# d.sched.answerCard(c, 2)
|
||||
|
||||
|
||||
def test_newLimits():
|
||||
d = getEmptyCol()
|
||||
# add some notes
|
||||
g2 = d.decks.id("Default::foo")
|
||||
for i in range(30):
|
||||
f = d.newNote()
|
||||
f['Front'] = str(i)
|
||||
f["Front"] = str(i)
|
||||
if i > 4:
|
||||
f.model()['did'] = g2
|
||||
f.model()["did"] = g2
|
||||
d.addNote(f)
|
||||
# give the child deck a different configuration
|
||||
c2 = d.decks.confId("new conf")
|
||||
|
@ -87,33 +103,36 @@ def test_newLimits():
|
|||
assert c.did == 1
|
||||
# limit the parent to 10 cards, meaning we get 10 in total
|
||||
conf1 = d.decks.confForDid(1)
|
||||
conf1['new']['perDay'] = 10
|
||||
conf1["new"]["perDay"] = 10
|
||||
d.reset()
|
||||
assert d.sched.newCount == 10
|
||||
# if we limit child to 4, we should get 9
|
||||
conf2 = d.decks.confForDid(g2)
|
||||
conf2['new']['perDay'] = 4
|
||||
conf2["new"]["perDay"] = 4
|
||||
d.reset()
|
||||
assert d.sched.newCount == 9
|
||||
|
||||
|
||||
def test_newBoxes():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
c = d.sched.getCard()
|
||||
d.sched._cardConf(c)['new']['delays'] = [1,2,3,4,5]
|
||||
d.sched._cardConf(c)["new"]["delays"] = [1, 2, 3, 4, 5]
|
||||
d.sched.answerCard(c, 2)
|
||||
# should handle gracefully
|
||||
d.sched._cardConf(c)['new']['delays'] = [1]
|
||||
d.sched._cardConf(c)["new"]["delays"] = [1]
|
||||
d.sched.answerCard(c, 2)
|
||||
|
||||
|
||||
def test_learn():
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
f = d.addNote(f)
|
||||
# set as a learn card and rebuild queues
|
||||
d.db.execute("update cards set queue=0, type=0")
|
||||
|
@ -121,12 +140,12 @@ def test_learn():
|
|||
# sched.getCard should return it, since it's due in the past
|
||||
c = d.sched.getCard()
|
||||
assert c
|
||||
d.sched._cardConf(c)['new']['delays'] = [0.5, 3, 10]
|
||||
d.sched._cardConf(c)["new"]["delays"] = [0.5, 3, 10]
|
||||
# fail it
|
||||
d.sched.answerCard(c, 1)
|
||||
# it should have three reps left to graduation
|
||||
assert c.left%1000 == 3
|
||||
assert c.left//1000 == 3
|
||||
assert c.left % 1000 == 3
|
||||
assert c.left // 1000 == 3
|
||||
# it should by due in 30 seconds
|
||||
t = round(c.due - time.time())
|
||||
assert t >= 25 and t <= 40
|
||||
|
@ -134,9 +153,9 @@ def test_learn():
|
|||
d.sched.answerCard(c, 3)
|
||||
# it should by due in 3 minutes
|
||||
dueIn = c.due - time.time()
|
||||
assert 179 <= dueIn <= 180*1.25
|
||||
assert c.left%1000 == 2
|
||||
assert c.left//1000 == 2
|
||||
assert 179 <= dueIn <= 180 * 1.25
|
||||
assert c.left % 1000 == 2
|
||||
assert c.left // 1000 == 2
|
||||
# check log is accurate
|
||||
log = d.db.first("select * from revlog order by id desc")
|
||||
assert log[3] == 3
|
||||
|
@ -146,9 +165,9 @@ def test_learn():
|
|||
d.sched.answerCard(c, 3)
|
||||
# it should by due in 10 minutes
|
||||
dueIn = c.due - time.time()
|
||||
assert 599 <= dueIn <= 600*1.25
|
||||
assert c.left%1000 == 1
|
||||
assert c.left//1000 == 1
|
||||
assert 599 <= dueIn <= 600 * 1.25
|
||||
assert c.left % 1000 == 1
|
||||
assert c.left // 1000 == 1
|
||||
# the next pass should graduate the card
|
||||
assert c.queue == 1
|
||||
assert c.type == 1
|
||||
|
@ -156,7 +175,7 @@ def test_learn():
|
|||
assert c.queue == 2
|
||||
assert c.type == 2
|
||||
# should be due tomorrow, with an interval of 1
|
||||
assert c.due == d.sched.today+1
|
||||
assert c.due == d.sched.today + 1
|
||||
assert c.ivl == 1
|
||||
# or normal removal
|
||||
c.type = 0
|
||||
|
@ -168,10 +187,11 @@ def test_learn():
|
|||
# revlog should have been updated each time
|
||||
assert d.db.scalar("select count() from revlog where type = 0") == 5
|
||||
|
||||
|
||||
def test_relearn():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.ivl = 100
|
||||
|
@ -193,10 +213,11 @@ def test_relearn():
|
|||
assert c.ivl == 2
|
||||
assert c.due == d.sched.today + c.ivl
|
||||
|
||||
|
||||
def test_relearn_no_steps():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.ivl = 100
|
||||
|
@ -205,7 +226,7 @@ def test_relearn_no_steps():
|
|||
c.flush()
|
||||
|
||||
conf = d.decks.confForDid(1)
|
||||
conf['lapse']['delays'] = []
|
||||
conf["lapse"]["delays"] = []
|
||||
d.decks.save(conf)
|
||||
|
||||
# fail the card
|
||||
|
@ -214,14 +235,15 @@ def test_relearn_no_steps():
|
|||
d.sched.answerCard(c, 1)
|
||||
assert c.type == c.queue == 2
|
||||
|
||||
|
||||
def test_learn_collapsed():
|
||||
d = getEmptyCol()
|
||||
# add 2 notes
|
||||
f = d.newNote()
|
||||
f['Front'] = "1"
|
||||
f["Front"] = "1"
|
||||
f = d.addNote(f)
|
||||
f = d.newNote()
|
||||
f['Front'] = "2"
|
||||
f["Front"] = "2"
|
||||
f = d.addNote(f)
|
||||
# set as a learn card and rebuild queues
|
||||
d.db.execute("update cards set queue=0, type=0")
|
||||
|
@ -240,27 +262,28 @@ def test_learn_collapsed():
|
|||
c = d.sched.getCard()
|
||||
assert not c.q().endswith("2")
|
||||
|
||||
|
||||
def test_learn_day():
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
f = d.addNote(f)
|
||||
d.sched.reset()
|
||||
c = d.sched.getCard()
|
||||
d.sched._cardConf(c)['new']['delays'] = [1, 10, 1440, 2880]
|
||||
d.sched._cardConf(c)["new"]["delays"] = [1, 10, 1440, 2880]
|
||||
# pass it
|
||||
d.sched.answerCard(c, 3)
|
||||
# two reps to graduate, 1 more today
|
||||
assert c.left%1000 == 3
|
||||
assert c.left//1000 == 1
|
||||
assert c.left % 1000 == 3
|
||||
assert c.left // 1000 == 1
|
||||
assert d.sched.counts() == (0, 1, 0)
|
||||
c = d.sched.getCard()
|
||||
ni = d.sched.nextIvl
|
||||
assert ni(c, 3) == 86400
|
||||
# answering it will place it in queue 3
|
||||
d.sched.answerCard(c, 3)
|
||||
assert c.due == d.sched.today+1
|
||||
assert c.due == d.sched.today + 1
|
||||
assert c.queue == 3
|
||||
assert not d.sched.getCard()
|
||||
# for testing, move it back a day
|
||||
|
@ -270,7 +293,7 @@ def test_learn_day():
|
|||
assert d.sched.counts() == (0, 1, 0)
|
||||
c = d.sched.getCard()
|
||||
# nextIvl should work
|
||||
assert ni(c, 3) == 86400*2
|
||||
assert ni(c, 3) == 86400 * 2
|
||||
# if we fail it, it should be back in the correct queue
|
||||
d.sched.answerCard(c, 1)
|
||||
assert c.queue == 1
|
||||
|
@ -292,17 +315,19 @@ def test_learn_day():
|
|||
c.flush()
|
||||
d.reset()
|
||||
assert d.sched.counts() == (0, 0, 1)
|
||||
d.sched._cardConf(c)['lapse']['delays'] = [1440]
|
||||
d.sched._cardConf(c)["lapse"]["delays"] = [1440]
|
||||
c = d.sched.getCard()
|
||||
d.sched.answerCard(c, 1)
|
||||
assert c.queue == 3
|
||||
assert d.sched.counts() == (0, 0, 0)
|
||||
|
||||
|
||||
def test_reviews():
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
d.addNote(f)
|
||||
# set the card up as a review card, due 8 days ago
|
||||
c = f.cards()[0]
|
||||
|
@ -359,8 +384,10 @@ def test_reviews():
|
|||
c.flush()
|
||||
# steup hook
|
||||
hooked = []
|
||||
|
||||
def onLeech(card):
|
||||
hooked.append(1)
|
||||
|
||||
addHook("leech", onLeech)
|
||||
d.sched.answerCard(c, 1)
|
||||
assert hooked
|
||||
|
@ -368,6 +395,7 @@ def test_reviews():
|
|||
c.load()
|
||||
assert c.queue == -1
|
||||
|
||||
|
||||
def test_review_limits():
|
||||
d = getEmptyCol()
|
||||
|
||||
|
@ -377,21 +405,22 @@ def test_review_limits():
|
|||
pconf = d.decks.getConf(d.decks.confId("parentConf"))
|
||||
cconf = d.decks.getConf(d.decks.confId("childConf"))
|
||||
|
||||
pconf['rev']['perDay'] = 5
|
||||
pconf["rev"]["perDay"] = 5
|
||||
d.decks.updateConf(pconf)
|
||||
d.decks.setConf(parent, pconf['id'])
|
||||
cconf['rev']['perDay'] = 10
|
||||
d.decks.setConf(parent, pconf["id"])
|
||||
cconf["rev"]["perDay"] = 10
|
||||
d.decks.updateConf(cconf)
|
||||
d.decks.setConf(child, cconf['id'])
|
||||
d.decks.setConf(child, cconf["id"])
|
||||
|
||||
m = d.models.current()
|
||||
m['did'] = child['id']
|
||||
m["did"] = child["id"]
|
||||
d.models.save(m, updateReqs=False)
|
||||
|
||||
# add some cards
|
||||
for i in range(20):
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
d.addNote(f)
|
||||
|
||||
# make them reviews
|
||||
|
@ -402,11 +431,11 @@ def test_review_limits():
|
|||
|
||||
tree = d.sched.deckDueTree()
|
||||
# (('Default', 1, 0, 0, 0, ()), ('parent', 1514457677462, 5, 0, 0, (('child', 1514457677463, 5, 0, 0, ()),)))
|
||||
assert tree[1][2] == 5 # parent
|
||||
assert tree[1][5][0][2] == 5 # child
|
||||
assert tree[1][2] == 5 # parent
|
||||
assert tree[1][5][0][2] == 5 # child
|
||||
|
||||
# .counts() should match
|
||||
d.decks.select(child['id'])
|
||||
d.decks.select(child["id"])
|
||||
d.sched.reset()
|
||||
assert d.sched.counts() == (0, 0, 5)
|
||||
|
||||
|
@ -416,24 +445,25 @@ def test_review_limits():
|
|||
assert d.sched.counts() == (0, 0, 4)
|
||||
|
||||
tree = d.sched.deckDueTree()
|
||||
assert tree[1][2] == 4 # parent
|
||||
assert tree[1][5][0][2] == 4 # child
|
||||
assert tree[1][2] == 4 # parent
|
||||
assert tree[1][5][0][2] == 4 # child
|
||||
|
||||
# switch limits
|
||||
d.decks.setConf(parent, cconf['id'])
|
||||
d.decks.setConf(child, pconf['id'])
|
||||
d.decks.select(parent['id'])
|
||||
d.decks.setConf(parent, cconf["id"])
|
||||
d.decks.setConf(child, pconf["id"])
|
||||
d.decks.select(parent["id"])
|
||||
d.sched.reset()
|
||||
|
||||
# child limits do not affect the parent
|
||||
tree = d.sched.deckDueTree()
|
||||
assert tree[1][2] == 9 # parent
|
||||
assert tree[1][5][0][2] == 4 # child
|
||||
assert tree[1][2] == 9 # parent
|
||||
assert tree[1][5][0][2] == 4 # child
|
||||
|
||||
|
||||
def test_button_spacing():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
# 1 day ivl review card due now
|
||||
c = f.cards()[0]
|
||||
|
@ -452,16 +482,17 @@ def test_button_spacing():
|
|||
|
||||
# if hard factor is <= 1, then hard may not increase
|
||||
conf = d.decks.confForDid(1)
|
||||
conf['rev']['hardFactor'] = 1
|
||||
conf["rev"]["hardFactor"] = 1
|
||||
assert ni(c, 2) == "1 day"
|
||||
|
||||
|
||||
def test_overdue_lapse():
|
||||
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2
|
||||
return
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
# simulate a review that was lapsed and is now due for its normal review
|
||||
c = f.cards()[0]
|
||||
|
@ -490,13 +521,15 @@ def test_overdue_lapse():
|
|||
d.sched.reset()
|
||||
assert d.sched.counts() == (0, 0, 1)
|
||||
|
||||
|
||||
def test_finished():
|
||||
d = getEmptyCol()
|
||||
# nothing due
|
||||
assert "Congratulations" in d.sched.finishedMsg()
|
||||
assert "limit" not in d.sched.finishedMsg()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
d.addNote(f)
|
||||
# have a new card
|
||||
assert "new cards available" in d.sched.finishedMsg()
|
||||
|
@ -509,47 +542,49 @@ def test_finished():
|
|||
assert "Congratulations" in d.sched.finishedMsg()
|
||||
assert "limit" not in d.sched.finishedMsg()
|
||||
|
||||
|
||||
def test_nextIvl():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
conf = d.decks.confForDid(1)
|
||||
conf['new']['delays'] = [0.5, 3, 10]
|
||||
conf['lapse']['delays'] = [1, 5, 9]
|
||||
conf["new"]["delays"] = [0.5, 3, 10]
|
||||
conf["lapse"]["delays"] = [1, 5, 9]
|
||||
c = d.sched.getCard()
|
||||
# new cards
|
||||
##################################################
|
||||
ni = d.sched.nextIvl
|
||||
assert ni(c, 1) == 30
|
||||
assert ni(c, 2) == (30+180)//2
|
||||
assert ni(c, 2) == (30 + 180) // 2
|
||||
assert ni(c, 3) == 180
|
||||
assert ni(c, 4) == 4*86400
|
||||
assert ni(c, 4) == 4 * 86400
|
||||
d.sched.answerCard(c, 1)
|
||||
# cards in learning
|
||||
##################################################
|
||||
assert ni(c, 1) == 30
|
||||
assert ni(c, 2) == (30+180)//2
|
||||
assert ni(c, 2) == (30 + 180) // 2
|
||||
assert ni(c, 3) == 180
|
||||
assert ni(c, 4) == 4*86400
|
||||
assert ni(c, 4) == 4 * 86400
|
||||
d.sched.answerCard(c, 3)
|
||||
assert ni(c, 1) == 30
|
||||
assert ni(c, 2) == (180+600)//2
|
||||
assert ni(c, 2) == (180 + 600) // 2
|
||||
assert ni(c, 3) == 600
|
||||
assert ni(c, 4) == 4*86400
|
||||
assert ni(c, 4) == 4 * 86400
|
||||
d.sched.answerCard(c, 3)
|
||||
# normal graduation is tomorrow
|
||||
assert ni(c, 3) == 1*86400
|
||||
assert ni(c, 4) == 4*86400
|
||||
assert ni(c, 3) == 1 * 86400
|
||||
assert ni(c, 4) == 4 * 86400
|
||||
# lapsed cards
|
||||
##################################################
|
||||
c.type = 2
|
||||
c.ivl = 100
|
||||
c.factor = STARTING_FACTOR
|
||||
assert ni(c, 1) == 60
|
||||
assert ni(c, 3) == 100*86400
|
||||
assert ni(c, 4) == 101*86400
|
||||
assert ni(c, 3) == 100 * 86400
|
||||
assert ni(c, 4) == 101 * 86400
|
||||
# review cards
|
||||
##################################################
|
||||
c.queue = 2
|
||||
|
@ -558,8 +593,8 @@ def test_nextIvl():
|
|||
# failing it should put it at 60s
|
||||
assert ni(c, 1) == 60
|
||||
# or 1 day if relearn is false
|
||||
d.sched._cardConf(c)['lapse']['delays']=[]
|
||||
assert ni(c, 1) == 1*86400
|
||||
d.sched._cardConf(c)["lapse"]["delays"] = []
|
||||
assert ni(c, 1) == 1 * 86400
|
||||
# (* 100 1.2 86400)10368000.0
|
||||
assert ni(c, 2) == 10368000
|
||||
# (* 100 2.5 86400)21600000.0
|
||||
|
@ -568,14 +603,15 @@ def test_nextIvl():
|
|||
assert ni(c, 4) == 28080000
|
||||
assert d.sched.nextIvlStr(c, 4) == "10.8 months"
|
||||
|
||||
|
||||
def test_bury():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
f = d.newNote()
|
||||
f['Front'] = "two"
|
||||
f["Front"] = "two"
|
||||
d.addNote(f)
|
||||
c2 = f.cards()[0]
|
||||
# burying
|
||||
|
@ -590,11 +626,14 @@ def test_bury():
|
|||
assert not d.sched.getCard()
|
||||
|
||||
d.sched.unburyCardsForDeck(type="manual")
|
||||
c.load(); assert c.queue == 0
|
||||
c2.load(); assert c2.queue == -2
|
||||
c.load()
|
||||
assert c.queue == 0
|
||||
c2.load()
|
||||
assert c2.queue == -2
|
||||
|
||||
d.sched.unburyCardsForDeck(type="siblings")
|
||||
c2.load(); assert c2.queue == 0
|
||||
c2.load()
|
||||
assert c2.queue == 0
|
||||
|
||||
d.sched.buryCards([c.id, c2.id])
|
||||
d.sched.unburyCardsForDeck(type="all")
|
||||
|
@ -603,10 +642,11 @@ def test_bury():
|
|||
|
||||
assert d.sched.counts() == (2, 0, 0)
|
||||
|
||||
|
||||
def test_suspend():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
# suspending
|
||||
|
@ -620,7 +660,11 @@ def test_suspend():
|
|||
d.reset()
|
||||
assert d.sched.getCard()
|
||||
# should cope with rev cards being relearnt
|
||||
c.due = 0; c.ivl = 100; c.type = 2; c.queue = 2; c.flush()
|
||||
c.due = 0
|
||||
c.ivl = 100
|
||||
c.type = 2
|
||||
c.queue = 2
|
||||
c.flush()
|
||||
d.reset()
|
||||
c = d.sched.getCard()
|
||||
d.sched.answerCard(c, 1)
|
||||
|
@ -648,10 +692,11 @@ def test_suspend():
|
|||
assert c.did != 1
|
||||
assert c.odue == 1
|
||||
|
||||
|
||||
def test_filt_reviewing_early_normal():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.ivl = 100
|
||||
|
@ -663,7 +708,7 @@ def test_filt_reviewing_early_normal():
|
|||
c.startTimer()
|
||||
c.flush()
|
||||
d.reset()
|
||||
assert d.sched.counts() == (0,0,0)
|
||||
assert d.sched.counts() == (0, 0, 0)
|
||||
# create a dynamic deck and refresh it
|
||||
did = d.decks.newDyn("Cram")
|
||||
d.sched.rebuildDyn(did)
|
||||
|
@ -671,14 +716,14 @@ def test_filt_reviewing_early_normal():
|
|||
# should appear as normal in the deck list
|
||||
assert sorted(d.sched.deckDueList())[0][2] == 1
|
||||
# and should appear in the counts
|
||||
assert d.sched.counts() == (0,0,1)
|
||||
assert d.sched.counts() == (0, 0, 1)
|
||||
# grab it and check estimates
|
||||
c = d.sched.getCard()
|
||||
assert d.sched.answerButtons(c) == 4
|
||||
assert d.sched.nextIvl(c, 1) == 600
|
||||
assert d.sched.nextIvl(c, 2) == int(75*1.2)*86400
|
||||
assert d.sched.nextIvl(c, 3) == int(75*2.5)*86400
|
||||
assert d.sched.nextIvl(c, 4) == int(75*2.5*1.15)*86400
|
||||
assert d.sched.nextIvl(c, 2) == int(75 * 1.2) * 86400
|
||||
assert d.sched.nextIvl(c, 3) == int(75 * 2.5) * 86400
|
||||
assert d.sched.nextIvl(c, 4) == int(75 * 2.5 * 1.15) * 86400
|
||||
|
||||
# answer 'good'
|
||||
d.sched.answerCard(c, 3)
|
||||
|
@ -688,8 +733,7 @@ def test_filt_reviewing_early_normal():
|
|||
# should not be in learning
|
||||
assert c.queue == 2
|
||||
# should be logged as a cram rep
|
||||
assert d.db.scalar(
|
||||
"select type from revlog order by id desc limit 1") == 3
|
||||
assert d.db.scalar("select type from revlog order by id desc limit 1") == 3
|
||||
|
||||
# due in 75 days, so it's been waiting 25 days
|
||||
c.ivl = 100
|
||||
|
@ -699,20 +743,21 @@ def test_filt_reviewing_early_normal():
|
|||
d.reset()
|
||||
c = d.sched.getCard()
|
||||
|
||||
assert d.sched.nextIvl(c, 2) == 60*86400
|
||||
assert d.sched.nextIvl(c, 3) == 100*86400
|
||||
assert d.sched.nextIvl(c, 4) == 114*86400
|
||||
assert d.sched.nextIvl(c, 2) == 60 * 86400
|
||||
assert d.sched.nextIvl(c, 3) == 100 * 86400
|
||||
assert d.sched.nextIvl(c, 4) == 114 * 86400
|
||||
|
||||
|
||||
def test_filt_keep_lrn_state():
|
||||
d = getEmptyCol()
|
||||
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
|
||||
# fail the card outside filtered deck
|
||||
c = d.sched.getCard()
|
||||
d.sched._cardConf(c)['new']['delays'] = [1, 10, 61]
|
||||
d.sched._cardConf(c)["new"]["delays"] = [1, 10, 61]
|
||||
d.decks.save()
|
||||
|
||||
d.sched.answerCard(c, 1)
|
||||
|
@ -736,30 +781,31 @@ def test_filt_keep_lrn_state():
|
|||
# should be able to advance learning steps
|
||||
d.sched.answerCard(c, 3)
|
||||
# should be due at least an hour in the future
|
||||
assert c.due - intTime() > 60*60
|
||||
assert c.due - intTime() > 60 * 60
|
||||
|
||||
# emptying the deck preserves learning state
|
||||
d.sched.emptyDyn(did)
|
||||
c.load()
|
||||
assert c.type == c.queue == 1
|
||||
assert c.left == 1001
|
||||
assert c.due - intTime() > 60*60
|
||||
assert c.due - intTime() > 60 * 60
|
||||
|
||||
|
||||
def test_preview():
|
||||
# add cards
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
orig = copy.copy(c)
|
||||
f2 = d.newNote()
|
||||
f2['Front'] = "two"
|
||||
f2["Front"] = "two"
|
||||
d.addNote(f2)
|
||||
# cram deck
|
||||
did = d.decks.newDyn("Cram")
|
||||
cram = d.decks.get(did)
|
||||
cram['resched'] = False
|
||||
cram["resched"] = False
|
||||
d.sched.rebuildDyn(did)
|
||||
d.reset()
|
||||
# grab the first card
|
||||
|
@ -793,22 +839,25 @@ def test_preview():
|
|||
assert c.reps == 0
|
||||
assert c.type == 0
|
||||
|
||||
|
||||
def test_ordcycle():
|
||||
d = getEmptyCol()
|
||||
# add two more templates and set second active
|
||||
m = d.models.current(); mm = d.models
|
||||
m = d.models.current()
|
||||
mm = d.models
|
||||
t = mm.newTemplate("Reverse")
|
||||
t['qfmt'] = "{{Back}}"
|
||||
t['afmt'] = "{{Front}}"
|
||||
t["qfmt"] = "{{Back}}"
|
||||
t["afmt"] = "{{Front}}"
|
||||
mm.addTemplate(m, t)
|
||||
t = mm.newTemplate("f2")
|
||||
t['qfmt'] = "{{Front}}"
|
||||
t['afmt'] = "{{Back}}"
|
||||
t["qfmt"] = "{{Front}}"
|
||||
t["afmt"] = "{{Back}}"
|
||||
mm.addTemplate(m, t)
|
||||
mm.save(m)
|
||||
# create a new note; it should have 3 cards
|
||||
f = d.newNote()
|
||||
f['Front'] = "1"; f['Back'] = "1"
|
||||
f["Front"] = "1"
|
||||
f["Back"] = "1"
|
||||
d.addNote(f)
|
||||
assert d.cardCount() == 3
|
||||
d.reset()
|
||||
|
@ -817,10 +866,12 @@ def test_ordcycle():
|
|||
assert d.sched.getCard().ord == 1
|
||||
assert d.sched.getCard().ord == 2
|
||||
|
||||
|
||||
def test_counts_idx():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
assert d.sched.counts() == (1, 0, 0)
|
||||
|
@ -839,10 +890,11 @@ def test_counts_idx():
|
|||
d.sched.answerCard(c, 1)
|
||||
assert d.sched.counts() == (0, 1, 0)
|
||||
|
||||
|
||||
def test_repCounts():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
# lrnReps should be accurate on pass/fail
|
||||
|
@ -860,7 +912,7 @@ def test_repCounts():
|
|||
d.sched.answerCard(d.sched.getCard(), 3)
|
||||
assert d.sched.counts() == (0, 0, 0)
|
||||
f = d.newNote()
|
||||
f['Front'] = "two"
|
||||
f["Front"] = "two"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
# initial pass should be correct too
|
||||
|
@ -872,14 +924,14 @@ def test_repCounts():
|
|||
assert d.sched.counts() == (0, 0, 0)
|
||||
# immediate graduate should work
|
||||
f = d.newNote()
|
||||
f['Front'] = "three"
|
||||
f["Front"] = "three"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
d.sched.answerCard(d.sched.getCard(), 4)
|
||||
assert d.sched.counts() == (0, 0, 0)
|
||||
# and failing a review should too
|
||||
f = d.newNote()
|
||||
f['Front'] = "three"
|
||||
f["Front"] = "three"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.type = 2
|
||||
|
@ -891,12 +943,13 @@ def test_repCounts():
|
|||
d.sched.answerCard(d.sched.getCard(), 1)
|
||||
assert d.sched.counts() == (0, 1, 0)
|
||||
|
||||
|
||||
def test_timing():
|
||||
d = getEmptyCol()
|
||||
# add a few review cards, due today
|
||||
for i in range(5):
|
||||
f = d.newNote()
|
||||
f['Front'] = "num"+str(i)
|
||||
f["Front"] = "num" + str(i)
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.type = 2
|
||||
|
@ -917,11 +970,12 @@ def test_timing():
|
|||
c = d.sched.getCard()
|
||||
assert c.queue == 1
|
||||
|
||||
|
||||
def test_collapse():
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
# test collapsing
|
||||
|
@ -931,16 +985,17 @@ def test_collapse():
|
|||
d.sched.answerCard(c, 4)
|
||||
assert not d.sched.getCard()
|
||||
|
||||
|
||||
def test_deckDue():
|
||||
d = getEmptyCol()
|
||||
# add a note with default deck
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
# and one that's a child
|
||||
f = d.newNote()
|
||||
f['Front'] = "two"
|
||||
default1 = f.model()['did'] = d.decks.id("Default::1")
|
||||
f["Front"] = "two"
|
||||
default1 = f.model()["did"] = d.decks.id("Default::1")
|
||||
d.addNote(f)
|
||||
# make it a review card
|
||||
c = f.cards()[0]
|
||||
|
@ -949,13 +1004,13 @@ def test_deckDue():
|
|||
c.flush()
|
||||
# add one more with a new deck
|
||||
f = d.newNote()
|
||||
f['Front'] = "two"
|
||||
foobar = f.model()['did'] = d.decks.id("foo::bar")
|
||||
f["Front"] = "two"
|
||||
foobar = f.model()["did"] = d.decks.id("foo::bar")
|
||||
d.addNote(f)
|
||||
# and one that's a sibling
|
||||
f = d.newNote()
|
||||
f['Front'] = "three"
|
||||
foobaz = f.model()['did'] = d.decks.id("foo::baz")
|
||||
f["Front"] = "three"
|
||||
foobaz = f.model()["did"] = d.decks.id("foo::baz")
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
assert len(d.decks.decks) == 5
|
||||
|
@ -977,10 +1032,12 @@ def test_deckDue():
|
|||
assert tree[0][5][0][2] == 1
|
||||
assert tree[0][5][0][4] == 0
|
||||
# code should not fail if a card has an invalid deck
|
||||
c.did = 12345; c.flush()
|
||||
c.did = 12345
|
||||
c.flush()
|
||||
d.sched.deckDueList()
|
||||
d.sched.deckDueTree()
|
||||
|
||||
|
||||
def test_deckTree():
|
||||
d = getEmptyCol()
|
||||
d.decks.id("new::b::c")
|
||||
|
@ -990,75 +1047,80 @@ def test_deckTree():
|
|||
names.remove("new")
|
||||
assert "new" not in names
|
||||
|
||||
|
||||
def test_deckFlow():
|
||||
d = getEmptyCol()
|
||||
# add a note with default deck
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
# and one that's a child
|
||||
f = d.newNote()
|
||||
f['Front'] = "two"
|
||||
default1 = f.model()['did'] = d.decks.id("Default::2")
|
||||
f["Front"] = "two"
|
||||
default1 = f.model()["did"] = d.decks.id("Default::2")
|
||||
d.addNote(f)
|
||||
# and another that's higher up
|
||||
f = d.newNote()
|
||||
f['Front'] = "three"
|
||||
default1 = f.model()['did'] = d.decks.id("Default::1")
|
||||
f["Front"] = "three"
|
||||
default1 = f.model()["did"] = d.decks.id("Default::1")
|
||||
d.addNote(f)
|
||||
# should get top level one first, then ::1, then ::2
|
||||
d.reset()
|
||||
assert d.sched.counts() == (3,0,0)
|
||||
assert d.sched.counts() == (3, 0, 0)
|
||||
for i in "one", "three", "two":
|
||||
c = d.sched.getCard()
|
||||
assert c.note()['Front'] == i
|
||||
assert c.note()["Front"] == i
|
||||
d.sched.answerCard(c, 3)
|
||||
|
||||
|
||||
def test_reorder():
|
||||
d = getEmptyCol()
|
||||
# add a note with default deck
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
f2 = d.newNote()
|
||||
f2['Front'] = "two"
|
||||
f2["Front"] = "two"
|
||||
d.addNote(f2)
|
||||
assert f2.cards()[0].due == 2
|
||||
found=False
|
||||
found = False
|
||||
# 50/50 chance of being reordered
|
||||
for i in range(20):
|
||||
d.sched.randomizeCards(1)
|
||||
if f.cards()[0].due != f.id:
|
||||
found=True
|
||||
found = True
|
||||
break
|
||||
assert found
|
||||
d.sched.orderCards(1)
|
||||
assert f.cards()[0].due == 1
|
||||
# shifting
|
||||
f3 = d.newNote()
|
||||
f3['Front'] = "three"
|
||||
f3["Front"] = "three"
|
||||
d.addNote(f3)
|
||||
f4 = d.newNote()
|
||||
f4['Front'] = "four"
|
||||
f4["Front"] = "four"
|
||||
d.addNote(f4)
|
||||
assert f.cards()[0].due == 1
|
||||
assert f2.cards()[0].due == 2
|
||||
assert f3.cards()[0].due == 3
|
||||
assert f4.cards()[0].due == 4
|
||||
d.sched.sortCards([
|
||||
f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
|
||||
d.sched.sortCards([f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
|
||||
assert f.cards()[0].due == 3
|
||||
assert f2.cards()[0].due == 4
|
||||
assert f3.cards()[0].due == 1
|
||||
assert f4.cards()[0].due == 2
|
||||
|
||||
|
||||
def test_forget():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.queue = 2; c.type = 2; c.ivl = 100; c.due = 0
|
||||
c.queue = 2
|
||||
c.type = 2
|
||||
c.ivl = 100
|
||||
c.due = 0
|
||||
c.flush()
|
||||
d.reset()
|
||||
assert d.sched.counts() == (0, 0, 1)
|
||||
|
@ -1066,10 +1128,11 @@ def test_forget():
|
|||
d.reset()
|
||||
assert d.sched.counts() == (1, 0, 0)
|
||||
|
||||
|
||||
def test_resched():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
d.sched.reschedCards([c.id], 0, 0)
|
||||
|
@ -1079,14 +1142,15 @@ def test_resched():
|
|||
assert c.queue == c.type == 2
|
||||
d.sched.reschedCards([c.id], 1, 1)
|
||||
c.load()
|
||||
assert c.due == d.sched.today+1
|
||||
assert c.due == d.sched.today + 1
|
||||
assert c.ivl == +1
|
||||
|
||||
|
||||
def test_norelearn():
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.type = 2
|
||||
|
@ -1100,13 +1164,15 @@ def test_norelearn():
|
|||
c.flush()
|
||||
d.reset()
|
||||
d.sched.answerCard(c, 1)
|
||||
d.sched._cardConf(c)['lapse']['delays'] = []
|
||||
d.sched._cardConf(c)["lapse"]["delays"] = []
|
||||
d.sched.answerCard(c, 1)
|
||||
|
||||
|
||||
def test_failmult():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.type = 2
|
||||
|
@ -1118,19 +1184,20 @@ def test_failmult():
|
|||
c.lapses = 1
|
||||
c.startTimer()
|
||||
c.flush()
|
||||
d.sched._cardConf(c)['lapse']['mult'] = 0.5
|
||||
d.sched._cardConf(c)["lapse"]["mult"] = 0.5
|
||||
c = d.sched.getCard()
|
||||
d.sched.answerCard(c, 1)
|
||||
assert c.ivl == 50
|
||||
d.sched.answerCard(c, 1)
|
||||
assert c.ivl == 25
|
||||
|
||||
|
||||
def test_moveVersions():
|
||||
col = getEmptyCol()
|
||||
col.changeSchedulerVer(1)
|
||||
|
||||
n = col.newNote()
|
||||
n['Front'] = "one"
|
||||
n["Front"] = "one"
|
||||
col.addNote(n)
|
||||
|
||||
# make it a learning card
|
||||
|
@ -1168,8 +1235,10 @@ def test_moveVersions():
|
|||
col.changeSchedulerVer(2)
|
||||
# card with 100 day interval, answering again
|
||||
col.sched.reschedCards([c.id], 100, 100)
|
||||
c.load(); c.due = 0; c.flush()
|
||||
col.sched._cardConf(c)['lapse']['mult'] = 0.5
|
||||
c.load()
|
||||
c.due = 0
|
||||
c.flush()
|
||||
col.sched._cardConf(c)["lapse"]["mult"] = 0.5
|
||||
col.sched.reset()
|
||||
c = col.sched.getCard()
|
||||
col.sched.answerCard(c, 1)
|
||||
|
@ -1178,6 +1247,7 @@ def test_moveVersions():
|
|||
c.load()
|
||||
assert c.due == 50
|
||||
|
||||
|
||||
# cards with a due date earlier than the collection should retain
|
||||
# their due date when removed
|
||||
def test_negativeDueFilter():
|
||||
|
@ -1185,7 +1255,8 @@ def test_negativeDueFilter():
|
|||
|
||||
# card due prior to collection date
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"; f['Back'] = "two"
|
||||
f["Front"] = "one"
|
||||
f["Back"] = "two"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
c.due = -5
|
||||
|
@ -1201,4 +1272,3 @@ def test_negativeDueFilter():
|
|||
|
||||
c.load()
|
||||
assert c.due == -5
|
||||
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
# coding: utf-8
|
||||
|
||||
import os
|
||||
from tests.shared import getEmptyCol
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from tests.shared import getEmptyCol
|
||||
|
||||
|
||||
def test_stats():
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "foo"
|
||||
f["Front"] = "foo"
|
||||
d.addNote(f)
|
||||
c = f.cards()[0]
|
||||
# card stats
|
||||
|
@ -17,15 +20,20 @@ def test_stats():
|
|||
d.sched.answerCard(c, 2)
|
||||
assert d.cardStats(c)
|
||||
|
||||
|
||||
def test_graphs_empty():
|
||||
d = getEmptyCol()
|
||||
assert d.stats().report()
|
||||
|
||||
|
||||
def test_graphs():
|
||||
from anki import Collection as aopen
|
||||
d = aopen(os.path.expanduser("~/test.anki2"))
|
||||
|
||||
dir = tempfile.gettempdir()
|
||||
|
||||
d = aopen(os.path.join(dir, "test.anki2"))
|
||||
g = d.stats()
|
||||
rep = g.report()
|
||||
with open(os.path.expanduser("~/test.html"), "w") as f:
|
||||
with open(os.path.join(dir, "test.html"), "w") as f:
|
||||
f.write(rep)
|
||||
return
|
||||
|
|
|
@ -2,15 +2,18 @@ from anki.template import Template
|
|||
|
||||
|
||||
def test_remove_formatting_from_mathjax():
|
||||
t = Template('')
|
||||
assert t._removeFormattingFromMathjax(r'\(2^{{c3::2}}\)', 3) == r'\(2^{{C3::2}}\)'
|
||||
t = Template("")
|
||||
assert t._removeFormattingFromMathjax(r"\(2^{{c3::2}}\)", 3) == r"\(2^{{C3::2}}\)"
|
||||
|
||||
txt = (r'{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) '
|
||||
r'{{c4::blah}} {{c5::text with \(x^2\) jax}}')
|
||||
txt = (
|
||||
r"{{c1::ok}} \(2^2\) {{c2::not ok}} \(2^{{c3::2}}\) \(x^3\) "
|
||||
r"{{c4::blah}} {{c5::text with \(x^2\) jax}}"
|
||||
)
|
||||
# Cloze 2 is not in MathJax, so it should not get protected against
|
||||
# formatting.
|
||||
assert t._removeFormattingFromMathjax(txt, 2) == txt
|
||||
|
||||
txt = r'\(a\) {{c1::b}} \[ {{c1::c}} \]'
|
||||
txt = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
|
||||
assert t._removeFormattingFromMathjax(txt, 1) == (
|
||||
r'\(a\) {{c1::b}} \[ {{C1::c}} \]')
|
||||
r"\(a\) {{c1::b}} \[ {{C1::c}} \]"
|
||||
)
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
# coding: utf-8
|
||||
|
||||
import time
|
||||
from tests.shared import getEmptyCol
|
||||
|
||||
from anki.consts import *
|
||||
from tests.shared import getEmptyCol
|
||||
|
||||
|
||||
def test_op():
|
||||
d = getEmptyCol()
|
||||
|
@ -10,7 +12,7 @@ def test_op():
|
|||
assert not d.undoName()
|
||||
# let's adjust a study option
|
||||
d.save("studyopts")
|
||||
d.conf['abc'] = 5
|
||||
d.conf["abc"] = 5
|
||||
# it should be listed as undoable
|
||||
assert d.undoName() == "studyopts"
|
||||
# with about 5 minutes until it's clobbered
|
||||
|
@ -18,7 +20,7 @@ def test_op():
|
|||
# undoing should restore the old value
|
||||
d.undo()
|
||||
assert not d.undoName()
|
||||
assert 'abc' not in d.conf
|
||||
assert "abc" not in d.conf
|
||||
# an (auto)save will clear the undo
|
||||
d.save("foo")
|
||||
assert d.undoName() == "foo"
|
||||
|
@ -27,7 +29,7 @@ def test_op():
|
|||
# and a review will, too
|
||||
d.save("add")
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
assert d.undoName() == "add"
|
||||
|
@ -35,11 +37,12 @@ def test_op():
|
|||
d.sched.answerCard(c, 2)
|
||||
assert d.undoName() == "Review"
|
||||
|
||||
|
||||
def test_review():
|
||||
d = getEmptyCol()
|
||||
d.conf['counts'] = COUNT_REMAINING
|
||||
d.conf["counts"] = COUNT_REMAINING
|
||||
f = d.newNote()
|
||||
f['Front'] = "one"
|
||||
f["Front"] = "one"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
assert not d.undoName()
|
||||
|
@ -62,7 +65,7 @@ def test_review():
|
|||
assert not d.undoName()
|
||||
# we should be able to undo multiple answers too
|
||||
f = d.newNote()
|
||||
f['Front'] = "two"
|
||||
f["Front"] = "two"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
assert d.sched.counts() == (2, 0, 0)
|
||||
|
@ -85,5 +88,3 @@ def test_review():
|
|||
assert d.undoName() == "foo"
|
||||
d.undo()
|
||||
assert not d.undoName()
|
||||
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from anki.utils import fmtTimeSpan
|
||||
|
||||
|
||||
def test_fmtTimeSpan():
|
||||
assert fmtTimeSpan(5) == "5 seconds"
|
||||
assert fmtTimeSpan(5, inTime=True) == "in 5 seconds"
|
||||
|
|
|
@ -10,11 +10,7 @@ set -e
|
|||
BIN="$(cd "`dirname "$0"`"; pwd)"
|
||||
export PYTHONPATH=${BIN}/..:${PYTHONPATH}
|
||||
|
||||
# favour nosetests3 if available
|
||||
nose=nosetests
|
||||
if which nosetests3 >/dev/null 2>&1; then
|
||||
nose=nosetests3
|
||||
fi
|
||||
nose="python -m nose2 --plugin=nose2.plugins.mp -N 16"
|
||||
|
||||
dir=.
|
||||
|
||||
|
@ -24,7 +20,4 @@ else
|
|||
lim="tests.test_$1"
|
||||
fi
|
||||
|
||||
if [ x$coverage != x ]; then
|
||||
args="--with-coverage"
|
||||
fi
|
||||
(cd $dir && $nose -s --processes=16 --process-timeout=300 $lim $args --cover-package=anki)
|
||||
(cd $dir && $nose $lim $args)
|
||||
|
|
4
ts/package-lock.json
generated
4
ts/package-lock.json
generated
|
@ -10,7 +10,7 @@
|
|||
"integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/sizzle": "*"
|
||||
"@types/sizzle": "2.3.2"
|
||||
}
|
||||
},
|
||||
"@types/jqueryui": {
|
||||
|
@ -19,7 +19,7 @@
|
|||
"integrity": "sha512-bHE7BiG+5Sviy/eA9Npz5HHF3hv40XjaEbpYtSJPaNwuyxhSJ0qWlE8C5DgNMfobVOZ2aSTrM1iGDCGmvlbxOg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/jquery": "*"
|
||||
"@types/jquery": "3.3.31"
|
||||
}
|
||||
},
|
||||
"@types/mathjax": {
|
||||
|
|
Loading…
Reference in a new issue