mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -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
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 12
|
node-version: 12
|
||||||
- name: Install dependencies
|
- name: Set up Protoc
|
||||||
run: |
|
uses: Arduino/actions/setup-protoc@master
|
||||||
sudo apt install portaudio19-dev
|
|
||||||
pip install -r requirements.qt
|
|
||||||
- name: Run checks
|
- name: Run checks
|
||||||
run: |
|
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
|
.*.swp
|
||||||
.DS_Store
|
|
||||||
.build
|
.build
|
||||||
.coverage
|
.coverage
|
||||||
|
.DS_Store
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
.pytype
|
.pytype
|
||||||
__pycache__
|
__pycache__
|
||||||
anki/buildhash.py
|
anki/buildhash.py
|
||||||
|
anki/backend_pb2.*
|
||||||
aqt/forms
|
aqt/forms
|
||||||
locale
|
locale
|
||||||
|
rs/ankirs/src/backend_proto.rs
|
||||||
|
rs/target
|
||||||
tools/runanki.system
|
tools/runanki.system
|
||||||
ts/node_modules
|
ts/node_modules
|
||||||
web/deckbrowser.js
|
web/deckbrowser.js
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[settings]
|
[settings]
|
||||||
skip=aqt/forms
|
skip=aqt/forms,anki/backend_pb2.py,backend_pb2.pyi
|
||||||
multi_line_output=3
|
multi_line_output=3
|
||||||
include_trailing_comma=True
|
include_trailing_comma=True
|
||||||
force_grid_wrap=0
|
force_grid_wrap=0
|
||||||
|
|
165
Makefile
165
Makefile
|
@ -1,13 +1,14 @@
|
||||||
PREFIX := /usr
|
PREFIX := /usr
|
||||||
SHELL := bash
|
SHELL := bash
|
||||||
.SHELLFLAGS := -eu -o pipefail -c
|
.SHELLFLAGS := -eu -o pipefail -c
|
||||||
.ONESHELL:
|
|
||||||
.DELETE_ON_ERROR:
|
.DELETE_ON_ERROR:
|
||||||
MAKEFLAGS += --warn-undefined-variables
|
MAKEFLAGS += --warn-undefined-variables
|
||||||
MAKEFLAGS += --no-builtin-rules
|
MAKEFLAGS += --no-builtin-rules
|
||||||
RUNARGS :=
|
RUNARGS :=
|
||||||
.SUFFIXES:
|
.SUFFIXES:
|
||||||
BLACKARGS := -t py36 anki aqt
|
BLACKARGS := -t py36 anki aqt tests
|
||||||
|
RUSTARGS := --release --strip
|
||||||
|
ISORTARGS := anki aqt tests
|
||||||
|
|
||||||
$(shell mkdir -p .build)
|
$(shell mkdir -p .build)
|
||||||
|
|
||||||
|
@ -40,6 +41,8 @@ install:
|
||||||
-xdg-mime default anki.desktop application/x-apkg
|
-xdg-mime default anki.desktop application/x-apkg
|
||||||
@echo
|
@echo
|
||||||
@echo "Install complete."
|
@echo "Install complete."
|
||||||
|
# fixme: _ankirs.so needs to be copied into system python env or
|
||||||
|
# 'maturin build' used
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
rm -rf ${DESTDIR}${PREFIX}/share/anki
|
rm -rf ${DESTDIR}${PREFIX}/share/anki
|
||||||
|
@ -55,20 +58,48 @@ uninstall:
|
||||||
# Prerequisites
|
# 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 $<
|
pip install -r $<
|
||||||
touch $@
|
@touch $@
|
||||||
|
|
||||||
.build/pycheckreqs: requirements.check .build/pyrunreqs
|
.build/py-check-reqs: requirements.check .build/py-run-deps
|
||||||
pip install -r $<
|
pip install -r $<
|
||||||
./tools/typecheck-setup.sh
|
./tools/typecheck-setup.sh
|
||||||
touch $@
|
@touch $@
|
||||||
|
|
||||||
.build/jsreqs: ts/package.json
|
# TS prerequisites
|
||||||
|
######################
|
||||||
|
|
||||||
|
.build/ts-deps: ts/package.json
|
||||||
(cd ts && npm i)
|
(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
|
# Typescript source
|
||||||
######################
|
######################
|
||||||
|
@ -76,18 +107,31 @@ RUNREQS := .build/pyrunreqs .build/jsreqs
|
||||||
TSDEPS := $(wildcard ts/src/*.ts)
|
TSDEPS := $(wildcard ts/src/*.ts)
|
||||||
JSDEPS := $(patsubst ts/src/%.ts, web/%.js, $(TSDEPS))
|
JSDEPS := $(patsubst ts/src/%.ts, web/%.js, $(TSDEPS))
|
||||||
|
|
||||||
|
# Rust source
|
||||||
|
######################
|
||||||
|
|
||||||
|
RSDEPS := $(shell find rs -type f | egrep -v 'target|/\.|proto.rs')
|
||||||
|
|
||||||
# Building
|
# Building
|
||||||
######################
|
######################
|
||||||
|
|
||||||
BUILDDEPS := .build/ui .build/js
|
BUILDDEPS := .build/ui .build/js .build/rs .build/py-proto
|
||||||
|
|
||||||
.build/ui: $(RUNREQS) $(shell find designer -type f)
|
.build/ui: $(RUNREQS) $(shell find designer -type f)
|
||||||
./tools/build_ui.sh
|
./tools/build_ui.sh
|
||||||
touch $@
|
@touch $@
|
||||||
|
|
||||||
.build/js: .build/jsreqs $(TSDEPS)
|
.build/js: .build/ts-deps $(TSDEPS)
|
||||||
(cd ts && npm run build)
|
(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
|
.PHONY: build clean
|
||||||
|
|
||||||
|
@ -97,6 +141,7 @@ build: $(BUILDDEPS)
|
||||||
clean:
|
clean:
|
||||||
rm -rf .build
|
rm -rf .build
|
||||||
rm -rf $(JSDEPS)
|
rm -rf $(JSDEPS)
|
||||||
|
rm -rf rs/target
|
||||||
|
|
||||||
# Running
|
# Running
|
||||||
######################
|
######################
|
||||||
|
@ -109,61 +154,89 @@ run: build
|
||||||
######################
|
######################
|
||||||
|
|
||||||
.PHONY: check
|
.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
|
# 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
|
mypy anki aqt
|
||||||
touch $@
|
@touch $@
|
||||||
|
|
||||||
.build/pytest: $(PYCHECKDEPS) $(wildcard tests/*.py)
|
.build/py-test: $(PYCHECKDEPS) $(PYTESTDEPS)
|
||||||
./tools/tests.sh
|
./tools/tests.sh
|
||||||
touch $@
|
@touch $@
|
||||||
|
|
||||||
.build/pylint: $(PYCHECKDEPS)
|
.build/py-lint: $(PYCHECKDEPS)
|
||||||
pylint -j 0 --rcfile=.pylintrc -f colorized --extension-pkg-whitelist=PyQt5 anki aqt
|
pylint -j 0 --rcfile=.pylintrc -f colorized --extension-pkg-whitelist=PyQt5,_ankirs anki aqt
|
||||||
touch $@
|
@touch $@
|
||||||
|
|
||||||
.build/pyimports: $(PYCHECKDEPS)
|
.build/py-imports: $(PYCHECKDEPS) $(PYTESTDEPS)
|
||||||
isort anki aqt --check # if this fails, run 'make fixpyimports'
|
isort $(ISORTARGS) --check # if this fails, run 'make fix-py-imports'
|
||||||
touch $@
|
@touch $@
|
||||||
|
|
||||||
.build/pyfmt: $(PYCHECKDEPS)
|
.build/py-fmt: $(PYCHECKDEPS) $(PYTESTDEPS)
|
||||||
black --check $(BLACKARGS) # if this fails, run 'make fixpyfmt'
|
black --check $(BLACKARGS) # if this fails, run 'make fix-py-fmt'
|
||||||
touch $@
|
@touch $@
|
||||||
|
|
||||||
.PHONY: mypy pytest pylint pyimports pyfmt
|
.PHONY: py-mypy py-test py-lint py-imports py-fmt
|
||||||
mypy: .build/mypy
|
py-mypy: .build/py-mypy
|
||||||
pytest: .build/pytest
|
py-test: .build/py-test
|
||||||
pylint: .build/pylint
|
py-lint: .build/py-lint
|
||||||
pyimports: .build/pyimports
|
py-imports: .build/py-imports
|
||||||
pyfmt: .build/pyfmt
|
py-fmt: .build/py-fmt
|
||||||
|
|
||||||
.PHONY: fixpyimports fixpyfmt
|
.PHONY: fix-py-imports fix-py-fmt
|
||||||
|
|
||||||
fixpyimports:
|
fix-py-imports:
|
||||||
isort anki aqt
|
isort $(ISORTARGS)
|
||||||
|
|
||||||
fixpyfmt:
|
fix-py-fmt:
|
||||||
black $(BLACKARGS) anki aqt
|
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
|
# Checking typescript
|
||||||
######################
|
######################
|
||||||
|
|
||||||
TSCHECKDEPS := $(BUILDDEPS) $(TSDEPS)
|
TSCHECKDEPS := $(BUILDDEPS) $(TSDEPS)
|
||||||
|
|
||||||
.build/checkpretty: $(TSCHECKDEPS)
|
.build/ts-fmt: $(TSCHECKDEPS)
|
||||||
(cd ts && npm run check-pretty) # if this fails, run 'make pretty'
|
(cd ts && npm run check-pretty) # if this fails, run 'make fix-ts-fmt'
|
||||||
touch $@
|
@touch $@
|
||||||
|
|
||||||
.build/pretty: $(TSCHECKDEPS)
|
.PHONY: fix-ts-fmt ts-fmt
|
||||||
|
ts-fmt: .build/ts-fmt
|
||||||
|
|
||||||
|
fix-ts-fmt:
|
||||||
(cd ts && npm run pretty)
|
(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
|
- mpv
|
||||||
- lame
|
- lame
|
||||||
- npm
|
- 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
|
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.
|
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:
|
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
|
Windows users
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
The build scripts have not been tested on Windows, and you'll find things
|
The build process uses a GNU makefile, so you'll either need to run
|
||||||
easiest if you build Anki using WSL.
|
GNU make via WSL (https://docs.microsoft.com/en-us/windows/wsl/install-win10)
|
||||||
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.latex # sets up hook
|
||||||
import anki.notes
|
import anki.notes
|
||||||
import anki.template
|
import anki.template
|
||||||
|
from anki.backend import Backend
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.db import DB
|
from anki.db import DB
|
||||||
|
@ -84,8 +85,12 @@ class _Collection:
|
||||||
ls: int
|
ls: int
|
||||||
conf: Dict[str, Any]
|
conf: Dict[str, Any]
|
||||||
_undo: List[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._debugLog = log
|
||||||
self.db = db
|
self.db = db
|
||||||
self.path = db._path
|
self.path = db._path
|
||||||
|
|
|
@ -8,6 +8,7 @@ import operator
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
||||||
|
|
||||||
|
import anki # pylint: disable=unused-import
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.errors import DeckRenameError
|
from anki.errors import DeckRenameError
|
||||||
from anki.hooks import runHook
|
from anki.hooks import runHook
|
||||||
|
@ -98,7 +99,7 @@ class DeckManager:
|
||||||
# Registry save/load
|
# Registry save/load
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
def __init__(self, col) -> None:
|
def __init__(self, col: "anki.storage._Collection") -> None:
|
||||||
self.col = col
|
self.col = col
|
||||||
self.decks = {}
|
self.decks = {}
|
||||||
self.dconf = {}
|
self.dconf = {}
|
||||||
|
|
|
@ -608,9 +608,7 @@ def findDupes(col, fieldName, search="") -> List[Tuple[Any, List]]:
|
||||||
# empty does not count as duplicate
|
# empty does not count as duplicate
|
||||||
if not val:
|
if not val:
|
||||||
continue
|
continue
|
||||||
if val not in vals:
|
vals.setdefault(val, []).append(nid)
|
||||||
vals[val] = []
|
|
||||||
vals[val].append(nid)
|
|
||||||
if len(vals[val]) == 2:
|
if len(vals[val]) == 2:
|
||||||
dupes.append((val, vals[val]))
|
dupes.append((val, vals[val]))
|
||||||
return dupes
|
return dupes
|
||||||
|
|
|
@ -556,6 +556,9 @@ select id from notes where mid = ?)"""
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def _updateRequired(self, m: NoteType) -> None:
|
def _updateRequired(self, m: NoteType) -> None:
|
||||||
|
self._updateRequiredNew(m)
|
||||||
|
|
||||||
|
def _updateRequiredLegacy(self, m: NoteType) -> None:
|
||||||
if m["type"] == MODEL_CLOZE:
|
if m["type"] == MODEL_CLOZE:
|
||||||
# nothing to do
|
# nothing to do
|
||||||
return
|
return
|
||||||
|
@ -566,6 +569,14 @@ select id from notes where mid = ?)"""
|
||||||
req.append([t["ord"], ret[0], ret[1]])
|
req.append([t["ord"], ret[0], ret[1]])
|
||||||
m["req"] = req
|
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(
|
def _reqForTemplate(
|
||||||
self, m: NoteType, flds: List[str], t: Template
|
self, m: NoteType, flds: List[str], t: Template
|
||||||
) -> Tuple[Union[str, List[int]], ...]:
|
) -> Tuple[Union[str, List[int]], ...]:
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
from typing import Any, List, Optional, Tuple
|
from typing import Any, List, Optional, Tuple
|
||||||
|
|
||||||
|
import anki # pylint: disable=unused-import
|
||||||
from anki.utils import (
|
from anki.utils import (
|
||||||
fieldChecksum,
|
fieldChecksum,
|
||||||
guid64,
|
guid64,
|
||||||
|
@ -19,7 +20,10 @@ class Note:
|
||||||
tags: List[str]
|
tags: List[str]
|
||||||
|
|
||||||
def __init__(
|
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:
|
) -> None:
|
||||||
assert not (model and id)
|
assert not (model and id)
|
||||||
self.col = col
|
self.col = col
|
||||||
|
|
|
@ -8,6 +8,7 @@ import os
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, Tuple
|
from typing import Any, Dict, Tuple
|
||||||
|
|
||||||
|
from anki.backend import Backend
|
||||||
from anki.collection import _Collection
|
from anki.collection import _Collection
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
from anki.db import DB
|
from anki.db import DB
|
||||||
|
@ -26,6 +27,10 @@ def Collection(
|
||||||
path: str, lock: bool = True, server: bool = False, log: bool = False
|
path: str, lock: bool = True, server: bool = False, log: bool = False
|
||||||
) -> _Collection:
|
) -> _Collection:
|
||||||
"Open a new or existing collection. Path must be unicode."
|
"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")
|
assert path.endswith(".anki2")
|
||||||
path = os.path.abspath(path)
|
path = os.path.abspath(path)
|
||||||
create = not os.path.exists(path)
|
create = not os.path.exists(path)
|
||||||
|
@ -46,7 +51,7 @@ def Collection(
|
||||||
db.execute("pragma journal_mode = wal")
|
db.execute("pragma journal_mode = wal")
|
||||||
db.setAutocommit(False)
|
db.setAutocommit(False)
|
||||||
# add db to col and do any remaining upgrades
|
# 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:
|
if ver < SCHEMA_VERSION:
|
||||||
_upgrade(col, ver)
|
_upgrade(col, ver)
|
||||||
elif ver > SCHEMA_VERSION:
|
elif ver > SCHEMA_VERSION:
|
||||||
|
@ -60,7 +65,11 @@ def Collection(
|
||||||
addBasicModel(col)
|
addBasicModel(col)
|
||||||
col.save()
|
col.save()
|
||||||
if lock:
|
if lock:
|
||||||
col.lock()
|
try:
|
||||||
|
col.lock()
|
||||||
|
except:
|
||||||
|
col.db.close()
|
||||||
|
raise
|
||||||
return col
|
return col
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import json
|
||||||
import re
|
import re
|
||||||
from typing import Callable, Dict, List, Tuple
|
from typing import Callable, Dict, List, Tuple
|
||||||
|
|
||||||
|
import anki # pylint: disable=unused-import
|
||||||
from anki.hooks import runHook
|
from anki.hooks import runHook
|
||||||
from anki.utils import ids2str, intTime
|
from anki.utils import ids2str, intTime
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ class TagManager:
|
||||||
# Registry save/load
|
# Registry save/load
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
def __init__(self, col) -> None:
|
def __init__(self, col: "anki.storage._Collection") -> None:
|
||||||
self.col = col
|
self.col = col
|
||||||
self.tags: Dict[str, int] = {}
|
self.tags: Dict[str, int] = {}
|
||||||
|
|
||||||
|
|
|
@ -210,14 +210,15 @@ class Template:
|
||||||
return "{unknown field %s}" % tag_name
|
return "{unknown field %s}" % tag_name
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
|
@classmethod
|
||||||
def clozeText(self, txt: str, ord: str, type: str) -> str:
|
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
|
reg = clozeReg
|
||||||
currentRegex = clozeReg % ord
|
currentRegex = clozeReg % ord
|
||||||
if not re.search(currentRegex, txt):
|
if not re.search(currentRegex, txt):
|
||||||
# No Cloze deletion was found in txt.
|
# No Cloze deletion was found in txt.
|
||||||
return ""
|
return ""
|
||||||
txt = self._removeFormattingFromMathjax(txt, ord)
|
txt = cls._removeFormattingFromMathjax(txt, ord)
|
||||||
|
|
||||||
def repl(m):
|
def repl(m):
|
||||||
# replace chosen cloze with type
|
# replace chosen cloze with type
|
||||||
|
@ -237,7 +238,8 @@ class Template:
|
||||||
# and display other clozes normally
|
# and display other clozes normally
|
||||||
return re.sub(reg % r"\d+", "\\2", txt)
|
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.
|
"""Marks all clozes within MathJax to prevent formatting them.
|
||||||
|
|
||||||
Active Cloze deletions within MathJax should not be wrapped inside
|
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
|
# Model attributes are stored in a dict keyed by strings. This type alias
|
||||||
# provides more descriptive function signatures than just 'Dict[str, Any]'
|
# provides more descriptive function signatures than just 'Dict[str, Any]'
|
||||||
|
@ -31,3 +31,8 @@ QAData = Tuple[
|
||||||
# Corresponds to 'cardFlags' column. TODO: document
|
# Corresponds to 'cardFlags' column. TODO: document
|
||||||
int,
|
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)
|
opts, args = parseArgs(argv)
|
||||||
|
|
||||||
# profile manager
|
# profile manager
|
||||||
pm = ProfileManager(opts.base)
|
pm = None
|
||||||
pmLoadResult = pm.setupMeta()
|
try:
|
||||||
|
pm = ProfileManager(opts.base)
|
||||||
|
pmLoadResult = pm.setupMeta()
|
||||||
|
except:
|
||||||
|
# will handle below
|
||||||
|
pass
|
||||||
|
|
||||||
# gl workarounds
|
if pm:
|
||||||
setupGL(pm)
|
# gl workarounds
|
||||||
|
setupGL(pm)
|
||||||
|
# apply user-provided scale factor
|
||||||
|
os.environ["QT_SCALE_FACTOR"] = str(pm.uiScale())
|
||||||
|
|
||||||
# opt in to full hidpi support?
|
# opt in to full hidpi support?
|
||||||
if not os.environ.get("ANKI_NOHIGHDPI"):
|
if not os.environ.get("ANKI_NOHIGHDPI"):
|
||||||
|
@ -348,9 +356,6 @@ def _run(argv=None, exec=True):
|
||||||
if os.environ.get("ANKI_SOFTWAREOPENGL"):
|
if os.environ.get("ANKI_SOFTWAREOPENGL"):
|
||||||
QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL)
|
QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL)
|
||||||
|
|
||||||
# apply user-provided scale factor
|
|
||||||
os.environ["QT_SCALE_FACTOR"] = str(pm.uiScale())
|
|
||||||
|
|
||||||
# create the app
|
# create the app
|
||||||
QCoreApplication.setApplicationName("Anki")
|
QCoreApplication.setApplicationName("Anki")
|
||||||
QGuiApplication.setDesktopFileName("anki.desktop")
|
QGuiApplication.setDesktopFileName("anki.desktop")
|
||||||
|
@ -359,6 +364,16 @@ def _run(argv=None, exec=True):
|
||||||
# we've signaled the primary instance, so we should close
|
# we've signaled the primary instance, so we should close
|
||||||
return
|
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
|
# disable icons on mac; this must be done before window created
|
||||||
if isMac:
|
if isMac:
|
||||||
app.setAttribute(Qt.AA_DontShowIconsInMenus)
|
app.setAttribute(Qt.AA_DontShowIconsInMenus)
|
||||||
|
@ -392,12 +407,14 @@ environment points to a valid, writable folder.""",
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if pmLoadResult.firstTime:
|
||||||
|
pm.setDefaultLang()
|
||||||
|
|
||||||
if pmLoadResult.loadError:
|
if pmLoadResult.loadError:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
None,
|
None,
|
||||||
"Preferences Corrupt",
|
"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.""",
|
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",
|
"David Bailey",
|
||||||
"Arman High",
|
"Arman High",
|
||||||
"Arthur Milchior",
|
"Arthur Milchior",
|
||||||
|
"Rai (Michael Pokorny)",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -97,22 +97,7 @@ class ProfileManager:
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def ensureBaseExists(self):
|
def ensureBaseExists(self):
|
||||||
try:
|
self._ensureExists(self.base)
|
||||||
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
|
|
||||||
|
|
||||||
# Folder migration
|
# Folder migration
|
||||||
######################################################################
|
######################################################################
|
||||||
|
@ -385,7 +370,6 @@ create table if not exists profiles
|
||||||
"insert or replace into profiles values ('_global', ?)",
|
"insert or replace into profiles values ('_global', ?)",
|
||||||
self._pickle(metaConf),
|
self._pickle(metaConf),
|
||||||
)
|
)
|
||||||
self._setDefaultLang()
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _ensureProfile(self):
|
def _ensureProfile(self):
|
||||||
|
@ -409,7 +393,7 @@ please see:
|
||||||
######################################################################
|
######################################################################
|
||||||
# On first run, allow the user to choose the default language
|
# On first run, allow the user to choose the default language
|
||||||
|
|
||||||
def _setDefaultLang(self):
|
def setDefaultLang(self):
|
||||||
# create dialog
|
# create dialog
|
||||||
class NoCloseDiag(QDialog):
|
class NoCloseDiag(QDialog):
|
||||||
def reject(self):
|
def reject(self):
|
||||||
|
@ -452,7 +436,7 @@ please see:
|
||||||
None, "Anki", en % name, QMessageBox.Yes | QMessageBox.No, QMessageBox.No
|
None, "Anki", en % name, QMessageBox.Yes | QMessageBox.No, QMessageBox.No
|
||||||
)
|
)
|
||||||
if r != QMessageBox.Yes:
|
if r != QMessageBox.Yes:
|
||||||
return self._setDefaultLang()
|
return self.setDefaultLang()
|
||||||
self.setLang(code)
|
self.setLang(code)
|
||||||
|
|
||||||
def setLang(self, code):
|
def setLang(self, code):
|
||||||
|
|
|
@ -416,11 +416,16 @@ body {{ zoom: {}; background: {}; {} }}
|
||||||
self.evalWithCallback("$(document.body).height()", self._onHeight)
|
self.evalWithCallback("$(document.body).height()", self._onHeight)
|
||||||
|
|
||||||
def _onHeight(self, qvar):
|
def _onHeight(self, qvar):
|
||||||
|
from aqt import mw
|
||||||
|
|
||||||
if qvar is None:
|
if qvar is None:
|
||||||
from aqt import mw
|
|
||||||
|
|
||||||
mw.progress.timer(1000, mw.reset, False)
|
mw.progress.timer(1000, mw.reset, False)
|
||||||
return
|
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)
|
self.setFixedHeight(height)
|
||||||
|
|
2
mypy.ini
2
mypy.ini
|
@ -34,3 +34,5 @@ ignore_missing_imports = True
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
[mypy-jsonschema.*]
|
[mypy-jsonschema.*]
|
||||||
ignore_missing_imports = True
|
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
|
mock
|
||||||
mypy==0.750
|
|
||||||
# fixme: when isort 5.0 is released, switch to pypy
|
# fixme: when isort 5.0 is released, switch to pypy
|
||||||
git+https://github.com/dae/isort#egg=isort
|
git+https://github.com/dae/isort#egg=isort
|
||||||
# fixme: when pylint supports isort 5.0, switch to pypy
|
# fixme: when pylint supports isort 5.0, switch to pypy
|
||||||
|
|
|
@ -8,3 +8,7 @@ jsonschema
|
||||||
psutil; sys_platform == "win32"
|
psutil; sys_platform == "win32"
|
||||||
distro; sys_platform != "win32" and sys_platform != "darwin"
|
distro; sys_platform != "win32" and sys_platform != "darwin"
|
||||||
typing
|
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
|
from anki import Collection as aopen
|
||||||
|
|
||||||
|
|
||||||
def assertException(exception, func):
|
def assertException(exception, func):
|
||||||
found = False
|
found = False
|
||||||
try:
|
try:
|
||||||
|
@ -25,6 +29,7 @@ def getEmptyCol():
|
||||||
col = aopen(nam)
|
col = aopen(nam)
|
||||||
return col
|
return col
|
||||||
|
|
||||||
|
|
||||||
getEmptyCol.master = ""
|
getEmptyCol.master = ""
|
||||||
|
|
||||||
# Fallback for when the DB needs options passed in.
|
# Fallback for when the DB needs options passed in.
|
||||||
|
@ -34,10 +39,12 @@ def getEmptyDeckWith(**kwargs):
|
||||||
os.unlink(nam)
|
os.unlink(nam)
|
||||||
return aopen(nam, **kwargs)
|
return aopen(nam, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def getUpgradeDeckPath(name="anki12.anki"):
|
def getUpgradeDeckPath(name="anki12.anki"):
|
||||||
src = os.path.join(testDir, "support", name)
|
src = os.path.join(testDir, "support", name)
|
||||||
(fd, dst) = tempfile.mkstemp(suffix=".anki2")
|
(fd, dst) = tempfile.mkstemp(suffix=".anki2")
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
return dst
|
return dst
|
||||||
|
|
||||||
|
|
||||||
testDir = os.path.dirname(__file__)
|
testDir = os.path.dirname(__file__)
|
||||||
|
|
|
@ -1,73 +1,57 @@
|
||||||
import os.path
|
import os.path
|
||||||
from nose.tools import assert_equals
|
|
||||||
from mock import MagicMock
|
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
from mock import MagicMock
|
||||||
|
from nose2.tools.such import helper
|
||||||
|
|
||||||
from aqt.addons import AddonManager
|
from aqt.addons import AddonManager
|
||||||
|
|
||||||
|
|
||||||
def test_readMinimalManifest():
|
def test_readMinimalManifest():
|
||||||
assertReadManifest(
|
assertReadManifest(
|
||||||
'{"package": "yes", "name": "no"}',
|
'{"package": "yes", "name": "no"}', {"package": "yes", "name": "no"}
|
||||||
{"package": "yes", "name": "no"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_readExtraKeys():
|
def test_readExtraKeys():
|
||||||
assertReadManifest(
|
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"]}
|
{"package": "a", "name": "b", "mod": 3, "conflicts": ["d", "e"]},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_invalidManifest():
|
def test_invalidManifest():
|
||||||
assertReadManifest(
|
assertReadManifest('{"one": 1}', {})
|
||||||
'{"one": 1}',
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_mustHaveName():
|
def test_mustHaveName():
|
||||||
assertReadManifest(
|
assertReadManifest('{"package": "something"}', {})
|
||||||
'{"package": "something"}',
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_mustHavePackage():
|
def test_mustHavePackage():
|
||||||
assertReadManifest(
|
assertReadManifest('{"name": "something"}', {})
|
||||||
'{"name": "something"}',
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalidJson():
|
def test_invalidJson():
|
||||||
assertReadManifest(
|
assertReadManifest("this is not a JSON dictionary", {})
|
||||||
'this is not a JSON dictionary',
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_missingManifest():
|
def test_missingManifest():
|
||||||
assertReadManifest(
|
assertReadManifest(
|
||||||
'{"package": "what", "name": "ever"}',
|
'{"package": "what", "name": "ever"}', {}, nameInZip="not-manifest.bin"
|
||||||
{},
|
|
||||||
nameInZip="not-manifest.bin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_ignoreExtraKeys():
|
def test_ignoreExtraKeys():
|
||||||
assertReadManifest(
|
assertReadManifest(
|
||||||
'{"package": "a", "name": "b", "game": "c"}',
|
'{"package": "a", "name": "b", "game": "c"}', {"package": "a", "name": "b"}
|
||||||
{"package": "a", "name": "b"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_conflictsMustBeStrings():
|
def test_conflictsMustBeStrings():
|
||||||
assertReadManifest(
|
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())
|
adm = AddonManager(MagicMock())
|
||||||
|
|
||||||
with ZipFile(zfn, "r") as zfile:
|
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
|
from tests.shared import getEmptyCol
|
||||||
|
|
||||||
|
|
||||||
def test_previewCards():
|
def test_previewCards():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = '1'
|
f["Front"] = "1"
|
||||||
f['Back'] = '2'
|
f["Back"] = "2"
|
||||||
# non-empty and active
|
# non-empty and active
|
||||||
cards = deck.previewCards(f, 0)
|
cards = deck.previewCards(f, 0)
|
||||||
assert len(cards) == 1
|
assert len(cards) == 1
|
||||||
|
@ -22,11 +23,12 @@ def test_previewCards():
|
||||||
# make sure we haven't accidentally added cards to the db
|
# make sure we haven't accidentally added cards to the db
|
||||||
assert deck.cardCount() == 1
|
assert deck.cardCount() == 1
|
||||||
|
|
||||||
|
|
||||||
def test_delete():
|
def test_delete():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = '1'
|
f["Front"] = "1"
|
||||||
f['Back'] = '2'
|
f["Back"] = "2"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
cid = f.cards()[0].id
|
cid = f.cards()[0].id
|
||||||
deck.reset()
|
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 cards") == 0
|
||||||
assert deck.db.scalar("select count() from graves") == 2
|
assert deck.db.scalar("select count() from graves") == 2
|
||||||
|
|
||||||
|
|
||||||
def test_misc():
|
def test_misc():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = '1'
|
f["Front"] = "1"
|
||||||
f['Back'] = '2'
|
f["Back"] = "2"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
id = d.models.current()['id']
|
id = d.models.current()["id"]
|
||||||
assert c.template()['ord'] == 0
|
assert c.template()["ord"] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_genrem():
|
def test_genrem():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = '1'
|
f["Front"] = "1"
|
||||||
f['Back'] = ''
|
f["Back"] = ""
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
assert len(f.cards()) == 1
|
assert len(f.cards()) == 1
|
||||||
m = d.models.current()
|
m = d.models.current()
|
||||||
mm = d.models
|
mm = d.models
|
||||||
# adding a new template should automatically create cards
|
# adding a new template should automatically create cards
|
||||||
t = mm.newTemplate("rev")
|
t = mm.newTemplate("rev")
|
||||||
t['qfmt'] = '{{Front}}'
|
t["qfmt"] = "{{Front}}"
|
||||||
t['afmt'] = ""
|
t["afmt"] = ""
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
mm.save(m, templates=True)
|
mm.save(m, templates=True)
|
||||||
assert len(f.cards()) == 2
|
assert len(f.cards()) == 2
|
||||||
# if the template is changed to remove cards, they'll be removed
|
# if the template is changed to remove cards, they'll be removed
|
||||||
t['qfmt'] = "{{Back}}"
|
t["qfmt"] = "{{Back}}"
|
||||||
mm.save(m, templates=True)
|
mm.save(m, templates=True)
|
||||||
d.remCards(d.emptyCids())
|
d.remCards(d.emptyCids())
|
||||||
assert len(f.cards()) == 1
|
assert len(f.cards()) == 1
|
||||||
# if we add to the note, a card should be automatically generated
|
# if we add to the note, a card should be automatically generated
|
||||||
f.load()
|
f.load()
|
||||||
f['Back'] = "1"
|
f["Back"] = "1"
|
||||||
f.flush()
|
f.flush()
|
||||||
assert len(f.cards()) == 2
|
assert len(f.cards()) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_gendeck():
|
def test_gendeck():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
cloze = d.models.byName("Cloze")
|
cloze = d.models.byName("Cloze")
|
||||||
d.models.setCurrent(cloze)
|
d.models.setCurrent(cloze)
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Text'] = '{{c1::one}}'
|
f["Text"] = "{{c1::one}}"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
assert d.cardCount() == 1
|
assert d.cardCount() == 1
|
||||||
assert f.cards()[0].did == 1
|
assert f.cards()[0].did == 1
|
||||||
# set the model to a new default deck
|
# set the model to a new default deck
|
||||||
newId = d.decks.id("new")
|
newId = d.decks.id("new")
|
||||||
cloze['did'] = newId
|
cloze["did"] = newId
|
||||||
d.models.save(cloze, updateReqs=False)
|
d.models.save(cloze, updateReqs=False)
|
||||||
# a newly generated card should share the first card's deck
|
# a newly generated card should share the first card's deck
|
||||||
f['Text'] += '{{c2::two}}'
|
f["Text"] += "{{c2::two}}"
|
||||||
f.flush()
|
f.flush()
|
||||||
assert f.cards()[1].did == 1
|
assert f.cards()[1].did == 1
|
||||||
# and same with multiple cards
|
# and same with multiple cards
|
||||||
f['Text'] += '{{c3::three}}'
|
f["Text"] += "{{c3::three}}"
|
||||||
f.flush()
|
f.flush()
|
||||||
assert f.cards()[2].did == 1
|
assert f.cards()[2].did == 1
|
||||||
# if one of the cards is in a different deck, it should revert to the
|
# 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 = f.cards()[1]
|
||||||
c.did = newId
|
c.did = newId
|
||||||
c.flush()
|
c.flush()
|
||||||
f['Text'] += '{{c4::four}}'
|
f["Text"] += "{{c4::four}}"
|
||||||
f.flush()
|
f.flush()
|
||||||
assert f.cards()[3].did == newId
|
assert f.cards()[3].did == newId
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import os, tempfile
|
import os
|
||||||
from tests.shared import assertException, getEmptyCol
|
import tempfile
|
||||||
from anki.stdmodels import addBasicModel, models
|
|
||||||
|
|
||||||
from anki import Collection as aopen
|
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():
|
def test_create_open():
|
||||||
global newPath, newMod
|
|
||||||
(fd, path) = tempfile.mkstemp(suffix=".anki2", prefix="test_attachNew")
|
(fd, path) = tempfile.mkstemp(suffix=".anki2", prefix="test_attachNew")
|
||||||
try:
|
try:
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
|
@ -30,27 +29,32 @@ def test_create_open():
|
||||||
deck.close()
|
deck.close()
|
||||||
|
|
||||||
# non-writeable dir
|
# non-writeable dir
|
||||||
assertException(Exception,
|
if isWin:
|
||||||
lambda: aopen("/attachroot.anki2"))
|
dir = "c:\root.anki2"
|
||||||
|
else:
|
||||||
|
dir = "/attachroot.anki2"
|
||||||
|
assertException(Exception, lambda: aopen(dir))
|
||||||
# reuse tmp file from before, test non-writeable file
|
# reuse tmp file from before, test non-writeable file
|
||||||
os.chmod(newPath, 0)
|
os.chmod(newPath, 0)
|
||||||
assertException(Exception,
|
assertException(Exception, lambda: aopen(newPath))
|
||||||
lambda: aopen(newPath))
|
|
||||||
os.chmod(newPath, 0o666)
|
os.chmod(newPath, 0o666)
|
||||||
os.unlink(newPath)
|
os.unlink(newPath)
|
||||||
|
|
||||||
|
|
||||||
def test_noteAddDelete():
|
def test_noteAddDelete():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
n = deck.addNote(f)
|
n = deck.addNote(f)
|
||||||
assert n == 1
|
assert n == 1
|
||||||
# test multiple cards - add another template
|
# 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 = mm.newTemplate("Reverse")
|
||||||
t['qfmt'] = "{{Back}}"
|
t["qfmt"] = "{{Back}}"
|
||||||
t['afmt'] = "{{Front}}"
|
t["afmt"] = "{{Front}}"
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
mm.save(m)
|
mm.save(m)
|
||||||
# the default save doesn't generate cards
|
# the default save doesn't generate cards
|
||||||
|
@ -61,7 +65,8 @@ def test_noteAddDelete():
|
||||||
assert deck.cardCount() == 2
|
assert deck.cardCount() == 2
|
||||||
# creating new notes should use both cards
|
# creating new notes should use both cards
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = "three"; f['Back'] = "four"
|
f["Front"] = "three"
|
||||||
|
f["Back"] = "four"
|
||||||
n = deck.addNote(f)
|
n = deck.addNote(f)
|
||||||
assert n == 2
|
assert n == 2
|
||||||
assert deck.cardCount() == 4
|
assert deck.cardCount() == 4
|
||||||
|
@ -72,36 +77,39 @@ def test_noteAddDelete():
|
||||||
assert not f.dupeOrEmpty()
|
assert not f.dupeOrEmpty()
|
||||||
# now let's make a duplicate
|
# now let's make a duplicate
|
||||||
f2 = deck.newNote()
|
f2 = deck.newNote()
|
||||||
f2['Front'] = "one"; f2['Back'] = ""
|
f2["Front"] = "one"
|
||||||
|
f2["Back"] = ""
|
||||||
assert f2.dupeOrEmpty()
|
assert f2.dupeOrEmpty()
|
||||||
# empty first field should not be permitted either
|
# empty first field should not be permitted either
|
||||||
f2['Front'] = " "
|
f2["Front"] = " "
|
||||||
assert f2.dupeOrEmpty()
|
assert f2.dupeOrEmpty()
|
||||||
|
|
||||||
|
|
||||||
def test_fieldChecksum():
|
def test_fieldChecksum():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = "new"; f['Back'] = "new2"
|
f["Front"] = "new"
|
||||||
|
f["Back"] = "new2"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
assert deck.db.scalar(
|
assert deck.db.scalar("select csum from notes") == int("c2a6b03f", 16)
|
||||||
"select csum from notes") == int("c2a6b03f", 16)
|
|
||||||
# changing the val should change the checksum
|
# changing the val should change the checksum
|
||||||
f['Front'] = "newx"
|
f["Front"] = "newx"
|
||||||
f.flush()
|
f.flush()
|
||||||
assert deck.db.scalar(
|
assert deck.db.scalar("select csum from notes") == int("302811ae", 16)
|
||||||
"select csum from notes") == int("302811ae", 16)
|
|
||||||
|
|
||||||
def test_addDelTags():
|
def test_addDelTags():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = "1"
|
f["Front"] = "1"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
f2 = deck.newNote()
|
f2 = deck.newNote()
|
||||||
f2['Front'] = "2"
|
f2["Front"] = "2"
|
||||||
deck.addNote(f2)
|
deck.addNote(f2)
|
||||||
# adding for a given id
|
# adding for a given id
|
||||||
deck.tags.bulkAdd([f.id], "foo")
|
deck.tags.bulkAdd([f.id], "foo")
|
||||||
f.load(); f2.load()
|
f.load()
|
||||||
|
f2.load()
|
||||||
assert "foo" in f.tags
|
assert "foo" in f.tags
|
||||||
assert "foo" not in f2.tags
|
assert "foo" not in f2.tags
|
||||||
# should be canonified
|
# should be canonified
|
||||||
|
@ -110,6 +118,7 @@ def test_addDelTags():
|
||||||
assert f.tags[0] == "aaa"
|
assert f.tags[0] == "aaa"
|
||||||
assert len(f.tags) == 2
|
assert len(f.tags) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_timestamps():
|
def test_timestamps():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
assert len(deck.models.models) == len(models)
|
assert len(deck.models.models) == len(models)
|
||||||
|
@ -117,23 +126,24 @@ def test_timestamps():
|
||||||
addBasicModel(deck)
|
addBasicModel(deck)
|
||||||
assert len(deck.models.models) == 100 + len(models)
|
assert len(deck.models.models) == 100 + len(models)
|
||||||
|
|
||||||
|
|
||||||
def test_furigana():
|
def test_furigana():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
mm = deck.models
|
mm = deck.models
|
||||||
m = mm.current()
|
m = mm.current()
|
||||||
# filter should work
|
# filter should work
|
||||||
m['tmpls'][0]['qfmt'] = '{{kana:Front}}'
|
m["tmpls"][0]["qfmt"] = "{{kana:Front}}"
|
||||||
mm.save(m)
|
mm.save(m)
|
||||||
n = deck.newNote()
|
n = deck.newNote()
|
||||||
n['Front'] = 'foo[abc]'
|
n["Front"] = "foo[abc]"
|
||||||
deck.addNote(n)
|
deck.addNote(n)
|
||||||
c = n.cards()[0]
|
c = n.cards()[0]
|
||||||
assert c.q().endswith("abc")
|
assert c.q().endswith("abc")
|
||||||
# and should avoid sound
|
# and should avoid sound
|
||||||
n['Front'] = 'foo[sound:abc.mp3]'
|
n["Front"] = "foo[sound:abc.mp3]"
|
||||||
n.flush()
|
n.flush()
|
||||||
assert "sound:" in c.q(reload=True)
|
assert "sound:" in c.q(reload=True)
|
||||||
# it shouldn't throw an error while people are editing
|
# it shouldn't throw an error while people are editing
|
||||||
m['tmpls'][0]['qfmt'] = '{{kana:}}'
|
m["tmpls"][0]["qfmt"] = "{{kana:}}"
|
||||||
mm.save(m)
|
mm.save(m)
|
||||||
c.q(reload=True)
|
c.q(reload=True)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
from anki.errors import DeckRenameError
|
from anki.errors import DeckRenameError
|
||||||
from tests.shared import assertException, getEmptyCol
|
from tests.shared import assertException, getEmptyCol
|
||||||
|
|
||||||
|
|
||||||
def test_basic():
|
def test_basic():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
# we start with a standard deck
|
# we start with a standard deck
|
||||||
|
@ -34,21 +35,22 @@ def test_basic():
|
||||||
# parents with a different case should be handled correctly
|
# parents with a different case should be handled correctly
|
||||||
deck.decks.id("ONE")
|
deck.decks.id("ONE")
|
||||||
m = deck.models.current()
|
m = deck.models.current()
|
||||||
m['did'] = deck.decks.id("one::two")
|
m["did"] = deck.decks.id("one::two")
|
||||||
deck.models.save(m, updateReqs=False)
|
deck.models.save(m, updateReqs=False)
|
||||||
n = deck.newNote()
|
n = deck.newNote()
|
||||||
n['Front'] = "abc"
|
n["Front"] = "abc"
|
||||||
deck.addNote(n)
|
deck.addNote(n)
|
||||||
# this will error if child and parent case don't match
|
# this will error if child and parent case don't match
|
||||||
deck.sched.deckDueList()
|
deck.sched.deckDueList()
|
||||||
|
|
||||||
|
|
||||||
def test_remove():
|
def test_remove():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
# create a new deck, and add a note/card to it
|
# create a new deck, and add a note/card to it
|
||||||
g1 = deck.decks.id("g1")
|
g1 = deck.decks.id("g1")
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = "1"
|
f["Front"] = "1"
|
||||||
f.model()['did'] = g1
|
f.model()["did"] = g1
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
assert c.did == g1
|
assert c.did == g1
|
||||||
|
@ -62,12 +64,14 @@ def test_remove():
|
||||||
assert deck.decks.name(c.did) == "[no deck]"
|
assert deck.decks.name(c.did) == "[no deck]"
|
||||||
# let's create another deck and explicitly set the card to it
|
# let's create another deck and explicitly set the card to it
|
||||||
g2 = deck.decks.id("g2")
|
g2 = deck.decks.id("g2")
|
||||||
c.did = g2; c.flush()
|
c.did = g2
|
||||||
|
c.flush()
|
||||||
# this time we'll delete the card/note too
|
# this time we'll delete the card/note too
|
||||||
deck.decks.rem(g2, cardsToo=True)
|
deck.decks.rem(g2, cardsToo=True)
|
||||||
assert deck.cardCount() == 0
|
assert deck.cardCount() == 0
|
||||||
assert deck.noteCount() == 0
|
assert deck.noteCount() == 0
|
||||||
|
|
||||||
|
|
||||||
def test_rename():
|
def test_rename():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
id = d.decks.id("hello::world")
|
id = d.decks.id("hello::world")
|
||||||
|
@ -80,8 +84,7 @@ def test_rename():
|
||||||
# create another deck
|
# create another deck
|
||||||
id = d.decks.id("tmp")
|
id = d.decks.id("tmp")
|
||||||
# we can't rename it if it conflicts
|
# we can't rename it if it conflicts
|
||||||
assertException(
|
assertException(Exception, lambda: d.decks.rename(d.decks.get(id), "foo"))
|
||||||
Exception, lambda: d.decks.rename(d.decks.get(id), "foo"))
|
|
||||||
# when renaming, the children should be renamed too
|
# when renaming, the children should be renamed too
|
||||||
d.decks.id("one::two::three")
|
d.decks.id("one::two::three")
|
||||||
id = d.decks.id("one")
|
id = d.decks.id("one")
|
||||||
|
@ -102,62 +105,66 @@ def test_rename():
|
||||||
assertException(DeckRenameError, lambda: d.decks.rename(child, "PARENT::child"))
|
assertException(DeckRenameError, lambda: d.decks.rename(child, "PARENT::child"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_renameForDragAndDrop():
|
def test_renameForDragAndDrop():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
|
|
||||||
def deckNames():
|
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')
|
languages_did = d.decks.id("Languages")
|
||||||
chinese_did = d.decks.id('Chinese')
|
chinese_did = d.decks.id("Chinese")
|
||||||
hsk_did = d.decks.id('Chinese::HSK')
|
hsk_did = d.decks.id("Chinese::HSK")
|
||||||
|
|
||||||
# Renaming also renames children
|
# Renaming also renames children
|
||||||
d.decks.renameForDragAndDrop(chinese_did, languages_did)
|
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
|
# Dragging a deck onto itself is a no-op
|
||||||
d.decks.renameForDragAndDrop(languages_did, languages_did)
|
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
|
# Dragging a deck onto its parent is a no-op
|
||||||
d.decks.renameForDragAndDrop(hsk_did, chinese_did)
|
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
|
# Dragging a deck onto a descendant is a no-op
|
||||||
d.decks.renameForDragAndDrop(languages_did, hsk_did)
|
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
|
# Can drag a grandchild onto its grandparent. It becomes a child
|
||||||
d.decks.renameForDragAndDrop(hsk_did, languages_did)
|
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
|
# Can drag a deck onto its sibling
|
||||||
d.decks.renameForDragAndDrop(hsk_did, chinese_did)
|
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
|
# Can drag a deck back to the top level
|
||||||
d.decks.renameForDragAndDrop(chinese_did, None)
|
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
|
# Dragging a top level deck to the top level is a no-op
|
||||||
d.decks.renameForDragAndDrop(chinese_did, None)
|
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
|
# can't drack a deck where sibling have same name
|
||||||
new_hsk_did = d.decks.id("HSK")
|
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)
|
d.decks.rem(new_hsk_did)
|
||||||
|
|
||||||
# can't drack a deck where sibling have same name different case
|
# can't drack a deck where sibling have same name different case
|
||||||
new_hsk_did = d.decks.id("hsk")
|
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)
|
d.decks.rem(new_hsk_did)
|
||||||
|
|
||||||
# '' is a convenient alias for the top level DID
|
# '' is a convenient alias for the top level DID
|
||||||
d.decks.renameForDragAndDrop(hsk_did, '')
|
d.decks.renameForDragAndDrop(hsk_did, "")
|
||||||
assert deckNames() == [ 'Chinese', 'HSK', 'Languages' ]
|
assert deckNames() == ["Chinese", "HSK", "Languages"]
|
||||||
|
|
||||||
|
|
||||||
def test_check():
|
def test_check():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
|
|
|
@ -1,37 +1,48 @@
|
||||||
# coding: utf-8
|
# 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 import Collection as aopen
|
||||||
from anki.exporting import *
|
from anki.exporting import *
|
||||||
from anki.importing import Anki2Importer
|
from anki.importing import Anki2Importer
|
||||||
|
|
||||||
from .shared import getEmptyCol
|
from .shared import getEmptyCol
|
||||||
|
|
||||||
deck = None
|
deck = None
|
||||||
ds = None
|
ds = None
|
||||||
testDir = os.path.dirname(__file__)
|
testDir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
|
||||||
def setup1():
|
def setup1():
|
||||||
global deck
|
global deck
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
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)
|
deck.addNote(f)
|
||||||
# with a different deck
|
# with a different deck
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = "baz"; f['Back'] = "qux"
|
f["Front"] = "baz"
|
||||||
f.model()['did'] = deck.decks.id("new deck")
|
f["Back"] = "qux"
|
||||||
|
f.model()["did"] = deck.decks.id("new deck")
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
|
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
@nose.with_setup(setup1)
|
|
||||||
|
@with_setup(setup1)
|
||||||
def test_export_anki():
|
def test_export_anki():
|
||||||
# create a new deck with its own conf to test conf copying
|
# create a new deck with its own conf to test conf copying
|
||||||
did = deck.decks.id("test")
|
did = deck.decks.id("test")
|
||||||
dobj = deck.decks.get(did)
|
dobj = deck.decks.get(did)
|
||||||
confId = deck.decks.confId("newconf")
|
confId = deck.decks.confId("newconf")
|
||||||
conf = deck.decks.getConf(confId)
|
conf = deck.decks.getConf(confId)
|
||||||
conf['new']['perDay'] = 5
|
conf["new"]["perDay"] = 5
|
||||||
deck.decks.save(conf)
|
deck.decks.save(conf)
|
||||||
deck.decks.setConf(dobj, confId)
|
deck.decks.setConf(dobj, confId)
|
||||||
# export
|
# export
|
||||||
|
@ -43,7 +54,7 @@ def test_export_anki():
|
||||||
e.exportInto(newname)
|
e.exportInto(newname)
|
||||||
# exporting should not have changed conf for original deck
|
# exporting should not have changed conf for original deck
|
||||||
conf = deck.decks.confForDid(did)
|
conf = deck.decks.confForDid(did)
|
||||||
assert conf['id'] != 1
|
assert conf["id"] != 1
|
||||||
# connect to new deck
|
# connect to new deck
|
||||||
d2 = aopen(newname)
|
d2 = aopen(newname)
|
||||||
assert d2.cardCount() == 2
|
assert d2.cardCount() == 2
|
||||||
|
@ -51,10 +62,10 @@ def test_export_anki():
|
||||||
did = d2.decks.id("test", create=False)
|
did = d2.decks.id("test", create=False)
|
||||||
assert did
|
assert did
|
||||||
conf2 = d2.decks.confForDid(did)
|
conf2 = d2.decks.confForDid(did)
|
||||||
assert conf2['new']['perDay'] == 20
|
assert conf2["new"]["perDay"] == 20
|
||||||
dobj = d2.decks.get(did)
|
dobj = d2.decks.get(did)
|
||||||
# conf should be 1
|
# conf should be 1
|
||||||
assert dobj['conf'] == 1
|
assert dobj["conf"] == 1
|
||||||
# try again, limited to a deck
|
# try again, limited to a deck
|
||||||
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
|
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
|
||||||
newname = str(newname)
|
newname = str(newname)
|
||||||
|
@ -65,13 +76,14 @@ def test_export_anki():
|
||||||
d2 = aopen(newname)
|
d2 = aopen(newname)
|
||||||
assert d2.cardCount() == 1
|
assert d2.cardCount() == 1
|
||||||
|
|
||||||
@nose.with_setup(setup1)
|
|
||||||
|
@with_setup(setup1)
|
||||||
def test_export_ankipkg():
|
def test_export_ankipkg():
|
||||||
# add a test file to the media folder
|
# add a test file to the media folder
|
||||||
with open(os.path.join(deck.media.dir(), "今日.mp3"), "w") as f:
|
with open(os.path.join(deck.media.dir(), "今日.mp3"), "w") as f:
|
||||||
f.write("test")
|
f.write("test")
|
||||||
n = deck.newNote()
|
n = deck.newNote()
|
||||||
n['Front'] = '[sound:今日.mp3]'
|
n["Front"] = "[sound:今日.mp3]"
|
||||||
deck.addNote(n)
|
deck.addNote(n)
|
||||||
e = AnkiPackageExporter(deck)
|
e = AnkiPackageExporter(deck)
|
||||||
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".apkg")
|
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".apkg")
|
||||||
|
@ -80,13 +92,14 @@ def test_export_ankipkg():
|
||||||
os.unlink(newname)
|
os.unlink(newname)
|
||||||
e.exportInto(newname)
|
e.exportInto(newname)
|
||||||
|
|
||||||
@nose.with_setup(setup1)
|
|
||||||
|
@with_setup(setup1)
|
||||||
def test_export_anki_due():
|
def test_export_anki_due():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = "foo"
|
f["Front"] = "foo"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
deck.crt -= 86400*10
|
deck.crt -= 86400 * 10
|
||||||
deck.sched.reset()
|
deck.sched.reset()
|
||||||
c = deck.sched.getCard()
|
c = deck.sched.getCard()
|
||||||
deck.sched.answerCard(c, 3)
|
deck.sched.answerCard(c, 3)
|
||||||
|
@ -112,7 +125,8 @@ def test_export_anki_due():
|
||||||
deck2.sched.reset()
|
deck2.sched.reset()
|
||||||
assert c.due - deck2.sched.today == 1
|
assert c.due - deck2.sched.today == 1
|
||||||
|
|
||||||
# @nose.with_setup(setup1)
|
|
||||||
|
# @with_setup(setup1)
|
||||||
# def test_export_textcard():
|
# def test_export_textcard():
|
||||||
# e = TextCardExporter(deck)
|
# e = TextCardExporter(deck)
|
||||||
# f = unicode(tempfile.mkstemp(prefix="ankitest")[1])
|
# f = unicode(tempfile.mkstemp(prefix="ankitest")[1])
|
||||||
|
@ -121,7 +135,8 @@ def test_export_anki_due():
|
||||||
# e.includeTags = True
|
# e.includeTags = True
|
||||||
# e.exportInto(f)
|
# e.exportInto(f)
|
||||||
|
|
||||||
@nose.with_setup(setup1)
|
|
||||||
|
@with_setup(setup1)
|
||||||
def test_export_textnote():
|
def test_export_textnote():
|
||||||
e = TextNoteExporter(deck)
|
e = TextNoteExporter(deck)
|
||||||
fd, f = tempfile.mkstemp(prefix="ankitest")
|
fd, f = tempfile.mkstemp(prefix="ankitest")
|
||||||
|
@ -135,5 +150,6 @@ def test_export_textnote():
|
||||||
e.exportInto(f)
|
e.exportInto(f)
|
||||||
assert open(f).readline() == "foo\tbar\n"
|
assert open(f).readline() == "foo\tbar\n"
|
||||||
|
|
||||||
|
|
||||||
def test_exporters():
|
def test_exporters():
|
||||||
assert "*.apkg" in str(exporters())
|
assert "*.apkg" in str(exporters())
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from nose.tools import assert_raises
|
from nose2.tools.such import helper
|
||||||
|
|
||||||
from anki.find import Finder
|
from anki.find import Finder
|
||||||
from tests.shared import getEmptyCol
|
from tests.shared import getEmptyCol
|
||||||
|
|
||||||
|
|
||||||
def test_parse():
|
def test_parse():
|
||||||
f = Finder(None)
|
f = Finder(None)
|
||||||
assert f._tokenize("hello world") == ["hello", "world"]
|
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 - two") == ["one", "-", "two"]
|
assert f._tokenize("one - two") == ["one", "-", "two"]
|
||||||
assert f._tokenize("one or -two") == ["one", "or", "-", "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('"hello world"') == ["hello world"]
|
||||||
assert f._tokenize("one (two or ( three or four))") == [
|
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("embedded'string") == ["embedded'string"]
|
||||||
assert f._tokenize("deck:'two words'") == ["deck:two words"]
|
assert f._tokenize("deck:'two words'") == ["deck:two words"]
|
||||||
|
|
||||||
|
|
||||||
def test_findCards():
|
def test_findCards():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = 'dog'
|
f["Front"] = "dog"
|
||||||
f['Back'] = 'cat'
|
f["Back"] = "cat"
|
||||||
f.tags.append("monkey animal_1 * %")
|
f.tags.append("monkey animal_1 * %")
|
||||||
f1id = f.id
|
f1id = f.id
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
firstCardId = f.cards()[0].id
|
firstCardId = f.cards()[0].id
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = 'goats are fun'
|
f["Front"] = "goats are fun"
|
||||||
f['Back'] = 'sheep'
|
f["Back"] = "sheep"
|
||||||
f.tags.append("sheep goat horse animal11")
|
f.tags.append("sheep goat horse animal11")
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
f2id = f.id
|
f2id = f.id
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = 'cat'
|
f["Front"] = "cat"
|
||||||
f['Back'] = 'sheep'
|
f["Back"] = "sheep"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
catCard = f.cards()[0]
|
catCard = f.cards()[0]
|
||||||
m = deck.models.current(); mm = deck.models
|
m = deck.models.current()
|
||||||
|
mm = deck.models
|
||||||
t = mm.newTemplate("Reverse")
|
t = mm.newTemplate("Reverse")
|
||||||
t['qfmt'] = "{{Back}}"
|
t["qfmt"] = "{{Back}}"
|
||||||
t['afmt'] = "{{Front}}"
|
t["afmt"] = "{{Front}}"
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
mm.save(m)
|
mm.save(m)
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = 'test'
|
f["Front"] = "test"
|
||||||
f['Back'] = 'foo bar'
|
f["Back"] = "foo bar"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
latestCardIds = [c.id for c in f.cards()]
|
latestCardIds = [c.id for c in f.cards()]
|
||||||
# tag searches
|
# tag searches
|
||||||
|
@ -66,9 +78,7 @@ def test_findCards():
|
||||||
assert len(deck.findCards("tag:sheep -tag:monkey")) == 1
|
assert len(deck.findCards("tag:sheep -tag:monkey")) == 1
|
||||||
assert len(deck.findCards("-tag:sheep")) == 4
|
assert len(deck.findCards("-tag:sheep")) == 4
|
||||||
deck.tags.bulkAdd(deck.db.list("select id from notes"), "foo bar")
|
deck.tags.bulkAdd(deck.db.list("select id from notes"), "foo bar")
|
||||||
assert (len(deck.findCards("tag:foo")) ==
|
assert len(deck.findCards("tag:foo")) == len(deck.findCards("tag:bar")) == 5
|
||||||
len(deck.findCards("tag:bar")) ==
|
|
||||||
5)
|
|
||||||
deck.tags.bulkRem(deck.db.list("select id from notes"), "foo")
|
deck.tags.bulkRem(deck.db.list("select id from notes"), "foo")
|
||||||
assert len(deck.findCards("tag:foo")) == 0
|
assert len(deck.findCards("tag:foo")) == 0
|
||||||
assert len(deck.findCards("tag:bar")) == 5
|
assert len(deck.findCards("tag:bar")) == 5
|
||||||
|
@ -86,7 +96,8 @@ def test_findCards():
|
||||||
c.flush()
|
c.flush()
|
||||||
assert deck.findCards("is:review") == [c.id]
|
assert deck.findCards("is:review") == [c.id]
|
||||||
assert deck.findCards("is:due") == []
|
assert deck.findCards("is:due") == []
|
||||||
c.due = 0; c.queue = 2
|
c.due = 0
|
||||||
|
c.queue = 2
|
||||||
c.flush()
|
c.flush()
|
||||||
assert deck.findCards("is:due") == [c.id]
|
assert deck.findCards("is:due") == [c.id]
|
||||||
assert len(deck.findCards("-is:due")) == 4
|
assert len(deck.findCards("-is:due")) == 4
|
||||||
|
@ -97,10 +108,10 @@ def test_findCards():
|
||||||
assert deck.findCards("is:suspended") == [c.id]
|
assert deck.findCards("is:suspended") == [c.id]
|
||||||
# nids
|
# nids
|
||||||
assert deck.findCards("nid:54321") == []
|
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
|
assert len(deck.findCards("nid:%d,%d" % (f1id, f2id))) == 2
|
||||||
# templates
|
# templates
|
||||||
with assert_raises(Exception):
|
with helper.assertRaises(Exception):
|
||||||
deck.findCards("card:foo")
|
deck.findCards("card:foo")
|
||||||
assert len(deck.findCards("'card:card 1'")) == 4
|
assert len(deck.findCards("'card:card 1'")) == 4
|
||||||
assert len(deck.findCards("card:reverse")) == 1
|
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:do")) == 0
|
||||||
assert len(deck.findCards("front:*")) == 5
|
assert len(deck.findCards("front:*")) == 5
|
||||||
# ordering
|
# ordering
|
||||||
deck.conf['sortType'] = "noteCrt"
|
deck.conf["sortType"] = "noteCrt"
|
||||||
assert deck.findCards("front:*", order=True)[-1] in latestCardIds
|
assert deck.findCards("front:*", order=True)[-1] in latestCardIds
|
||||||
assert deck.findCards("", 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)[0] == catCard.id
|
||||||
assert deck.findCards("", order=True)[-1] in latestCardIds
|
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)[-1] in latestCardIds
|
||||||
assert deck.findCards("", order=True)[0] == firstCardId
|
assert deck.findCards("", order=True)[0] == firstCardId
|
||||||
deck.conf['sortBackwards'] = True
|
deck.conf["sortBackwards"] = True
|
||||||
assert deck.findCards("", order=True)[0] in latestCardIds
|
assert deck.findCards("", order=True)[0] in latestCardIds
|
||||||
# model
|
# model
|
||||||
assert len(deck.findCards("note:basic")) == 5
|
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:foo")) == 5
|
||||||
assert len(deck.findCards("deck:def*")) == 5
|
assert len(deck.findCards("deck:def*")) == 5
|
||||||
assert len(deck.findCards("deck:*EFAULT")) == 5
|
assert len(deck.findCards("deck:*EFAULT")) == 5
|
||||||
with assert_raises(Exception):
|
with helper.assertRaises(Exception):
|
||||||
deck.findCards("deck:*cefault")
|
deck.findCards("deck:*cefault")
|
||||||
# full search
|
# full search
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = 'hello<b>world</b>'
|
f["Front"] = "hello<b>world</b>"
|
||||||
f['Back'] = 'abc'
|
f["Back"] = "abc"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
# as it's the sort field, it matches
|
# as it's the sort field, it matches
|
||||||
assert len(deck.findCards("helloworld")) == 2
|
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
|
# 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()
|
f.flush()
|
||||||
assert len(deck.findCards("helloworld")) == 0
|
assert len(deck.findCards("helloworld")) == 0
|
||||||
#assert len(deck.findCards("helloworld", full=True)) == 2
|
# assert len(deck.findCards("helloworld", full=True)) == 2
|
||||||
#assert len(deck.findCards("back:helloworld", full=True)) == 2
|
# assert len(deck.findCards("back:helloworld", full=True)) == 2
|
||||||
# searching for an invalid special tag should not error
|
# searching for an invalid special tag should not error
|
||||||
with assert_raises(Exception):
|
with helper.assertRaises(Exception):
|
||||||
len(deck.findCards("is:invalid"))
|
len(deck.findCards("is:invalid"))
|
||||||
# should be able to limit to parent deck, no children
|
# should be able to limit to parent deck, no children
|
||||||
id = deck.db.scalar("select id from cards limit 1")
|
id = deck.db.scalar("select id from cards limit 1")
|
||||||
deck.db.execute("update cards set did = ? where id = ?",
|
deck.db.execute(
|
||||||
deck.decks.id("Default::Child"), id)
|
"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")) == 7
|
||||||
assert len(deck.findCards("deck:default::child")) == 1
|
assert len(deck.findCards("deck:default::child")) == 1
|
||||||
assert len(deck.findCards("deck:default -deck:default::*")) == 6
|
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")
|
id = deck.db.scalar("select id from cards limit 1")
|
||||||
deck.db.execute(
|
deck.db.execute(
|
||||||
"update cards set queue=2, ivl=10, reps=20, due=30, factor=2200 "
|
"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
|
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
|
# empty field
|
||||||
assert len(deck.findCards("front:")) == 0
|
assert len(deck.findCards("front:")) == 0
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = ''
|
f["Front"] = ""
|
||||||
f['Back'] = 'abc2'
|
f["Back"] = "abc2"
|
||||||
assert deck.addNote(f) == 1
|
assert deck.addNote(f) == 1
|
||||||
assert len(deck.findCards("front:")) == 1
|
assert len(deck.findCards("front:")) == 1
|
||||||
# OR searches and nesting
|
# OR searches and nesting
|
||||||
|
@ -220,60 +234,67 @@ def test_findCards():
|
||||||
assert len(deck.findCards("(()")) == 0
|
assert len(deck.findCards("(()")) == 0
|
||||||
# added
|
# added
|
||||||
assert len(deck.findCards("added:0")) == 0
|
assert len(deck.findCards("added:0")) == 0
|
||||||
deck.db.execute("update cards set id = id - 86400*1000 where id = ?",
|
deck.db.execute("update cards set id = id - 86400*1000 where id = ?", id)
|
||||||
id)
|
|
||||||
assert len(deck.findCards("added:1")) == deck.cardCount() - 1
|
assert len(deck.findCards("added:1")) == deck.cardCount() - 1
|
||||||
assert len(deck.findCards("added:2")) == deck.cardCount()
|
assert len(deck.findCards("added:2")) == deck.cardCount()
|
||||||
# flag
|
# flag
|
||||||
with assert_raises(Exception):
|
with helper.assertRaises(Exception):
|
||||||
deck.findCards("flag:01")
|
deck.findCards("flag:01")
|
||||||
with assert_raises(Exception):
|
with helper.assertRaises(Exception):
|
||||||
deck.findCards("flag:12")
|
deck.findCards("flag:12")
|
||||||
|
|
||||||
|
|
||||||
def test_findReplace():
|
def test_findReplace():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = 'foo'
|
f["Front"] = "foo"
|
||||||
f['Back'] = 'bar'
|
f["Back"] = "bar"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
f2 = deck.newNote()
|
f2 = deck.newNote()
|
||||||
f2['Front'] = 'baz'
|
f2["Front"] = "baz"
|
||||||
f2['Back'] = 'foo'
|
f2["Back"] = "foo"
|
||||||
deck.addNote(f2)
|
deck.addNote(f2)
|
||||||
nids = [f.id, f2.id]
|
nids = [f.id, f2.id]
|
||||||
# should do nothing
|
# should do nothing
|
||||||
assert deck.findReplace(nids, "abc", "123") == 0
|
assert deck.findReplace(nids, "abc", "123") == 0
|
||||||
# global replace
|
# global replace
|
||||||
assert deck.findReplace(nids, "foo", "qux") == 2
|
assert deck.findReplace(nids, "foo", "qux") == 2
|
||||||
f.load(); assert f['Front'] == "qux"
|
f.load()
|
||||||
f2.load(); assert f2['Back'] == "qux"
|
assert f["Front"] == "qux"
|
||||||
|
f2.load()
|
||||||
|
assert f2["Back"] == "qux"
|
||||||
# single field replace
|
# single field replace
|
||||||
assert deck.findReplace(nids, "qux", "foo", field="Front") == 1
|
assert deck.findReplace(nids, "qux", "foo", field="Front") == 1
|
||||||
f.load(); assert f['Front'] == "foo"
|
f.load()
|
||||||
f2.load(); assert f2['Back'] == "qux"
|
assert f["Front"] == "foo"
|
||||||
|
f2.load()
|
||||||
|
assert f2["Back"] == "qux"
|
||||||
# regex replace
|
# regex replace
|
||||||
assert deck.findReplace(nids, "B.r", "reg") == 0
|
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
|
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():
|
def test_findDupes():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = 'foo'
|
f["Front"] = "foo"
|
||||||
f['Back'] = 'bar'
|
f["Back"] = "bar"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
f2 = deck.newNote()
|
f2 = deck.newNote()
|
||||||
f2['Front'] = 'baz'
|
f2["Front"] = "baz"
|
||||||
f2['Back'] = 'bar'
|
f2["Back"] = "bar"
|
||||||
deck.addNote(f2)
|
deck.addNote(f2)
|
||||||
f3 = deck.newNote()
|
f3 = deck.newNote()
|
||||||
f3['Front'] = 'quux'
|
f3["Front"] = "quux"
|
||||||
f3['Back'] = 'bar'
|
f3["Back"] = "bar"
|
||||||
deck.addNote(f3)
|
deck.addNote(f3)
|
||||||
f4 = deck.newNote()
|
f4 = deck.newNote()
|
||||||
f4['Front'] = 'quuux'
|
f4["Front"] = "quuux"
|
||||||
f4['Back'] = 'nope'
|
f4["Back"] = "nope"
|
||||||
deck.addNote(f4)
|
deck.addNote(f4)
|
||||||
r = deck.findDupes("Back")
|
r = deck.findDupes("Back")
|
||||||
assert r[0][0] == "bar"
|
assert r[0][0] == "bar"
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
from tests.shared import assertException, getEmptyCol
|
from tests.shared import assertException, getEmptyCol
|
||||||
|
|
||||||
|
|
||||||
def test_flags():
|
def test_flags():
|
||||||
col = getEmptyCol()
|
col = getEmptyCol()
|
||||||
n = col.newNote()
|
n = col.newNote()
|
||||||
n['Front'] = "one"; n['Back'] = "two"
|
n["Front"] = "one"
|
||||||
|
n["Back"] = "two"
|
||||||
cnt = col.addNote(n)
|
cnt = col.addNote(n)
|
||||||
c = n.cards()[0]
|
c = n.cards()[0]
|
||||||
# make sure higher bits are preserved
|
# make sure higher bits are preserved
|
||||||
|
|
|
@ -1,22 +1,29 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from tests.shared import getUpgradeDeckPath, getEmptyCol
|
|
||||||
|
from anki.importing import (
|
||||||
|
Anki2Importer,
|
||||||
|
AnkiPackageImporter,
|
||||||
|
MnemosyneImporter,
|
||||||
|
SupermemoXmlImporter,
|
||||||
|
TextImporter,
|
||||||
|
)
|
||||||
from anki.utils import ids2str
|
from anki.utils import ids2str
|
||||||
from anki.importing import Anki2Importer, TextImporter, \
|
from tests.shared import getEmptyCol, getUpgradeDeckPath
|
||||||
SupermemoXmlImporter, MnemosyneImporter, AnkiPackageImporter
|
|
||||||
|
|
||||||
testDir = os.path.dirname(__file__)
|
testDir = os.path.dirname(__file__)
|
||||||
|
|
||||||
srcNotes=None
|
srcNotes = None
|
||||||
srcCards=None
|
srcCards = None
|
||||||
|
|
||||||
|
|
||||||
def test_anki2_mediadupes():
|
def test_anki2_mediadupes():
|
||||||
tmp = getEmptyCol()
|
tmp = getEmptyCol()
|
||||||
# add a note that references a sound
|
# add a note that references a sound
|
||||||
n = tmp.newNote()
|
n = tmp.newNote()
|
||||||
n['Front'] = "[sound:foo.mp3]"
|
n["Front"] = "[sound:foo.mp3]"
|
||||||
mid = n.model()['id']
|
mid = n.model()["id"]
|
||||||
tmp.addNote(n)
|
tmp.addNote(n)
|
||||||
# add that sound to media folder
|
# add that sound to media folder
|
||||||
with open(os.path.join(tmp.media.dir(), "foo.mp3"), "w") as f:
|
with open(os.path.join(tmp.media.dir(), "foo.mp3"), "w") as f:
|
||||||
|
@ -41,8 +48,7 @@ def test_anki2_mediadupes():
|
||||||
f.write("bar")
|
f.write("bar")
|
||||||
imp = Anki2Importer(empty, tmp.path)
|
imp = Anki2Importer(empty, tmp.path)
|
||||||
imp.run()
|
imp.run()
|
||||||
assert sorted(os.listdir(empty.media.dir())) == [
|
assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
|
||||||
"foo.mp3", "foo_%s.mp3" % mid]
|
|
||||||
n = empty.getNote(empty.db.scalar("select id from notes"))
|
n = empty.getNote(empty.db.scalar("select id from notes"))
|
||||||
assert "_" in n.fields[0]
|
assert "_" in n.fields[0]
|
||||||
# if the localized media file already exists, we rewrite the note and
|
# if the localized media file already exists, we rewrite the note and
|
||||||
|
@ -52,25 +58,24 @@ def test_anki2_mediadupes():
|
||||||
f.write("bar")
|
f.write("bar")
|
||||||
imp = Anki2Importer(empty, tmp.path)
|
imp = Anki2Importer(empty, tmp.path)
|
||||||
imp.run()
|
imp.run()
|
||||||
assert sorted(os.listdir(empty.media.dir())) == [
|
assert sorted(os.listdir(empty.media.dir())) == ["foo.mp3", "foo_%s.mp3" % mid]
|
||||||
"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"))
|
n = empty.getNote(empty.db.scalar("select id from notes"))
|
||||||
assert "_" in n.fields[0]
|
assert "_" in n.fields[0]
|
||||||
|
|
||||||
|
|
||||||
def test_apkg():
|
def test_apkg():
|
||||||
tmp = getEmptyCol()
|
tmp = getEmptyCol()
|
||||||
apkg = str(os.path.join(testDir, "support/media.apkg"))
|
apkg = str(os.path.join(testDir, "support/media.apkg"))
|
||||||
imp = AnkiPackageImporter(tmp, apkg)
|
imp = AnkiPackageImporter(tmp, apkg)
|
||||||
assert os.listdir(tmp.media.dir()) == []
|
assert os.listdir(tmp.media.dir()) == []
|
||||||
imp.run()
|
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
|
# importing again should be idempotent in terms of media
|
||||||
tmp.remCards(tmp.db.list("select id from cards"))
|
tmp.remCards(tmp.db.list("select id from cards"))
|
||||||
imp = AnkiPackageImporter(tmp, apkg)
|
imp = AnkiPackageImporter(tmp, apkg)
|
||||||
imp.run()
|
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
|
# but if the local file has different data, it will rename
|
||||||
tmp.remCards(tmp.db.list("select id from cards"))
|
tmp.remCards(tmp.db.list("select id from cards"))
|
||||||
with open(os.path.join(tmp.media.dir(), "foo.wav"), "w") as f:
|
with open(os.path.join(tmp.media.dir(), "foo.wav"), "w") as f:
|
||||||
|
@ -79,6 +84,7 @@ def test_apkg():
|
||||||
imp.run()
|
imp.run()
|
||||||
assert len(os.listdir(tmp.media.dir())) == 2
|
assert len(os.listdir(tmp.media.dir())) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_anki2_diffmodel_templates():
|
def test_anki2_diffmodel_templates():
|
||||||
# different from the above as this one tests only the template text being
|
# different from the above as this one tests only the template text being
|
||||||
# changed, not the number of cards/fields
|
# changed, not the number of cards/fields
|
||||||
|
@ -94,11 +100,12 @@ def test_anki2_diffmodel_templates():
|
||||||
imp.dupeOnSchemaChange = True
|
imp.dupeOnSchemaChange = True
|
||||||
imp.run()
|
imp.run()
|
||||||
# collection should contain the note we imported
|
# 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
|
# 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()
|
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():
|
def test_anki2_updates():
|
||||||
# create a new empty deck
|
# create a new empty deck
|
||||||
|
@ -127,6 +134,7 @@ def test_anki2_updates():
|
||||||
assert dst.noteCount() == 1
|
assert dst.noteCount() == 1
|
||||||
assert dst.db.scalar("select flds from notes").startswith("goodbye")
|
assert dst.db.scalar("select flds from notes").startswith("goodbye")
|
||||||
|
|
||||||
|
|
||||||
def test_csv():
|
def test_csv():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
file = str(os.path.join(testDir, "support/text-2fields.txt"))
|
file = str(os.path.join(testDir, "support/text-2fields.txt"))
|
||||||
|
@ -147,7 +155,7 @@ def test_csv():
|
||||||
n.flush()
|
n.flush()
|
||||||
i.run()
|
i.run()
|
||||||
n.load()
|
n.load()
|
||||||
assert n.tags == ['test']
|
assert n.tags == ["test"]
|
||||||
# if add-only mode, count will be 0
|
# if add-only mode, count will be 0
|
||||||
i.importMode = 1
|
i.importMode = 1
|
||||||
i.run()
|
i.run()
|
||||||
|
@ -161,6 +169,7 @@ def test_csv():
|
||||||
assert deck.cardCount() == 11
|
assert deck.cardCount() == 11
|
||||||
deck.close()
|
deck.close()
|
||||||
|
|
||||||
|
|
||||||
def test_csv2():
|
def test_csv2():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
mm = deck.models
|
mm = deck.models
|
||||||
|
@ -169,9 +178,9 @@ def test_csv2():
|
||||||
mm.addField(m, f)
|
mm.addField(m, f)
|
||||||
mm.save(m)
|
mm.save(m)
|
||||||
n = deck.newNote()
|
n = deck.newNote()
|
||||||
n['Front'] = "1"
|
n["Front"] = "1"
|
||||||
n['Back'] = "2"
|
n["Back"] = "2"
|
||||||
n['Three'] = "3"
|
n["Three"] = "3"
|
||||||
deck.addNote(n)
|
deck.addNote(n)
|
||||||
# an update with unmapped fields should not clobber those fields
|
# an update with unmapped fields should not clobber those fields
|
||||||
file = str(os.path.join(testDir, "support/text-update.txt"))
|
file = str(os.path.join(testDir, "support/text-update.txt"))
|
||||||
|
@ -179,16 +188,17 @@ def test_csv2():
|
||||||
i.initMapping()
|
i.initMapping()
|
||||||
i.run()
|
i.run()
|
||||||
n.load()
|
n.load()
|
||||||
assert n['Front'] == "1"
|
assert n["Front"] == "1"
|
||||||
assert n['Back'] == "x"
|
assert n["Back"] == "x"
|
||||||
assert n['Three'] == "3"
|
assert n["Three"] == "3"
|
||||||
deck.close()
|
deck.close()
|
||||||
|
|
||||||
|
|
||||||
def test_supermemo_xml_01_unicode():
|
def test_supermemo_xml_01_unicode():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
file = str(os.path.join(testDir, "support/supermemo1.xml"))
|
file = str(os.path.join(testDir, "support/supermemo1.xml"))
|
||||||
i = SupermemoXmlImporter(deck, file)
|
i = SupermemoXmlImporter(deck, file)
|
||||||
#i.META.logToStdOutput = True
|
# i.META.logToStdOutput = True
|
||||||
i.run()
|
i.run()
|
||||||
assert i.total == 1
|
assert i.total == 1
|
||||||
cid = deck.db.scalar("select id from cards")
|
cid = deck.db.scalar("select id from cards")
|
||||||
|
@ -198,6 +208,7 @@ def test_supermemo_xml_01_unicode():
|
||||||
assert c.reps == 7
|
assert c.reps == 7
|
||||||
deck.close()
|
deck.close()
|
||||||
|
|
||||||
|
|
||||||
def test_mnemo():
|
def test_mnemo():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
file = str(os.path.join(testDir, "support/mnemo.db"))
|
file = str(os.path.join(testDir, "support/mnemo.db"))
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from tests.shared import getEmptyCol
|
|
||||||
from anki.utils import stripHTML
|
from anki.utils import stripHTML
|
||||||
|
from tests.shared import getEmptyCol
|
||||||
|
|
||||||
|
|
||||||
def test_latex():
|
def test_latex():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# change latex cmd to simulate broken build
|
# change latex cmd to simulate broken build
|
||||||
import anki.latex
|
import anki.latex
|
||||||
|
|
||||||
anki.latex.pngCommands[0][0] = "nolatex"
|
anki.latex.pngCommands[0][0] = "nolatex"
|
||||||
# add a note with latex
|
# add a note with latex
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "[latex]hello[/latex]"
|
f["Front"] = "[latex]hello[/latex]"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# but since latex couldn't run, there's nothing there
|
# but since latex couldn't run, there's nothing there
|
||||||
assert len(os.listdir(d.media.dir())) == 0
|
assert len(os.listdir(d.media.dir())) == 0
|
||||||
|
@ -34,13 +35,13 @@ def test_latex():
|
||||||
assert ".png" in f.cards()[0].q()
|
assert ".png" in f.cards()[0].q()
|
||||||
# adding new notes should cause generation on question display
|
# adding new notes should cause generation on question display
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "[latex]world[/latex]"
|
f["Front"] = "[latex]world[/latex]"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
f.cards()[0].q()
|
f.cards()[0].q()
|
||||||
assert len(os.listdir(d.media.dir())) == 2
|
assert len(os.listdir(d.media.dir())) == 2
|
||||||
# another note with the same media should reuse
|
# another note with the same media should reuse
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = " [latex]world[/latex]"
|
f["Front"] = " [latex]world[/latex]"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
assert len(os.listdir(d.media.dir())) == 2
|
assert len(os.listdir(d.media.dir())) == 2
|
||||||
oldcard = f.cards()[0]
|
oldcard = f.cards()[0]
|
||||||
|
@ -49,7 +50,7 @@ def test_latex():
|
||||||
# missing media will show the latex
|
# missing media will show the latex
|
||||||
anki.latex.build = False
|
anki.latex.build = False
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "[latex]foo[/latex]"
|
f["Front"] = "[latex]foo[/latex]"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
assert len(os.listdir(d.media.dir())) == 2
|
assert len(os.listdir(d.media.dir())) == 2
|
||||||
assert stripHTML(f.cards()[0].q()) == "[latex]foo[/latex]"
|
assert stripHTML(f.cards()[0].q()) == "[latex]foo[/latex]"
|
||||||
|
@ -86,10 +87,11 @@ def test_latex():
|
||||||
(result, msg) = _test_includes_bad_command("\\emph")
|
(result, msg) = _test_includes_bad_command("\\emph")
|
||||||
assert not result, msg
|
assert not result, msg
|
||||||
|
|
||||||
|
|
||||||
def _test_includes_bad_command(bad):
|
def _test_includes_bad_command(bad):
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = '[latex]%s[/latex]' % bad
|
f["Front"] = "[latex]%s[/latex]" % bad
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
q = f.cards()[0].q()
|
q = f.cards()[0].q()
|
||||||
return ("'%s' is not allowed on cards" % bad in q, "Card content: %s" % q)
|
return ("'%s' is not allowed on cards" % bad in q, "Card content: %s" % q)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import tempfile
|
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from .shared import getEmptyCol, testDir
|
from .shared import getEmptyCol, testDir
|
||||||
|
@ -23,6 +23,7 @@ def test_add():
|
||||||
f.write("world")
|
f.write("world")
|
||||||
assert d.media.addFile(path) == "foo (1).jpg"
|
assert d.media.addFile(path) == "foo (1).jpg"
|
||||||
|
|
||||||
|
|
||||||
def test_strings():
|
def test_strings():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
mf = d.media.filesInStr
|
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'>ao") == ["foo.jpg"]
|
||||||
assert mf(mid, "aoeu<img src='foo.jpg' style='test'>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") == [
|
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, "aoeu<img src=foo.jpg style=bar>ao") == ["foo.jpg"]
|
||||||
assert mf(mid, "<img src=one><img src=two>") == ["one", "two"]
|
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">ao') == ["foo.jpg"]
|
||||||
assert mf(mid, "aoeu<img src=\"foo.jpg\"><img class=yo src=fo>ao") == [
|
assert mf(mid, 'aoeu<img src="foo.jpg"><img class=yo src=fo>ao') == [
|
||||||
"foo.jpg", "fo"]
|
"foo.jpg",
|
||||||
|
"fo",
|
||||||
|
]
|
||||||
assert mf(mid, "aou[sound:foo.mp3]aou") == ["foo.mp3"]
|
assert mf(mid, "aou[sound:foo.mp3]aou") == ["foo.mp3"]
|
||||||
sp = d.media.strip
|
sp = d.media.strip
|
||||||
assert sp("aoeu") == "aoeu"
|
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='http://foo.com'>") == "<img src='http://foo.com'>"
|
||||||
assert es('<img src="foo bar.jpg">') == '<img src="foo%20bar.jpg">'
|
assert es('<img src="foo bar.jpg">') == '<img src="foo%20bar.jpg">'
|
||||||
|
|
||||||
|
|
||||||
def test_deckIntegration():
|
def test_deckIntegration():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# create a media dir
|
# create a media dir
|
||||||
|
@ -56,11 +62,13 @@ def test_deckIntegration():
|
||||||
d.media.addFile(file)
|
d.media.addFile(file)
|
||||||
# add a note which references it
|
# add a note which references it
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "<img src='fake.png'>"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "<img src='fake.png'>"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# and one which references a non-existent file
|
# and one which references a non-existent file
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "<img src='fake2.png'>"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "<img src='fake2.png'>"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# and add another file which isn't used
|
# and add another file which isn't used
|
||||||
with open(os.path.join(d.media.dir(), "foo.jpg"), "w") as f:
|
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[0] == ["fake2.png"]
|
||||||
assert ret[1] == ["foo.jpg"]
|
assert ret[1] == ["foo.jpg"]
|
||||||
|
|
||||||
|
|
||||||
def test_changes():
|
def test_changes():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
|
|
||||||
def added():
|
def added():
|
||||||
return d.media.db.execute("select fname from media where csum is not null")
|
return d.media.db.execute("select fname from media where csum is not null")
|
||||||
|
|
||||||
def removed():
|
def removed():
|
||||||
return d.media.db.execute("select fname from media where csum is null")
|
return d.media.db.execute("select fname from media where csum is null")
|
||||||
|
|
||||||
assert not list(added())
|
assert not list(added())
|
||||||
assert not list(removed())
|
assert not list(removed())
|
||||||
# add a file
|
# add a file
|
||||||
|
@ -97,26 +109,27 @@ def test_changes():
|
||||||
assert not list(removed())
|
assert not list(removed())
|
||||||
# but if we add another file, it will
|
# but if we add another file, it will
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
with open(path+"2", "w") as f:
|
with open(path + "2", "w") as f:
|
||||||
f.write("yo")
|
f.write("yo")
|
||||||
d.media.findChanges()
|
d.media.findChanges()
|
||||||
assert len(list(added())) == 2
|
assert len(list(added())) == 2
|
||||||
assert not list(removed())
|
assert not list(removed())
|
||||||
# deletions should get noticed too
|
# deletions should get noticed too
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
os.unlink(path+"2")
|
os.unlink(path + "2")
|
||||||
d.media.findChanges()
|
d.media.findChanges()
|
||||||
assert len(list(added())) == 1
|
assert len(list(added())) == 1
|
||||||
assert len(list(removed())) == 1
|
assert len(list(removed())) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_illegal():
|
def test_illegal():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
aString = "a:b|cd\\e/f\0g*h"
|
aString = "a:b|cd\\e/f\0g*h"
|
||||||
good = "abcdefgh"
|
good = "abcdefgh"
|
||||||
assert d.media.stripIllegal(aString) == good
|
assert d.media.stripIllegal(aString) == good
|
||||||
for c in aString:
|
for c in aString:
|
||||||
bad = d.media.hasIllegal("somestring"+c+"morestring")
|
bad = d.media.hasIllegal("somestring" + c + "morestring")
|
||||||
if bad:
|
if bad:
|
||||||
assert(c not in good)
|
assert c not in good
|
||||||
else:
|
else:
|
||||||
assert(c in good)
|
assert c in good
|
||||||
|
|
|
@ -1,43 +1,47 @@
|
||||||
# coding: utf-8
|
# 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
|
import anki.template
|
||||||
|
from anki.consts import MODEL_CLOZE
|
||||||
|
from anki.utils import isWin, joinFields, stripHTML
|
||||||
|
from tests.shared import getEmptyCol
|
||||||
|
|
||||||
|
|
||||||
def test_modelDelete():
|
def test_modelDelete():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = '1'
|
f["Front"] = "1"
|
||||||
f['Back'] = '2'
|
f["Back"] = "2"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
assert deck.cardCount() == 1
|
assert deck.cardCount() == 1
|
||||||
deck.models.rem(deck.models.current())
|
deck.models.rem(deck.models.current())
|
||||||
assert deck.cardCount() == 0
|
assert deck.cardCount() == 0
|
||||||
|
|
||||||
|
|
||||||
def test_modelCopy():
|
def test_modelCopy():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
m = deck.models.current()
|
m = deck.models.current()
|
||||||
m2 = deck.models.copy(m)
|
m2 = deck.models.copy(m)
|
||||||
assert m2['name'] == "Basic copy"
|
assert m2["name"] == "Basic copy"
|
||||||
assert m2['id'] != m['id']
|
assert m2["id"] != m["id"]
|
||||||
assert len(m2['flds']) == 2
|
assert len(m2["flds"]) == 2
|
||||||
assert len(m['flds']) == 2
|
assert len(m["flds"]) == 2
|
||||||
assert len(m2['flds']) == len(m['flds'])
|
assert len(m2["flds"]) == len(m["flds"])
|
||||||
assert len(m['tmpls']) == 1
|
assert len(m["tmpls"]) == 1
|
||||||
assert len(m2['tmpls']) == 1
|
assert len(m2["tmpls"]) == 1
|
||||||
assert deck.models.scmhash(m) == deck.models.scmhash(m2)
|
assert deck.models.scmhash(m) == deck.models.scmhash(m2)
|
||||||
|
|
||||||
|
|
||||||
def test_fields():
|
def test_fields():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = '1'
|
f["Front"] = "1"
|
||||||
f['Back'] = '2'
|
f["Back"] = "2"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
m = d.models.current()
|
m = d.models.current()
|
||||||
# make sure renaming a field updates the templates
|
# make sure renaming a field updates the templates
|
||||||
d.models.renameField(m, m['flds'][0], "NewFront")
|
d.models.renameField(m, m["flds"][0], "NewFront")
|
||||||
assert "{{NewFront}}" in m['tmpls'][0]['qfmt']
|
assert "{{NewFront}}" in m["tmpls"][0]["qfmt"]
|
||||||
h = d.models.scmhash(m)
|
h = d.models.scmhash(m)
|
||||||
# add a field
|
# add a field
|
||||||
f = d.models.newField("foo")
|
f = d.models.newField("foo")
|
||||||
|
@ -46,44 +50,46 @@ def test_fields():
|
||||||
assert d.models.scmhash(m) != h
|
assert d.models.scmhash(m) != h
|
||||||
# rename it
|
# rename it
|
||||||
d.models.renameField(m, f, "bar")
|
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
|
# 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", ""]
|
assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""]
|
||||||
# move 0 -> 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"]
|
assert d.getNote(d.models.nids(m)[0]).fields == ["", "1"]
|
||||||
# move 1 -> 0
|
# 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", ""]
|
assert d.getNote(d.models.nids(m)[0]).fields == ["1", ""]
|
||||||
# add another and put in middle
|
# add another and put in middle
|
||||||
f = d.models.newField("baz")
|
f = d.models.newField("baz")
|
||||||
d.models.addField(m, f)
|
d.models.addField(m, f)
|
||||||
f = d.getNote(d.models.nids(m)[0])
|
f = d.getNote(d.models.nids(m)[0])
|
||||||
f['baz'] = "2"
|
f["baz"] = "2"
|
||||||
f.flush()
|
f.flush()
|
||||||
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "", "2"]
|
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "", "2"]
|
||||||
# move 2 -> 1
|
# 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", ""]
|
assert d.getNote(d.models.nids(m)[0]).fields == ["1", "2", ""]
|
||||||
# move 0 -> 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"]
|
assert d.getNote(d.models.nids(m)[0]).fields == ["2", "", "1"]
|
||||||
# move 0 -> 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"]
|
assert d.getNote(d.models.nids(m)[0]).fields == ["", "2", "1"]
|
||||||
|
|
||||||
|
|
||||||
def test_templates():
|
def test_templates():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
m = d.models.current(); mm = d.models
|
m = d.models.current()
|
||||||
|
mm = d.models
|
||||||
t = mm.newTemplate("Reverse")
|
t = mm.newTemplate("Reverse")
|
||||||
t['qfmt'] = "{{Back}}"
|
t["qfmt"] = "{{Back}}"
|
||||||
t['afmt'] = "{{Front}}"
|
t["afmt"] = "{{Front}}"
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
mm.save(m)
|
mm.save(m)
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = '1'
|
f["Front"] = "1"
|
||||||
f['Back'] = '2'
|
f["Back"] = "2"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
assert d.cardCount() == 2
|
assert d.cardCount() == 2
|
||||||
(c, c2) = f.cards()
|
(c, c2) = f.cards()
|
||||||
|
@ -92,11 +98,12 @@ def test_templates():
|
||||||
assert c2.ord == 1
|
assert c2.ord == 1
|
||||||
# switch templates
|
# switch templates
|
||||||
d.models.moveTemplate(m, c.template(), 1)
|
d.models.moveTemplate(m, c.template(), 1)
|
||||||
c.load(); c2.load()
|
c.load()
|
||||||
|
c2.load()
|
||||||
assert c.ord == 1
|
assert c.ord == 1
|
||||||
assert c2.ord == 0
|
assert c2.ord == 0
|
||||||
# removing a template should delete its cards
|
# 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
|
assert d.cardCount() == 1
|
||||||
# and should have updated the other cards' ordinals
|
# and should have updated the other cards' ordinals
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
|
@ -105,64 +112,67 @@ def test_templates():
|
||||||
# it shouldn't be possible to orphan notes by removing templates
|
# it shouldn't be possible to orphan notes by removing templates
|
||||||
t = mm.newTemplate("template name")
|
t = mm.newTemplate("template name")
|
||||||
mm.addTemplate(m, t)
|
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():
|
def test_cloze_ordinals():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
d.models.setCurrent(d.models.byName("Cloze"))
|
d.models.setCurrent(d.models.byName("Cloze"))
|
||||||
m = d.models.current(); mm = d.models
|
m = d.models.current()
|
||||||
|
mm = d.models
|
||||||
#We replace the default Cloze template
|
|
||||||
|
# We replace the default Cloze template
|
||||||
t = mm.newTemplate("ChainedCloze")
|
t = mm.newTemplate("ChainedCloze")
|
||||||
t['qfmt'] = "{{text:cloze:Text}}"
|
t["qfmt"] = "{{text:cloze:Text}}"
|
||||||
t['afmt'] = "{{text:cloze:Text}}"
|
t["afmt"] = "{{text:cloze:Text}}"
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
mm.save(m)
|
mm.save(m)
|
||||||
d.models.remTemplate(m, m['tmpls'][0])
|
d.models.remTemplate(m, m["tmpls"][0])
|
||||||
|
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Text'] = '{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}'
|
f["Text"] = "{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
assert d.cardCount() == 2
|
assert d.cardCount() == 2
|
||||||
(c, c2) = f.cards()
|
(c, c2) = f.cards()
|
||||||
# first card should have first ord
|
# first card should have first ord
|
||||||
assert c.ord == 0
|
assert c.ord == 0
|
||||||
assert c2.ord == 1
|
assert c2.ord == 1
|
||||||
|
|
||||||
|
|
||||||
def test_text():
|
def test_text():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
m = d.models.current()
|
m = d.models.current()
|
||||||
m['tmpls'][0]['qfmt'] = "{{text:Front}}"
|
m["tmpls"][0]["qfmt"] = "{{text:Front}}"
|
||||||
d.models.save(m)
|
d.models.save(m)
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = 'hello<b>world'
|
f["Front"] = "hello<b>world"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
assert "helloworld" in f.cards()[0].q()
|
assert "helloworld" in f.cards()[0].q()
|
||||||
|
|
||||||
|
|
||||||
def test_cloze():
|
def test_cloze():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
d.models.setCurrent(d.models.byName("Cloze"))
|
d.models.setCurrent(d.models.byName("Cloze"))
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
assert f.model()['name'] == "Cloze"
|
assert f.model()["name"] == "Cloze"
|
||||||
# a cloze model with no clozes is not empty
|
# a cloze model with no clozes is not empty
|
||||||
f['Text'] = 'nothing'
|
f["Text"] = "nothing"
|
||||||
assert d.addNote(f)
|
assert d.addNote(f)
|
||||||
# try with one cloze
|
# try with one cloze
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Text'] = "hello {{c1::world}}"
|
f["Text"] = "hello {{c1::world}}"
|
||||||
assert d.addNote(f) == 1
|
assert d.addNote(f) == 1
|
||||||
assert "hello <span class=cloze>[...]</span>" in f.cards()[0].q()
|
assert "hello <span class=cloze>[...]</span>" in f.cards()[0].q()
|
||||||
assert "hello <span class=cloze>world</span>" in f.cards()[0].a()
|
assert "hello <span class=cloze>world</span>" in f.cards()[0].a()
|
||||||
# and with a comment
|
# and with a comment
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Text'] = "hello {{c1::world::typical}}"
|
f["Text"] = "hello {{c1::world::typical}}"
|
||||||
assert d.addNote(f) == 1
|
assert d.addNote(f) == 1
|
||||||
assert "<span class=cloze>[typical]</span>" in f.cards()[0].q()
|
assert "<span class=cloze>[typical]</span>" in f.cards()[0].q()
|
||||||
assert "<span class=cloze>world</span>" in f.cards()[0].a()
|
assert "<span class=cloze>world</span>" in f.cards()[0].a()
|
||||||
# and with 2 clozes
|
# and with 2 clozes
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Text'] = "hello {{c1::world}} {{c2::bar}}"
|
f["Text"] = "hello {{c1::world}} {{c2::bar}}"
|
||||||
assert d.addNote(f) == 2
|
assert d.addNote(f) == 2
|
||||||
(c1, c2) = f.cards()
|
(c1, c2) = f.cards()
|
||||||
assert "<span class=cloze>[...]</span> bar" in c1.q()
|
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
|
# if there are multiple answers for a single cloze, they are given in a
|
||||||
# list
|
# list
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Text'] = "a {{c1::b}} {{c1::c}}"
|
f["Text"] = "a {{c1::b}} {{c1::c}}"
|
||||||
assert d.addNote(f) == 1
|
assert d.addNote(f) == 1
|
||||||
assert "<span class=cloze>b</span> <span class=cloze>c</span>" in (
|
assert "<span class=cloze>b</span> <span class=cloze>c</span>" in (f.cards()[0].a())
|
||||||
f.cards()[0].a())
|
|
||||||
# if we add another cloze, a card should be generated
|
# if we add another cloze, a card should be generated
|
||||||
cnt = d.cardCount()
|
cnt = d.cardCount()
|
||||||
f['Text'] = "{{c2::hello}} {{c1::foo}}"
|
f["Text"] = "{{c2::hello}} {{c1::foo}}"
|
||||||
f.flush()
|
f.flush()
|
||||||
assert d.cardCount() == cnt + 1
|
assert d.cardCount() == cnt + 1
|
||||||
# 0 or negative indices are not supported
|
# 0 or negative indices are not supported
|
||||||
f['Text'] += "{{c0::zero}} {{c-1:foo}}"
|
f["Text"] += "{{c0::zero}} {{c-1:foo}}"
|
||||||
f.flush()
|
f.flush()
|
||||||
assert len(f.cards()) == 2
|
assert len(f.cards()) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_cloze_mathjax():
|
def test_cloze_mathjax():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
d.models.setCurrent(d.models.byName("Cloze"))
|
d.models.setCurrent(d.models.byName("Cloze"))
|
||||||
f = d.newNote()
|
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 d.addNote(f)
|
||||||
assert len(f.cards()) == 5
|
assert len(f.cards()) == 5
|
||||||
assert "class=cloze" in f.cards()[0].q()
|
assert "class=cloze" in f.cards()[0].q()
|
||||||
|
@ -200,56 +212,70 @@ def test_cloze_mathjax():
|
||||||
assert "class=cloze" in f.cards()[4].q()
|
assert "class=cloze" in f.cards()[4].q()
|
||||||
|
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Text'] = r'\(a\) {{c1::b}} \[ {{c1::c}} \]'
|
f["Text"] = r"\(a\) {{c1::b}} \[ {{c1::c}} \]"
|
||||||
assert d.addNote(f)
|
assert d.addNote(f)
|
||||||
assert len(f.cards()) == 1
|
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():
|
def test_chained_mods():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
d.models.setCurrent(d.models.byName("Cloze"))
|
d.models.setCurrent(d.models.byName("Cloze"))
|
||||||
m = d.models.current(); mm = d.models
|
m = d.models.current()
|
||||||
|
mm = d.models
|
||||||
#We replace the default Cloze template
|
|
||||||
|
# We replace the default Cloze template
|
||||||
t = mm.newTemplate("ChainedCloze")
|
t = mm.newTemplate("ChainedCloze")
|
||||||
t['qfmt'] = "{{cloze:text:Text}}"
|
t["qfmt"] = "{{cloze:text:Text}}"
|
||||||
t['afmt'] = "{{cloze:text:Text}}"
|
t["afmt"] = "{{cloze:text:Text}}"
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
mm.save(m)
|
mm.save(m)
|
||||||
d.models.remTemplate(m, m['tmpls'][0])
|
d.models.remTemplate(m, m["tmpls"][0])
|
||||||
|
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
q1 = '<span style=\"color:red\">phrase</span>'
|
q1 = '<span style="color:red">phrase</span>'
|
||||||
a1 = '<b>sentence</b>'
|
a1 = "<b>sentence</b>"
|
||||||
q2 = '<span style=\"color:red\">en chaine</span>'
|
q2 = '<span style="color:red">en chaine</span>'
|
||||||
a2 = '<i>chained</i>'
|
a2 = "<i>chained</i>"
|
||||||
f['Text'] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (q1,a1,q2,a2)
|
f["Text"] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (
|
||||||
|
q1,
|
||||||
|
a1,
|
||||||
|
q2,
|
||||||
|
a2,
|
||||||
|
)
|
||||||
assert d.addNote(f) == 1
|
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 (
|
||||||
assert "This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes." in f.cards()[0].a()
|
"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():
|
def test_modelChange():
|
||||||
deck = getEmptyCol()
|
deck = getEmptyCol()
|
||||||
basic = deck.models.byName("Basic")
|
basic = deck.models.byName("Basic")
|
||||||
cloze = deck.models.byName("Cloze")
|
cloze = deck.models.byName("Cloze")
|
||||||
# enable second template and add a note
|
# 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 = mm.newTemplate("Reverse")
|
||||||
t['qfmt'] = "{{Back}}"
|
t["qfmt"] = "{{Back}}"
|
||||||
t['afmt'] = "{{Front}}"
|
t["afmt"] = "{{Front}}"
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
mm.save(m)
|
mm.save(m)
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = 'f'
|
f["Front"] = "f"
|
||||||
f['Back'] = 'b123'
|
f["Back"] = "b123"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
# switch fields
|
# switch fields
|
||||||
map = {0: 1, 1: 0}
|
map = {0: 1, 1: 0}
|
||||||
deck.models.change(basic, [f.id], basic, map, None)
|
deck.models.change(basic, [f.id], basic, map, None)
|
||||||
f.load()
|
f.load()
|
||||||
assert f['Front'] == 'b123'
|
assert f["Front"] == "b123"
|
||||||
assert f['Back'] == 'f'
|
assert f["Back"] == "f"
|
||||||
# switch cards
|
# switch cards
|
||||||
c0 = f.cards()[0]
|
c0 = f.cards()[0]
|
||||||
c1 = f.cards()[1]
|
c1 = f.cards()[1]
|
||||||
|
@ -258,7 +284,9 @@ def test_modelChange():
|
||||||
assert c0.ord == 0
|
assert c0.ord == 0
|
||||||
assert c1.ord == 1
|
assert c1.ord == 1
|
||||||
deck.models.change(basic, [f.id], basic, None, map)
|
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 "f" in c0.q()
|
||||||
assert "b123" in c1.q()
|
assert "b123" in c1.q()
|
||||||
assert c0.ord == 1
|
assert c0.ord == 1
|
||||||
|
@ -267,6 +295,9 @@ def test_modelChange():
|
||||||
assert f.cards()[0].id == c1.id
|
assert f.cards()[0].id == c1.id
|
||||||
# delete first card
|
# delete first card
|
||||||
map = {0: None, 1: 1}
|
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)
|
deck.models.change(basic, [f.id], basic, None, map)
|
||||||
f.load()
|
f.load()
|
||||||
c0.load()
|
c0.load()
|
||||||
|
@ -279,30 +310,31 @@ def test_modelChange():
|
||||||
# but we have two cards, as a new one was generated
|
# but we have two cards, as a new one was generated
|
||||||
assert len(f.cards()) == 2
|
assert len(f.cards()) == 2
|
||||||
# an unmapped field becomes blank
|
# an unmapped field becomes blank
|
||||||
assert f['Front'] == 'b123'
|
assert f["Front"] == "b123"
|
||||||
assert f['Back'] == 'f'
|
assert f["Back"] == "f"
|
||||||
deck.models.change(basic, [f.id], basic, map, None)
|
deck.models.change(basic, [f.id], basic, map, None)
|
||||||
f.load()
|
f.load()
|
||||||
assert f['Front'] == ''
|
assert f["Front"] == ""
|
||||||
assert f['Back'] == 'f'
|
assert f["Back"] == "f"
|
||||||
# another note to try model conversion
|
# another note to try model conversion
|
||||||
f = deck.newNote()
|
f = deck.newNote()
|
||||||
f['Front'] = 'f2'
|
f["Front"] = "f2"
|
||||||
f['Back'] = 'b2'
|
f["Back"] = "b2"
|
||||||
deck.addNote(f)
|
deck.addNote(f)
|
||||||
assert deck.models.useCount(basic) == 2
|
assert deck.models.useCount(basic) == 2
|
||||||
assert deck.models.useCount(cloze) == 0
|
assert deck.models.useCount(cloze) == 0
|
||||||
map = {0: 0, 1: 1}
|
map = {0: 0, 1: 1}
|
||||||
deck.models.change(basic, [f.id], cloze, map, map)
|
deck.models.change(basic, [f.id], cloze, map, map)
|
||||||
f.load()
|
f.load()
|
||||||
assert f['Text'] == "f2"
|
assert f["Text"] == "f2"
|
||||||
assert len(f.cards()) == 2
|
assert len(f.cards()) == 2
|
||||||
# back the other way, with deletion of second ord
|
# 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
|
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 2
|
||||||
deck.models.change(cloze, [f.id], basic, map, map)
|
deck.models.change(cloze, [f.id], basic, map, map)
|
||||||
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1
|
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_templates():
|
def test_templates():
|
||||||
d = dict(Foo="x", Bar="y")
|
d = dict(Foo="x", Bar="y")
|
||||||
assert anki.template.render("{{Foo}}", d) == "x"
|
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("{{#Bar}}{{#Foo}}{{Foo}}{{/Foo}}{{/Bar}}", d) == "x"
|
||||||
assert anki.template.render("{{#Baz}}{{#Foo}}{{Foo}}{{/Foo}}{{/Baz}}", d) == ""
|
assert anki.template.render("{{#Baz}}{{#Foo}}{{Foo}}{{/Foo}}{{/Baz}}", d) == ""
|
||||||
|
|
||||||
|
|
||||||
def test_availOrds():
|
def test_availOrds():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
m = d.models.current(); mm = d.models
|
m = d.models.current()
|
||||||
t = m['tmpls'][0]
|
mm = d.models
|
||||||
|
t = m["tmpls"][0]
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "1"
|
f["Front"] = "1"
|
||||||
# simple templates
|
# simple templates
|
||||||
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
||||||
t['qfmt'] = "{{Back}}"
|
t["qfmt"] = "{{Back}}"
|
||||||
mm.save(m, templates=True)
|
mm.save(m, templates=True)
|
||||||
assert not mm.availOrds(m, joinFields(f.fields))
|
assert not mm.availOrds(m, joinFields(f.fields))
|
||||||
# AND
|
# AND
|
||||||
t['qfmt'] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
|
t["qfmt"] = "{{#Front}}{{#Back}}{{Front}}{{/Back}}{{/Front}}"
|
||||||
mm.save(m, templates=True)
|
mm.save(m, templates=True)
|
||||||
assert not mm.availOrds(m, joinFields(f.fields))
|
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)
|
mm.save(m, templates=True)
|
||||||
assert not mm.availOrds(m, joinFields(f.fields))
|
assert not mm.availOrds(m, joinFields(f.fields))
|
||||||
# OR
|
# OR
|
||||||
t['qfmt'] = "{{Front}}\n{{Back}}"
|
t["qfmt"] = "{{Front}}\n{{Back}}"
|
||||||
mm.save(m, templates=True)
|
mm.save(m, templates=True)
|
||||||
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
||||||
t['Front'] = ""
|
t["Front"] = ""
|
||||||
t['Back'] = "1"
|
t["Back"] = "1"
|
||||||
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
assert mm.availOrds(m, joinFields(f.fields)) == [0]
|
||||||
|
|
||||||
|
|
||||||
def test_req():
|
def test_req():
|
||||||
def reqSize(model):
|
def reqSize(model):
|
||||||
if model['type'] == MODEL_CLOZE:
|
if model["type"] == MODEL_CLOZE:
|
||||||
return
|
return
|
||||||
assert (len(model['tmpls']) == len(model['req']))
|
assert len(model["tmpls"]) == len(model["req"])
|
||||||
|
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
mm = d.models
|
mm = d.models
|
||||||
basic = mm.byName("Basic")
|
basic = mm.byName("Basic")
|
||||||
assert 'req' in basic
|
assert "req" in basic
|
||||||
reqSize(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)")
|
opt = mm.byName("Basic (optional reversed card)")
|
||||||
reqSize(opt)
|
reqSize(opt)
|
||||||
assert opt['req'][0] == [0, 'all', [0]]
|
r = opt["req"][0]
|
||||||
assert opt['req'][1] == [1, 'all', [1, 2]]
|
assert r[1] in ("any", "all")
|
||||||
#testing any
|
assert r[2] == [0]
|
||||||
opt['tmpls'][1]['qfmt'] = "{{Back}}{{Add Reverse}}"
|
assert opt["req"][1] == [1, "all", [1, 2]]
|
||||||
|
# testing any
|
||||||
|
opt["tmpls"][1]["qfmt"] = "{{Back}}{{Add Reverse}}"
|
||||||
mm.save(opt, templates=True)
|
mm.save(opt, templates=True)
|
||||||
assert opt['req'][1] == [1, 'any', [1, 2]]
|
assert opt["req"][1] == [1, "any", [1, 2]]
|
||||||
#testing None
|
# testing None
|
||||||
opt['tmpls'][1]['qfmt'] = "{{^Add Reverse}}{{Back}}{{/Add Reverse}}"
|
opt["tmpls"][1]["qfmt"] = "{{^Add Reverse}}{{Back}}{{/Add Reverse}}"
|
||||||
mm.save(opt, templates=True)
|
mm.save(opt, templates=True)
|
||||||
assert opt['req'][1] == [1, 'none', []]
|
assert opt["req"][1] == [1, "none", []]
|
||||||
|
|
||||||
def test_updatereqs_performance():
|
opt = mm.byName("Basic (type in the answer)")
|
||||||
import time
|
reqSize(opt)
|
||||||
d = getEmptyCol()
|
r = opt["req"][0]
|
||||||
mm = d.models
|
assert r[1] in ("any", "all")
|
||||||
m = mm.byName("Basic")
|
assert r[2] == [0]
|
||||||
for i in range(100):
|
|
||||||
fld = mm.newField(f"field{i}")
|
|
||||||
mm.addField(m, fld)
|
# def test_updatereqs_performance():
|
||||||
tmpl = mm.newTemplate(f"template{i}")
|
# import time
|
||||||
tmpl['qfmt'] = "{{field%s}}" % i
|
# d = getEmptyCol()
|
||||||
mm.addTemplate(m, tmpl)
|
# mm = d.models
|
||||||
t = time.time()
|
# m = mm.byName("Basic")
|
||||||
mm.save(m, templates=True)
|
# for i in range(100):
|
||||||
print("took", (time.time()-t)*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
|
# coding: utf-8
|
||||||
|
|
||||||
import time
|
|
||||||
import copy
|
import copy
|
||||||
|
import time
|
||||||
|
|
||||||
from anki.consts import STARTING_FACTOR
|
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.hooks import addHook
|
||||||
|
from anki.utils import intTime
|
||||||
|
from tests.shared import getEmptyCol as getEmptyColOrig
|
||||||
|
|
||||||
|
|
||||||
def getEmptyCol():
|
def getEmptyCol():
|
||||||
col = getEmptyColOrig()
|
col = getEmptyColOrig()
|
||||||
col.changeSchedulerVer(1)
|
col.changeSchedulerVer(1)
|
||||||
return col
|
return col
|
||||||
|
|
||||||
|
|
||||||
def test_clock():
|
def test_clock():
|
||||||
d = getEmptyCol()
|
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.")
|
raise Exception("Unit tests will fail around the day rollover.")
|
||||||
|
|
||||||
|
|
||||||
def checkRevIvl(d, c, targetIvl):
|
def checkRevIvl(d, c, targetIvl):
|
||||||
min, max = d.sched._fuzzIvlRange(targetIvl)
|
min, max = d.sched._fuzzIvlRange(targetIvl)
|
||||||
return min <= c.ivl <= max
|
return min <= c.ivl <= max
|
||||||
|
|
||||||
|
|
||||||
def test_basics():
|
def test_basics():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
d.reset()
|
d.reset()
|
||||||
assert not d.sched.getCard()
|
assert not d.sched.getCard()
|
||||||
|
|
||||||
|
|
||||||
def test_new():
|
def test_new():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.newCount == 0
|
assert d.sched.newCount == 0
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.newCount == 1
|
assert d.sched.newCount == 1
|
||||||
|
@ -71,15 +77,16 @@ def test_new():
|
||||||
# assert qs[n] in c.q()
|
# assert qs[n] in c.q()
|
||||||
# d.sched.answerCard(c, 2)
|
# d.sched.answerCard(c, 2)
|
||||||
|
|
||||||
|
|
||||||
def test_newLimits():
|
def test_newLimits():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add some notes
|
# add some notes
|
||||||
g2 = d.decks.id("Default::foo")
|
g2 = d.decks.id("Default::foo")
|
||||||
for i in range(30):
|
for i in range(30):
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = str(i)
|
f["Front"] = str(i)
|
||||||
if i > 4:
|
if i > 4:
|
||||||
f.model()['did'] = g2
|
f.model()["did"] = g2
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# give the child deck a different configuration
|
# give the child deck a different configuration
|
||||||
c2 = d.decks.confId("new conf")
|
c2 = d.decks.confId("new conf")
|
||||||
|
@ -92,33 +99,36 @@ def test_newLimits():
|
||||||
assert c.did == 1
|
assert c.did == 1
|
||||||
# limit the parent to 10 cards, meaning we get 10 in total
|
# limit the parent to 10 cards, meaning we get 10 in total
|
||||||
conf1 = d.decks.confForDid(1)
|
conf1 = d.decks.confForDid(1)
|
||||||
conf1['new']['perDay'] = 10
|
conf1["new"]["perDay"] = 10
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.newCount == 10
|
assert d.sched.newCount == 10
|
||||||
# if we limit child to 4, we should get 9
|
# if we limit child to 4, we should get 9
|
||||||
conf2 = d.decks.confForDid(g2)
|
conf2 = d.decks.confForDid(g2)
|
||||||
conf2['new']['perDay'] = 4
|
conf2["new"]["perDay"] = 4
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.newCount == 9
|
assert d.sched.newCount == 9
|
||||||
|
|
||||||
|
|
||||||
def test_newBoxes():
|
def test_newBoxes():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
c = d.sched.getCard()
|
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)
|
d.sched.answerCard(c, 2)
|
||||||
# should handle gracefully
|
# should handle gracefully
|
||||||
d.sched._cardConf(c)['new']['delays'] = [1]
|
d.sched._cardConf(c)["new"]["delays"] = [1]
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
|
|
||||||
|
|
||||||
def test_learn():
|
def test_learn():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
f = d.addNote(f)
|
f = d.addNote(f)
|
||||||
# set as a learn card and rebuild queues
|
# set as a learn card and rebuild queues
|
||||||
d.db.execute("update cards set queue=0, type=0")
|
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
|
# sched.getCard should return it, since it's due in the past
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
assert c
|
assert c
|
||||||
d.sched._cardConf(c)['new']['delays'] = [0.5, 3, 10]
|
d.sched._cardConf(c)["new"]["delays"] = [0.5, 3, 10]
|
||||||
# fail it
|
# fail it
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
# it should have three reps left to graduation
|
# 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
|
# it should by due in 30 seconds
|
||||||
t = round(c.due - time.time())
|
t = round(c.due - time.time())
|
||||||
assert t >= 25 and t <= 40
|
assert t >= 25 and t <= 40
|
||||||
|
@ -139,8 +149,8 @@ def test_learn():
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
# it should by due in 3 minutes
|
# it should by due in 3 minutes
|
||||||
assert round(c.due - time.time()) in (179, 180)
|
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
|
# check log is accurate
|
||||||
log = d.db.first("select * from revlog order by id desc")
|
log = d.db.first("select * from revlog order by id desc")
|
||||||
assert log[3] == 2
|
assert log[3] == 2
|
||||||
|
@ -150,8 +160,8 @@ def test_learn():
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
# it should by due in 10 minutes
|
# it should by due in 10 minutes
|
||||||
assert round(c.due - time.time()) in (599, 600)
|
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
|
# the next pass should graduate the card
|
||||||
assert c.queue == 1
|
assert c.queue == 1
|
||||||
assert c.type == 1
|
assert c.type == 1
|
||||||
|
@ -159,7 +169,7 @@ def test_learn():
|
||||||
assert c.queue == 2
|
assert c.queue == 2
|
||||||
assert c.type == 2
|
assert c.type == 2
|
||||||
# should be due tomorrow, with an interval of 1
|
# 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
|
assert c.ivl == 1
|
||||||
# or normal removal
|
# or normal removal
|
||||||
c.type = 0
|
c.type = 0
|
||||||
|
@ -188,14 +198,15 @@ def test_learn():
|
||||||
assert c.queue == 2
|
assert c.queue == 2
|
||||||
assert c.due == 321
|
assert c.due == 321
|
||||||
|
|
||||||
|
|
||||||
def test_learn_collapsed():
|
def test_learn_collapsed():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add 2 notes
|
# add 2 notes
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "1"
|
f["Front"] = "1"
|
||||||
f = d.addNote(f)
|
f = d.addNote(f)
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "2"
|
f["Front"] = "2"
|
||||||
f = d.addNote(f)
|
f = d.addNote(f)
|
||||||
# set as a learn card and rebuild queues
|
# set as a learn card and rebuild queues
|
||||||
d.db.execute("update cards set queue=0, type=0")
|
d.db.execute("update cards set queue=0, type=0")
|
||||||
|
@ -214,27 +225,28 @@ def test_learn_collapsed():
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
assert not c.q().endswith("2")
|
assert not c.q().endswith("2")
|
||||||
|
|
||||||
|
|
||||||
def test_learn_day():
|
def test_learn_day():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
f = d.addNote(f)
|
f = d.addNote(f)
|
||||||
d.sched.reset()
|
d.sched.reset()
|
||||||
c = d.sched.getCard()
|
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
|
# pass it
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
# two reps to graduate, 1 more today
|
# two reps to graduate, 1 more today
|
||||||
assert c.left%1000 == 3
|
assert c.left % 1000 == 3
|
||||||
assert c.left//1000 == 1
|
assert c.left // 1000 == 1
|
||||||
assert d.sched.counts() == (0, 1, 0)
|
assert d.sched.counts() == (0, 1, 0)
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
ni = d.sched.nextIvl
|
ni = d.sched.nextIvl
|
||||||
assert ni(c, 2) == 86400
|
assert ni(c, 2) == 86400
|
||||||
# answering it will place it in queue 3
|
# answering it will place it in queue 3
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
assert c.due == d.sched.today+1
|
assert c.due == d.sched.today + 1
|
||||||
assert c.queue == 3
|
assert c.queue == 3
|
||||||
assert not d.sched.getCard()
|
assert not d.sched.getCard()
|
||||||
# for testing, move it back a day
|
# for testing, move it back a day
|
||||||
|
@ -244,7 +256,7 @@ def test_learn_day():
|
||||||
assert d.sched.counts() == (0, 1, 0)
|
assert d.sched.counts() == (0, 1, 0)
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
# nextIvl should work
|
# 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
|
# if we fail it, it should be back in the correct queue
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert c.queue == 1
|
assert c.queue == 1
|
||||||
|
@ -266,17 +278,19 @@ def test_learn_day():
|
||||||
c.flush()
|
c.flush()
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (0, 0, 1)
|
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()
|
c = d.sched.getCard()
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert c.queue == 3
|
assert c.queue == 3
|
||||||
assert d.sched.counts() == (0, 0, 0)
|
assert d.sched.counts() == (0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_reviews():
|
def test_reviews():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# set the card up as a review card, due 8 days ago
|
# set the card up as a review card, due 8 days ago
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
|
@ -295,7 +309,7 @@ def test_reviews():
|
||||||
##################################################
|
##################################################
|
||||||
# different delay to new
|
# different delay to new
|
||||||
d.reset()
|
d.reset()
|
||||||
d.sched._cardConf(c)['lapse']['delays'] = [2, 20]
|
d.sched._cardConf(c)["lapse"]["delays"] = [2, 20]
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert c.queue == 1
|
assert c.queue == 1
|
||||||
# it should be due tomorrow, with an interval of 1
|
# it should be due tomorrow, with an interval of 1
|
||||||
|
@ -313,7 +327,7 @@ def test_reviews():
|
||||||
# check ests.
|
# check ests.
|
||||||
ni = d.sched.nextIvl
|
ni = d.sched.nextIvl
|
||||||
assert ni(c, 1) == 120
|
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
|
# try again with an ease of 2 instead
|
||||||
##################################################
|
##################################################
|
||||||
c = copy.copy(cardcopy)
|
c = copy.copy(cardcopy)
|
||||||
|
@ -355,8 +369,10 @@ def test_reviews():
|
||||||
c.flush()
|
c.flush()
|
||||||
# steup hook
|
# steup hook
|
||||||
hooked = []
|
hooked = []
|
||||||
|
|
||||||
def onLeech(card):
|
def onLeech(card):
|
||||||
hooked.append(1)
|
hooked.append(1)
|
||||||
|
|
||||||
addHook("leech", onLeech)
|
addHook("leech", onLeech)
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert hooked
|
assert hooked
|
||||||
|
@ -364,10 +380,11 @@ def test_reviews():
|
||||||
c.load()
|
c.load()
|
||||||
assert c.queue == -1
|
assert c.queue == -1
|
||||||
|
|
||||||
|
|
||||||
def test_button_spacing():
|
def test_button_spacing():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# 1 day ivl review card due now
|
# 1 day ivl review card due now
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
|
@ -384,13 +401,14 @@ def test_button_spacing():
|
||||||
assert ni(c, 3) == "3 days"
|
assert ni(c, 3) == "3 days"
|
||||||
assert ni(c, 4) == "4 days"
|
assert ni(c, 4) == "4 days"
|
||||||
|
|
||||||
|
|
||||||
def test_overdue_lapse():
|
def test_overdue_lapse():
|
||||||
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2
|
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2
|
||||||
return
|
return
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# simulate a review that was lapsed and is now due for its normal review
|
# simulate a review that was lapsed and is now due for its normal review
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
|
@ -419,13 +437,15 @@ def test_overdue_lapse():
|
||||||
d.sched.reset()
|
d.sched.reset()
|
||||||
assert d.sched.counts() == (0, 0, 1)
|
assert d.sched.counts() == (0, 0, 1)
|
||||||
|
|
||||||
|
|
||||||
def test_finished():
|
def test_finished():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# nothing due
|
# nothing due
|
||||||
assert "Congratulations" in d.sched.finishedMsg()
|
assert "Congratulations" in d.sched.finishedMsg()
|
||||||
assert "limit" not in d.sched.finishedMsg()
|
assert "limit" not in d.sched.finishedMsg()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# have a new card
|
# have a new card
|
||||||
assert "new cards available" in d.sched.finishedMsg()
|
assert "new cards available" in d.sched.finishedMsg()
|
||||||
|
@ -438,44 +458,46 @@ def test_finished():
|
||||||
assert "Congratulations" in d.sched.finishedMsg()
|
assert "Congratulations" in d.sched.finishedMsg()
|
||||||
assert "limit" not in d.sched.finishedMsg()
|
assert "limit" not in d.sched.finishedMsg()
|
||||||
|
|
||||||
|
|
||||||
def test_nextIvl():
|
def test_nextIvl():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
conf = d.decks.confForDid(1)
|
conf = d.decks.confForDid(1)
|
||||||
conf['new']['delays'] = [0.5, 3, 10]
|
conf["new"]["delays"] = [0.5, 3, 10]
|
||||||
conf['lapse']['delays'] = [1, 5, 9]
|
conf["lapse"]["delays"] = [1, 5, 9]
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
# new cards
|
# new cards
|
||||||
##################################################
|
##################################################
|
||||||
ni = d.sched.nextIvl
|
ni = d.sched.nextIvl
|
||||||
assert ni(c, 1) == 30
|
assert ni(c, 1) == 30
|
||||||
assert ni(c, 2) == 180
|
assert ni(c, 2) == 180
|
||||||
assert ni(c, 3) == 4*86400
|
assert ni(c, 3) == 4 * 86400
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
# cards in learning
|
# cards in learning
|
||||||
##################################################
|
##################################################
|
||||||
assert ni(c, 1) == 30
|
assert ni(c, 1) == 30
|
||||||
assert ni(c, 2) == 180
|
assert ni(c, 2) == 180
|
||||||
assert ni(c, 3) == 4*86400
|
assert ni(c, 3) == 4 * 86400
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
assert ni(c, 1) == 30
|
assert ni(c, 1) == 30
|
||||||
assert ni(c, 2) == 600
|
assert ni(c, 2) == 600
|
||||||
assert ni(c, 3) == 4*86400
|
assert ni(c, 3) == 4 * 86400
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
# normal graduation is tomorrow
|
# normal graduation is tomorrow
|
||||||
assert ni(c, 2) == 1*86400
|
assert ni(c, 2) == 1 * 86400
|
||||||
assert ni(c, 3) == 4*86400
|
assert ni(c, 3) == 4 * 86400
|
||||||
# lapsed cards
|
# lapsed cards
|
||||||
##################################################
|
##################################################
|
||||||
c.type = 2
|
c.type = 2
|
||||||
c.ivl = 100
|
c.ivl = 100
|
||||||
c.factor = STARTING_FACTOR
|
c.factor = STARTING_FACTOR
|
||||||
assert ni(c, 1) == 60
|
assert ni(c, 1) == 60
|
||||||
assert ni(c, 2) == 100*86400
|
assert ni(c, 2) == 100 * 86400
|
||||||
assert ni(c, 3) == 100*86400
|
assert ni(c, 3) == 100 * 86400
|
||||||
# review cards
|
# review cards
|
||||||
##################################################
|
##################################################
|
||||||
c.queue = 2
|
c.queue = 2
|
||||||
|
@ -484,8 +506,8 @@ def test_nextIvl():
|
||||||
# failing it should put it at 60s
|
# failing it should put it at 60s
|
||||||
assert ni(c, 1) == 60
|
assert ni(c, 1) == 60
|
||||||
# or 1 day if relearn is false
|
# or 1 day if relearn is false
|
||||||
d.sched._cardConf(c)['lapse']['delays']=[]
|
d.sched._cardConf(c)["lapse"]["delays"] = []
|
||||||
assert ni(c, 1) == 1*86400
|
assert ni(c, 1) == 1 * 86400
|
||||||
# (* 100 1.2 86400)10368000.0
|
# (* 100 1.2 86400)10368000.0
|
||||||
assert ni(c, 2) == 10368000
|
assert ni(c, 2) == 10368000
|
||||||
# (* 100 2.5 86400)21600000.0
|
# (* 100 2.5 86400)21600000.0
|
||||||
|
@ -494,10 +516,11 @@ def test_nextIvl():
|
||||||
assert ni(c, 4) == 28080000
|
assert ni(c, 4) == 28080000
|
||||||
assert d.sched.nextIvlStr(c, 4) == "10.8 months"
|
assert d.sched.nextIvlStr(c, 4) == "10.8 months"
|
||||||
|
|
||||||
|
|
||||||
def test_misc():
|
def test_misc():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
# burying
|
# burying
|
||||||
|
@ -508,10 +531,11 @@ def test_misc():
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.getCard()
|
assert d.sched.getCard()
|
||||||
|
|
||||||
|
|
||||||
def test_suspend():
|
def test_suspend():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
# suspending
|
# suspending
|
||||||
|
@ -525,7 +549,11 @@ def test_suspend():
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.getCard()
|
assert d.sched.getCard()
|
||||||
# should cope with rev cards being relearnt
|
# 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()
|
d.reset()
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
|
@ -551,10 +579,11 @@ def test_suspend():
|
||||||
assert c.due == 1
|
assert c.due == 1
|
||||||
assert c.did == 1
|
assert c.did == 1
|
||||||
|
|
||||||
|
|
||||||
def test_cram():
|
def test_cram():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
c.ivl = 100
|
c.ivl = 100
|
||||||
|
@ -566,7 +595,7 @@ def test_cram():
|
||||||
c.startTimer()
|
c.startTimer()
|
||||||
c.flush()
|
c.flush()
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (0,0,0)
|
assert d.sched.counts() == (0, 0, 0)
|
||||||
cardcopy = copy.copy(c)
|
cardcopy = copy.copy(c)
|
||||||
# create a dynamic deck and refresh it
|
# create a dynamic deck and refresh it
|
||||||
did = d.decks.newDyn("Cram")
|
did = d.decks.newDyn("Cram")
|
||||||
|
@ -575,18 +604,18 @@ def test_cram():
|
||||||
# should appear as new in the deck list
|
# should appear as new in the deck list
|
||||||
assert sorted(d.sched.deckDueList())[0][4] == 1
|
assert sorted(d.sched.deckDueList())[0][4] == 1
|
||||||
# and should appear in the counts
|
# 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
|
# grab it and check estimates
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
assert d.sched.answerButtons(c) == 2
|
assert d.sched.answerButtons(c) == 2
|
||||||
assert d.sched.nextIvl(c, 1) == 600
|
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 = d.decks.get(did)
|
||||||
cram['delays'] = [1, 10]
|
cram["delays"] = [1, 10]
|
||||||
assert d.sched.answerButtons(c) == 3
|
assert d.sched.answerButtons(c) == 3
|
||||||
assert d.sched.nextIvl(c, 1) == 60
|
assert d.sched.nextIvl(c, 1) == 60
|
||||||
assert d.sched.nextIvl(c, 2) == 600
|
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)
|
d.sched.answerCard(c, 2)
|
||||||
# elapsed time was 75 days
|
# elapsed time was 75 days
|
||||||
# factor = 2.5+1.2/2 = 1.85
|
# factor = 2.5+1.2/2 = 1.85
|
||||||
|
@ -595,12 +624,11 @@ def test_cram():
|
||||||
assert c.odue == 138
|
assert c.odue == 138
|
||||||
assert c.queue == 1
|
assert c.queue == 1
|
||||||
# should be logged as a cram rep
|
# should be logged as a cram rep
|
||||||
assert d.db.scalar(
|
assert d.db.scalar("select type from revlog order by id desc limit 1") == 3
|
||||||
"select type from revlog order by id desc limit 1") == 3
|
|
||||||
# check ivls again
|
# check ivls again
|
||||||
assert d.sched.nextIvl(c, 1) == 60
|
assert d.sched.nextIvl(c, 1) == 60
|
||||||
assert d.sched.nextIvl(c, 2) == 138*60*60*24
|
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, 3) == 138 * 60 * 60 * 24
|
||||||
# when it graduates, due is updated
|
# when it graduates, due is updated
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
|
@ -616,7 +644,7 @@ def test_cram():
|
||||||
# check ivls again - passing should be idempotent
|
# check ivls again - passing should be idempotent
|
||||||
assert d.sched.nextIvl(c, 1) == 60
|
assert d.sched.nextIvl(c, 1) == 60
|
||||||
assert d.sched.nextIvl(c, 2) == 600
|
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)
|
d.sched.answerCard(c, 2)
|
||||||
assert c.ivl == 138
|
assert c.ivl == 138
|
||||||
assert c.odue == 138
|
assert c.odue == 138
|
||||||
|
@ -630,20 +658,20 @@ def test_cram():
|
||||||
assert len(d.sched.deckDueList()) == 1
|
assert len(d.sched.deckDueList()) == 1
|
||||||
c.load()
|
c.load()
|
||||||
assert c.ivl == 1
|
assert c.ivl == 1
|
||||||
assert c.due == d.sched.today+1
|
assert c.due == d.sched.today + 1
|
||||||
# make it due
|
# make it due
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (0,0,0)
|
assert d.sched.counts() == (0, 0, 0)
|
||||||
c.due = -5
|
c.due = -5
|
||||||
c.ivl = 100
|
c.ivl = 100
|
||||||
c.flush()
|
c.flush()
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (0,0,1)
|
assert d.sched.counts() == (0, 0, 1)
|
||||||
# cram again
|
# cram again
|
||||||
did = d.decks.newDyn("Cram")
|
did = d.decks.newDyn("Cram")
|
||||||
d.sched.rebuildDyn(did)
|
d.sched.rebuildDyn(did)
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (0,0,1)
|
assert d.sched.counts() == (0, 0, 1)
|
||||||
c.load()
|
c.load()
|
||||||
assert d.sched.answerButtons(c) == 4
|
assert d.sched.answerButtons(c) == 4
|
||||||
# add a sibling so we can test minSpace, etc
|
# 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
|
# it should have been moved back to the original deck
|
||||||
assert c.did == 1
|
assert c.did == 1
|
||||||
|
|
||||||
|
|
||||||
def test_cram_rem():
|
def test_cram_rem():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
oldDue = f.cards()[0].due
|
oldDue = f.cards()[0].due
|
||||||
did = d.decks.newDyn("Cram")
|
did = d.decks.newDyn("Cram")
|
||||||
|
@ -681,16 +710,17 @@ def test_cram_rem():
|
||||||
assert c.type == c.queue == 0
|
assert c.type == c.queue == 0
|
||||||
assert c.due == oldDue
|
assert c.due == oldDue
|
||||||
|
|
||||||
|
|
||||||
def test_cram_resched():
|
def test_cram_resched():
|
||||||
# add card
|
# add card
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# cram deck
|
# cram deck
|
||||||
did = d.decks.newDyn("Cram")
|
did = d.decks.newDyn("Cram")
|
||||||
cram = d.decks.get(did)
|
cram = d.decks.get(did)
|
||||||
cram['resched'] = False
|
cram["resched"] = False
|
||||||
d.sched.rebuildDyn(did)
|
d.sched.rebuildDyn(did)
|
||||||
d.reset()
|
d.reset()
|
||||||
# graduate should return it to new
|
# graduate should return it to new
|
||||||
|
@ -786,22 +816,25 @@ def test_cram_resched():
|
||||||
# d.sched.answerCard(c, 2)
|
# d.sched.answerCard(c, 2)
|
||||||
# print c.__dict__
|
# print c.__dict__
|
||||||
|
|
||||||
|
|
||||||
def test_ordcycle():
|
def test_ordcycle():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add two more templates and set second active
|
# 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 = mm.newTemplate("Reverse")
|
||||||
t['qfmt'] = "{{Back}}"
|
t["qfmt"] = "{{Back}}"
|
||||||
t['afmt'] = "{{Front}}"
|
t["afmt"] = "{{Front}}"
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
t = mm.newTemplate("f2")
|
t = mm.newTemplate("f2")
|
||||||
t['qfmt'] = "{{Front}}"
|
t["qfmt"] = "{{Front}}"
|
||||||
t['afmt'] = "{{Back}}"
|
t["afmt"] = "{{Back}}"
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
mm.save(m)
|
mm.save(m)
|
||||||
# create a new note; it should have 3 cards
|
# create a new note; it should have 3 cards
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "1"; f['Back'] = "1"
|
f["Front"] = "1"
|
||||||
|
f["Back"] = "1"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
assert d.cardCount() == 3
|
assert d.cardCount() == 3
|
||||||
d.reset()
|
d.reset()
|
||||||
|
@ -810,10 +843,12 @@ def test_ordcycle():
|
||||||
assert d.sched.getCard().ord == 1
|
assert d.sched.getCard().ord == 1
|
||||||
assert d.sched.getCard().ord == 2
|
assert d.sched.getCard().ord == 2
|
||||||
|
|
||||||
|
|
||||||
def test_counts_idx():
|
def test_counts_idx():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (1, 0, 0)
|
assert d.sched.counts() == (1, 0, 0)
|
||||||
|
@ -832,10 +867,11 @@ def test_counts_idx():
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert d.sched.counts() == (0, 2, 0)
|
assert d.sched.counts() == (0, 2, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_repCounts():
|
def test_repCounts():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
# lrnReps should be accurate on pass/fail
|
# lrnReps should be accurate on pass/fail
|
||||||
|
@ -853,7 +889,7 @@ def test_repCounts():
|
||||||
d.sched.answerCard(d.sched.getCard(), 2)
|
d.sched.answerCard(d.sched.getCard(), 2)
|
||||||
assert d.sched.counts() == (0, 0, 0)
|
assert d.sched.counts() == (0, 0, 0)
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "two"
|
f["Front"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
# initial pass should be correct too
|
# initial pass should be correct too
|
||||||
|
@ -865,14 +901,14 @@ def test_repCounts():
|
||||||
assert d.sched.counts() == (0, 0, 0)
|
assert d.sched.counts() == (0, 0, 0)
|
||||||
# immediate graduate should work
|
# immediate graduate should work
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "three"
|
f["Front"] = "three"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
d.sched.answerCard(d.sched.getCard(), 3)
|
d.sched.answerCard(d.sched.getCard(), 3)
|
||||||
assert d.sched.counts() == (0, 0, 0)
|
assert d.sched.counts() == (0, 0, 0)
|
||||||
# and failing a review should too
|
# and failing a review should too
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "three"
|
f["Front"] = "three"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
c.type = 2
|
c.type = 2
|
||||||
|
@ -884,12 +920,13 @@ def test_repCounts():
|
||||||
d.sched.answerCard(d.sched.getCard(), 1)
|
d.sched.answerCard(d.sched.getCard(), 1)
|
||||||
assert d.sched.counts() == (0, 1, 0)
|
assert d.sched.counts() == (0, 1, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_timing():
|
def test_timing():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a few review cards, due today
|
# add a few review cards, due today
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "num"+str(i)
|
f["Front"] = "num" + str(i)
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
c.type = 2
|
c.type = 2
|
||||||
|
@ -900,7 +937,7 @@ def test_timing():
|
||||||
d.reset()
|
d.reset()
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
# set a a fail delay of 1 second so we don't have to wait
|
# 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)
|
d.sched.answerCard(c, 1)
|
||||||
# the next card should be another review
|
# the next card should be another review
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
|
@ -910,11 +947,12 @@ def test_timing():
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
assert c.queue == 1
|
assert c.queue == 1
|
||||||
|
|
||||||
|
|
||||||
def test_collapse():
|
def test_collapse():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
# test collapsing
|
# test collapsing
|
||||||
|
@ -924,16 +962,17 @@ def test_collapse():
|
||||||
d.sched.answerCard(c, 3)
|
d.sched.answerCard(c, 3)
|
||||||
assert not d.sched.getCard()
|
assert not d.sched.getCard()
|
||||||
|
|
||||||
|
|
||||||
def test_deckDue():
|
def test_deckDue():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note with default deck
|
# add a note with default deck
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# and one that's a child
|
# and one that's a child
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "two"
|
f["Front"] = "two"
|
||||||
default1 = f.model()['did'] = d.decks.id("Default::1")
|
default1 = f.model()["did"] = d.decks.id("Default::1")
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# make it a review card
|
# make it a review card
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
|
@ -942,13 +981,13 @@ def test_deckDue():
|
||||||
c.flush()
|
c.flush()
|
||||||
# add one more with a new deck
|
# add one more with a new deck
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "two"
|
f["Front"] = "two"
|
||||||
foobar = f.model()['did'] = d.decks.id("foo::bar")
|
foobar = f.model()["did"] = d.decks.id("foo::bar")
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# and one that's a sibling
|
# and one that's a sibling
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "three"
|
f["Front"] = "three"
|
||||||
foobaz = f.model()['did'] = d.decks.id("foo::baz")
|
foobaz = f.model()["did"] = d.decks.id("foo::baz")
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
assert len(d.decks.decks) == 5
|
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][2] == 1
|
||||||
assert tree[0][5][0][4] == 0
|
assert tree[0][5][0][4] == 0
|
||||||
# code should not fail if a card has an invalid deck
|
# 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.deckDueList()
|
||||||
d.sched.deckDueTree()
|
d.sched.deckDueTree()
|
||||||
|
|
||||||
|
|
||||||
def test_deckTree():
|
def test_deckTree():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
d.decks.id("new::b::c")
|
d.decks.id("new::b::c")
|
||||||
|
@ -983,75 +1024,80 @@ def test_deckTree():
|
||||||
names.remove("new")
|
names.remove("new")
|
||||||
assert "new" not in names
|
assert "new" not in names
|
||||||
|
|
||||||
|
|
||||||
def test_deckFlow():
|
def test_deckFlow():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note with default deck
|
# add a note with default deck
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# and one that's a child
|
# and one that's a child
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "two"
|
f["Front"] = "two"
|
||||||
default1 = f.model()['did'] = d.decks.id("Default::2")
|
default1 = f.model()["did"] = d.decks.id("Default::2")
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# and another that's higher up
|
# and another that's higher up
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "three"
|
f["Front"] = "three"
|
||||||
default1 = f.model()['did'] = d.decks.id("Default::1")
|
default1 = f.model()["did"] = d.decks.id("Default::1")
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# should get top level one first, then ::1, then ::2
|
# should get top level one first, then ::1, then ::2
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (3,0,0)
|
assert d.sched.counts() == (3, 0, 0)
|
||||||
for i in "one", "three", "two":
|
for i in "one", "three", "two":
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
assert c.note()['Front'] == i
|
assert c.note()["Front"] == i
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
|
|
||||||
|
|
||||||
def test_reorder():
|
def test_reorder():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note with default deck
|
# add a note with default deck
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
f2 = d.newNote()
|
f2 = d.newNote()
|
||||||
f2['Front'] = "two"
|
f2["Front"] = "two"
|
||||||
d.addNote(f2)
|
d.addNote(f2)
|
||||||
assert f2.cards()[0].due == 2
|
assert f2.cards()[0].due == 2
|
||||||
found=False
|
found = False
|
||||||
# 50/50 chance of being reordered
|
# 50/50 chance of being reordered
|
||||||
for i in range(20):
|
for i in range(20):
|
||||||
d.sched.randomizeCards(1)
|
d.sched.randomizeCards(1)
|
||||||
if f.cards()[0].due != f.id:
|
if f.cards()[0].due != f.id:
|
||||||
found=True
|
found = True
|
||||||
break
|
break
|
||||||
assert found
|
assert found
|
||||||
d.sched.orderCards(1)
|
d.sched.orderCards(1)
|
||||||
assert f.cards()[0].due == 1
|
assert f.cards()[0].due == 1
|
||||||
# shifting
|
# shifting
|
||||||
f3 = d.newNote()
|
f3 = d.newNote()
|
||||||
f3['Front'] = "three"
|
f3["Front"] = "three"
|
||||||
d.addNote(f3)
|
d.addNote(f3)
|
||||||
f4 = d.newNote()
|
f4 = d.newNote()
|
||||||
f4['Front'] = "four"
|
f4["Front"] = "four"
|
||||||
d.addNote(f4)
|
d.addNote(f4)
|
||||||
assert f.cards()[0].due == 1
|
assert f.cards()[0].due == 1
|
||||||
assert f2.cards()[0].due == 2
|
assert f2.cards()[0].due == 2
|
||||||
assert f3.cards()[0].due == 3
|
assert f3.cards()[0].due == 3
|
||||||
assert f4.cards()[0].due == 4
|
assert f4.cards()[0].due == 4
|
||||||
d.sched.sortCards([
|
d.sched.sortCards([f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
|
||||||
f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
|
|
||||||
assert f.cards()[0].due == 3
|
assert f.cards()[0].due == 3
|
||||||
assert f2.cards()[0].due == 4
|
assert f2.cards()[0].due == 4
|
||||||
assert f3.cards()[0].due == 1
|
assert f3.cards()[0].due == 1
|
||||||
assert f4.cards()[0].due == 2
|
assert f4.cards()[0].due == 2
|
||||||
|
|
||||||
|
|
||||||
def test_forget():
|
def test_forget():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
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()
|
c.flush()
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (0, 0, 1)
|
assert d.sched.counts() == (0, 0, 1)
|
||||||
|
@ -1059,10 +1105,11 @@ def test_forget():
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (1, 0, 0)
|
assert d.sched.counts() == (1, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_resched():
|
def test_resched():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
d.sched.reschedCards([c.id], 0, 0)
|
d.sched.reschedCards([c.id], 0, 0)
|
||||||
|
@ -1072,14 +1119,15 @@ def test_resched():
|
||||||
assert c.queue == c.type == 2
|
assert c.queue == c.type == 2
|
||||||
d.sched.reschedCards([c.id], 1, 1)
|
d.sched.reschedCards([c.id], 1, 1)
|
||||||
c.load()
|
c.load()
|
||||||
assert c.due == d.sched.today+1
|
assert c.due == d.sched.today + 1
|
||||||
assert c.ivl == +1
|
assert c.ivl == +1
|
||||||
|
|
||||||
|
|
||||||
def test_norelearn():
|
def test_norelearn():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
c.type = 2
|
c.type = 2
|
||||||
|
@ -1093,13 +1141,15 @@ def test_norelearn():
|
||||||
c.flush()
|
c.flush()
|
||||||
d.reset()
|
d.reset()
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
d.sched._cardConf(c)['lapse']['delays'] = []
|
d.sched._cardConf(c)["lapse"]["delays"] = []
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
|
|
||||||
|
|
||||||
def test_failmult():
|
def test_failmult():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
c.type = 2
|
c.type = 2
|
||||||
|
@ -1111,7 +1161,7 @@ def test_failmult():
|
||||||
c.lapses = 1
|
c.lapses = 1
|
||||||
c.startTimer()
|
c.startTimer()
|
||||||
c.flush()
|
c.flush()
|
||||||
d.sched._cardConf(c)['lapse']['mult'] = 0.5
|
d.sched._cardConf(c)["lapse"]["mult"] = 0.5
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert c.ivl == 50
|
assert c.ivl == 50
|
||||||
|
|
|
@ -1,34 +1,49 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import time
|
|
||||||
import copy
|
import copy
|
||||||
|
import time
|
||||||
|
|
||||||
from anki.consts import STARTING_FACTOR
|
from anki.consts import STARTING_FACTOR
|
||||||
from tests.shared import getEmptyCol
|
|
||||||
from anki.utils import intTime
|
|
||||||
from anki.hooks import addHook
|
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():
|
def test_clock():
|
||||||
d = getEmptyCol()
|
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.")
|
raise Exception("Unit tests will fail around the day rollover.")
|
||||||
|
|
||||||
|
|
||||||
def checkRevIvl(d, c, targetIvl):
|
def checkRevIvl(d, c, targetIvl):
|
||||||
min, max = d.sched._fuzzIvlRange(targetIvl)
|
min, max = d.sched._fuzzIvlRange(targetIvl)
|
||||||
return min <= c.ivl <= max
|
return min <= c.ivl <= max
|
||||||
|
|
||||||
|
|
||||||
def test_basics():
|
def test_basics():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
d.reset()
|
d.reset()
|
||||||
assert not d.sched.getCard()
|
assert not d.sched.getCard()
|
||||||
|
|
||||||
|
|
||||||
def test_new():
|
def test_new():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.newCount == 0
|
assert d.sched.newCount == 0
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.newCount == 1
|
assert d.sched.newCount == 1
|
||||||
|
@ -66,15 +81,16 @@ def test_new():
|
||||||
# assert qs[n] in c.q()
|
# assert qs[n] in c.q()
|
||||||
# d.sched.answerCard(c, 2)
|
# d.sched.answerCard(c, 2)
|
||||||
|
|
||||||
|
|
||||||
def test_newLimits():
|
def test_newLimits():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add some notes
|
# add some notes
|
||||||
g2 = d.decks.id("Default::foo")
|
g2 = d.decks.id("Default::foo")
|
||||||
for i in range(30):
|
for i in range(30):
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = str(i)
|
f["Front"] = str(i)
|
||||||
if i > 4:
|
if i > 4:
|
||||||
f.model()['did'] = g2
|
f.model()["did"] = g2
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# give the child deck a different configuration
|
# give the child deck a different configuration
|
||||||
c2 = d.decks.confId("new conf")
|
c2 = d.decks.confId("new conf")
|
||||||
|
@ -87,33 +103,36 @@ def test_newLimits():
|
||||||
assert c.did == 1
|
assert c.did == 1
|
||||||
# limit the parent to 10 cards, meaning we get 10 in total
|
# limit the parent to 10 cards, meaning we get 10 in total
|
||||||
conf1 = d.decks.confForDid(1)
|
conf1 = d.decks.confForDid(1)
|
||||||
conf1['new']['perDay'] = 10
|
conf1["new"]["perDay"] = 10
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.newCount == 10
|
assert d.sched.newCount == 10
|
||||||
# if we limit child to 4, we should get 9
|
# if we limit child to 4, we should get 9
|
||||||
conf2 = d.decks.confForDid(g2)
|
conf2 = d.decks.confForDid(g2)
|
||||||
conf2['new']['perDay'] = 4
|
conf2["new"]["perDay"] = 4
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.newCount == 9
|
assert d.sched.newCount == 9
|
||||||
|
|
||||||
|
|
||||||
def test_newBoxes():
|
def test_newBoxes():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
c = d.sched.getCard()
|
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)
|
d.sched.answerCard(c, 2)
|
||||||
# should handle gracefully
|
# should handle gracefully
|
||||||
d.sched._cardConf(c)['new']['delays'] = [1]
|
d.sched._cardConf(c)["new"]["delays"] = [1]
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
|
|
||||||
|
|
||||||
def test_learn():
|
def test_learn():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
f = d.addNote(f)
|
f = d.addNote(f)
|
||||||
# set as a learn card and rebuild queues
|
# set as a learn card and rebuild queues
|
||||||
d.db.execute("update cards set queue=0, type=0")
|
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
|
# sched.getCard should return it, since it's due in the past
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
assert c
|
assert c
|
||||||
d.sched._cardConf(c)['new']['delays'] = [0.5, 3, 10]
|
d.sched._cardConf(c)["new"]["delays"] = [0.5, 3, 10]
|
||||||
# fail it
|
# fail it
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
# it should have three reps left to graduation
|
# 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
|
# it should by due in 30 seconds
|
||||||
t = round(c.due - time.time())
|
t = round(c.due - time.time())
|
||||||
assert t >= 25 and t <= 40
|
assert t >= 25 and t <= 40
|
||||||
|
@ -134,9 +153,9 @@ def test_learn():
|
||||||
d.sched.answerCard(c, 3)
|
d.sched.answerCard(c, 3)
|
||||||
# it should by due in 3 minutes
|
# it should by due in 3 minutes
|
||||||
dueIn = c.due - time.time()
|
dueIn = c.due - time.time()
|
||||||
assert 179 <= dueIn <= 180*1.25
|
assert 179 <= dueIn <= 180 * 1.25
|
||||||
assert c.left%1000 == 2
|
assert c.left % 1000 == 2
|
||||||
assert c.left//1000 == 2
|
assert c.left // 1000 == 2
|
||||||
# check log is accurate
|
# check log is accurate
|
||||||
log = d.db.first("select * from revlog order by id desc")
|
log = d.db.first("select * from revlog order by id desc")
|
||||||
assert log[3] == 3
|
assert log[3] == 3
|
||||||
|
@ -146,9 +165,9 @@ def test_learn():
|
||||||
d.sched.answerCard(c, 3)
|
d.sched.answerCard(c, 3)
|
||||||
# it should by due in 10 minutes
|
# it should by due in 10 minutes
|
||||||
dueIn = c.due - time.time()
|
dueIn = c.due - time.time()
|
||||||
assert 599 <= dueIn <= 600*1.25
|
assert 599 <= dueIn <= 600 * 1.25
|
||||||
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
|
# the next pass should graduate the card
|
||||||
assert c.queue == 1
|
assert c.queue == 1
|
||||||
assert c.type == 1
|
assert c.type == 1
|
||||||
|
@ -156,7 +175,7 @@ def test_learn():
|
||||||
assert c.queue == 2
|
assert c.queue == 2
|
||||||
assert c.type == 2
|
assert c.type == 2
|
||||||
# should be due tomorrow, with an interval of 1
|
# 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
|
assert c.ivl == 1
|
||||||
# or normal removal
|
# or normal removal
|
||||||
c.type = 0
|
c.type = 0
|
||||||
|
@ -168,10 +187,11 @@ def test_learn():
|
||||||
# revlog should have been updated each time
|
# revlog should have been updated each time
|
||||||
assert d.db.scalar("select count() from revlog where type = 0") == 5
|
assert d.db.scalar("select count() from revlog where type = 0") == 5
|
||||||
|
|
||||||
|
|
||||||
def test_relearn():
|
def test_relearn():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
c.ivl = 100
|
c.ivl = 100
|
||||||
|
@ -193,10 +213,11 @@ def test_relearn():
|
||||||
assert c.ivl == 2
|
assert c.ivl == 2
|
||||||
assert c.due == d.sched.today + c.ivl
|
assert c.due == d.sched.today + c.ivl
|
||||||
|
|
||||||
|
|
||||||
def test_relearn_no_steps():
|
def test_relearn_no_steps():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
c.ivl = 100
|
c.ivl = 100
|
||||||
|
@ -205,7 +226,7 @@ def test_relearn_no_steps():
|
||||||
c.flush()
|
c.flush()
|
||||||
|
|
||||||
conf = d.decks.confForDid(1)
|
conf = d.decks.confForDid(1)
|
||||||
conf['lapse']['delays'] = []
|
conf["lapse"]["delays"] = []
|
||||||
d.decks.save(conf)
|
d.decks.save(conf)
|
||||||
|
|
||||||
# fail the card
|
# fail the card
|
||||||
|
@ -214,14 +235,15 @@ def test_relearn_no_steps():
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert c.type == c.queue == 2
|
assert c.type == c.queue == 2
|
||||||
|
|
||||||
|
|
||||||
def test_learn_collapsed():
|
def test_learn_collapsed():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add 2 notes
|
# add 2 notes
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "1"
|
f["Front"] = "1"
|
||||||
f = d.addNote(f)
|
f = d.addNote(f)
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "2"
|
f["Front"] = "2"
|
||||||
f = d.addNote(f)
|
f = d.addNote(f)
|
||||||
# set as a learn card and rebuild queues
|
# set as a learn card and rebuild queues
|
||||||
d.db.execute("update cards set queue=0, type=0")
|
d.db.execute("update cards set queue=0, type=0")
|
||||||
|
@ -240,27 +262,28 @@ def test_learn_collapsed():
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
assert not c.q().endswith("2")
|
assert not c.q().endswith("2")
|
||||||
|
|
||||||
|
|
||||||
def test_learn_day():
|
def test_learn_day():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
f = d.addNote(f)
|
f = d.addNote(f)
|
||||||
d.sched.reset()
|
d.sched.reset()
|
||||||
c = d.sched.getCard()
|
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
|
# pass it
|
||||||
d.sched.answerCard(c, 3)
|
d.sched.answerCard(c, 3)
|
||||||
# two reps to graduate, 1 more today
|
# two reps to graduate, 1 more today
|
||||||
assert c.left%1000 == 3
|
assert c.left % 1000 == 3
|
||||||
assert c.left//1000 == 1
|
assert c.left // 1000 == 1
|
||||||
assert d.sched.counts() == (0, 1, 0)
|
assert d.sched.counts() == (0, 1, 0)
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
ni = d.sched.nextIvl
|
ni = d.sched.nextIvl
|
||||||
assert ni(c, 3) == 86400
|
assert ni(c, 3) == 86400
|
||||||
# answering it will place it in queue 3
|
# answering it will place it in queue 3
|
||||||
d.sched.answerCard(c, 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 c.queue == 3
|
||||||
assert not d.sched.getCard()
|
assert not d.sched.getCard()
|
||||||
# for testing, move it back a day
|
# for testing, move it back a day
|
||||||
|
@ -270,7 +293,7 @@ def test_learn_day():
|
||||||
assert d.sched.counts() == (0, 1, 0)
|
assert d.sched.counts() == (0, 1, 0)
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
# nextIvl should work
|
# 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
|
# if we fail it, it should be back in the correct queue
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert c.queue == 1
|
assert c.queue == 1
|
||||||
|
@ -292,17 +315,19 @@ def test_learn_day():
|
||||||
c.flush()
|
c.flush()
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (0, 0, 1)
|
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()
|
c = d.sched.getCard()
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert c.queue == 3
|
assert c.queue == 3
|
||||||
assert d.sched.counts() == (0, 0, 0)
|
assert d.sched.counts() == (0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_reviews():
|
def test_reviews():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# set the card up as a review card, due 8 days ago
|
# set the card up as a review card, due 8 days ago
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
|
@ -359,8 +384,10 @@ def test_reviews():
|
||||||
c.flush()
|
c.flush()
|
||||||
# steup hook
|
# steup hook
|
||||||
hooked = []
|
hooked = []
|
||||||
|
|
||||||
def onLeech(card):
|
def onLeech(card):
|
||||||
hooked.append(1)
|
hooked.append(1)
|
||||||
|
|
||||||
addHook("leech", onLeech)
|
addHook("leech", onLeech)
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert hooked
|
assert hooked
|
||||||
|
@ -368,6 +395,7 @@ def test_reviews():
|
||||||
c.load()
|
c.load()
|
||||||
assert c.queue == -1
|
assert c.queue == -1
|
||||||
|
|
||||||
|
|
||||||
def test_review_limits():
|
def test_review_limits():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
|
|
||||||
|
@ -377,21 +405,22 @@ def test_review_limits():
|
||||||
pconf = d.decks.getConf(d.decks.confId("parentConf"))
|
pconf = d.decks.getConf(d.decks.confId("parentConf"))
|
||||||
cconf = d.decks.getConf(d.decks.confId("childConf"))
|
cconf = d.decks.getConf(d.decks.confId("childConf"))
|
||||||
|
|
||||||
pconf['rev']['perDay'] = 5
|
pconf["rev"]["perDay"] = 5
|
||||||
d.decks.updateConf(pconf)
|
d.decks.updateConf(pconf)
|
||||||
d.decks.setConf(parent, pconf['id'])
|
d.decks.setConf(parent, pconf["id"])
|
||||||
cconf['rev']['perDay'] = 10
|
cconf["rev"]["perDay"] = 10
|
||||||
d.decks.updateConf(cconf)
|
d.decks.updateConf(cconf)
|
||||||
d.decks.setConf(child, cconf['id'])
|
d.decks.setConf(child, cconf["id"])
|
||||||
|
|
||||||
m = d.models.current()
|
m = d.models.current()
|
||||||
m['did'] = child['id']
|
m["did"] = child["id"]
|
||||||
d.models.save(m, updateReqs=False)
|
d.models.save(m, updateReqs=False)
|
||||||
|
|
||||||
# add some cards
|
# add some cards
|
||||||
for i in range(20):
|
for i in range(20):
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
|
||||||
# make them reviews
|
# make them reviews
|
||||||
|
@ -402,11 +431,11 @@ def test_review_limits():
|
||||||
|
|
||||||
tree = d.sched.deckDueTree()
|
tree = d.sched.deckDueTree()
|
||||||
# (('Default', 1, 0, 0, 0, ()), ('parent', 1514457677462, 5, 0, 0, (('child', 1514457677463, 5, 0, 0, ()),)))
|
# (('Default', 1, 0, 0, 0, ()), ('parent', 1514457677462, 5, 0, 0, (('child', 1514457677463, 5, 0, 0, ()),)))
|
||||||
assert tree[1][2] == 5 # parent
|
assert tree[1][2] == 5 # parent
|
||||||
assert tree[1][5][0][2] == 5 # child
|
assert tree[1][5][0][2] == 5 # child
|
||||||
|
|
||||||
# .counts() should match
|
# .counts() should match
|
||||||
d.decks.select(child['id'])
|
d.decks.select(child["id"])
|
||||||
d.sched.reset()
|
d.sched.reset()
|
||||||
assert d.sched.counts() == (0, 0, 5)
|
assert d.sched.counts() == (0, 0, 5)
|
||||||
|
|
||||||
|
@ -416,24 +445,25 @@ def test_review_limits():
|
||||||
assert d.sched.counts() == (0, 0, 4)
|
assert d.sched.counts() == (0, 0, 4)
|
||||||
|
|
||||||
tree = d.sched.deckDueTree()
|
tree = d.sched.deckDueTree()
|
||||||
assert tree[1][2] == 4 # parent
|
assert tree[1][2] == 4 # parent
|
||||||
assert tree[1][5][0][2] == 4 # child
|
assert tree[1][5][0][2] == 4 # child
|
||||||
|
|
||||||
# switch limits
|
# switch limits
|
||||||
d.decks.setConf(parent, cconf['id'])
|
d.decks.setConf(parent, cconf["id"])
|
||||||
d.decks.setConf(child, pconf['id'])
|
d.decks.setConf(child, pconf["id"])
|
||||||
d.decks.select(parent['id'])
|
d.decks.select(parent["id"])
|
||||||
d.sched.reset()
|
d.sched.reset()
|
||||||
|
|
||||||
# child limits do not affect the parent
|
# child limits do not affect the parent
|
||||||
tree = d.sched.deckDueTree()
|
tree = d.sched.deckDueTree()
|
||||||
assert tree[1][2] == 9 # parent
|
assert tree[1][2] == 9 # parent
|
||||||
assert tree[1][5][0][2] == 4 # child
|
assert tree[1][5][0][2] == 4 # child
|
||||||
|
|
||||||
|
|
||||||
def test_button_spacing():
|
def test_button_spacing():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# 1 day ivl review card due now
|
# 1 day ivl review card due now
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
|
@ -452,16 +482,17 @@ def test_button_spacing():
|
||||||
|
|
||||||
# if hard factor is <= 1, then hard may not increase
|
# if hard factor is <= 1, then hard may not increase
|
||||||
conf = d.decks.confForDid(1)
|
conf = d.decks.confForDid(1)
|
||||||
conf['rev']['hardFactor'] = 1
|
conf["rev"]["hardFactor"] = 1
|
||||||
assert ni(c, 2) == "1 day"
|
assert ni(c, 2) == "1 day"
|
||||||
|
|
||||||
|
|
||||||
def test_overdue_lapse():
|
def test_overdue_lapse():
|
||||||
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2
|
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2
|
||||||
return
|
return
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# simulate a review that was lapsed and is now due for its normal review
|
# simulate a review that was lapsed and is now due for its normal review
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
|
@ -490,13 +521,15 @@ def test_overdue_lapse():
|
||||||
d.sched.reset()
|
d.sched.reset()
|
||||||
assert d.sched.counts() == (0, 0, 1)
|
assert d.sched.counts() == (0, 0, 1)
|
||||||
|
|
||||||
|
|
||||||
def test_finished():
|
def test_finished():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# nothing due
|
# nothing due
|
||||||
assert "Congratulations" in d.sched.finishedMsg()
|
assert "Congratulations" in d.sched.finishedMsg()
|
||||||
assert "limit" not in d.sched.finishedMsg()
|
assert "limit" not in d.sched.finishedMsg()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# have a new card
|
# have a new card
|
||||||
assert "new cards available" in d.sched.finishedMsg()
|
assert "new cards available" in d.sched.finishedMsg()
|
||||||
|
@ -509,47 +542,49 @@ def test_finished():
|
||||||
assert "Congratulations" in d.sched.finishedMsg()
|
assert "Congratulations" in d.sched.finishedMsg()
|
||||||
assert "limit" not in d.sched.finishedMsg()
|
assert "limit" not in d.sched.finishedMsg()
|
||||||
|
|
||||||
|
|
||||||
def test_nextIvl():
|
def test_nextIvl():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
conf = d.decks.confForDid(1)
|
conf = d.decks.confForDid(1)
|
||||||
conf['new']['delays'] = [0.5, 3, 10]
|
conf["new"]["delays"] = [0.5, 3, 10]
|
||||||
conf['lapse']['delays'] = [1, 5, 9]
|
conf["lapse"]["delays"] = [1, 5, 9]
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
# new cards
|
# new cards
|
||||||
##################################################
|
##################################################
|
||||||
ni = d.sched.nextIvl
|
ni = d.sched.nextIvl
|
||||||
assert ni(c, 1) == 30
|
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, 3) == 180
|
||||||
assert ni(c, 4) == 4*86400
|
assert ni(c, 4) == 4 * 86400
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
# cards in learning
|
# cards in learning
|
||||||
##################################################
|
##################################################
|
||||||
assert ni(c, 1) == 30
|
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, 3) == 180
|
||||||
assert ni(c, 4) == 4*86400
|
assert ni(c, 4) == 4 * 86400
|
||||||
d.sched.answerCard(c, 3)
|
d.sched.answerCard(c, 3)
|
||||||
assert ni(c, 1) == 30
|
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, 3) == 600
|
||||||
assert ni(c, 4) == 4*86400
|
assert ni(c, 4) == 4 * 86400
|
||||||
d.sched.answerCard(c, 3)
|
d.sched.answerCard(c, 3)
|
||||||
# normal graduation is tomorrow
|
# normal graduation is tomorrow
|
||||||
assert ni(c, 3) == 1*86400
|
assert ni(c, 3) == 1 * 86400
|
||||||
assert ni(c, 4) == 4*86400
|
assert ni(c, 4) == 4 * 86400
|
||||||
# lapsed cards
|
# lapsed cards
|
||||||
##################################################
|
##################################################
|
||||||
c.type = 2
|
c.type = 2
|
||||||
c.ivl = 100
|
c.ivl = 100
|
||||||
c.factor = STARTING_FACTOR
|
c.factor = STARTING_FACTOR
|
||||||
assert ni(c, 1) == 60
|
assert ni(c, 1) == 60
|
||||||
assert ni(c, 3) == 100*86400
|
assert ni(c, 3) == 100 * 86400
|
||||||
assert ni(c, 4) == 101*86400
|
assert ni(c, 4) == 101 * 86400
|
||||||
# review cards
|
# review cards
|
||||||
##################################################
|
##################################################
|
||||||
c.queue = 2
|
c.queue = 2
|
||||||
|
@ -558,8 +593,8 @@ def test_nextIvl():
|
||||||
# failing it should put it at 60s
|
# failing it should put it at 60s
|
||||||
assert ni(c, 1) == 60
|
assert ni(c, 1) == 60
|
||||||
# or 1 day if relearn is false
|
# or 1 day if relearn is false
|
||||||
d.sched._cardConf(c)['lapse']['delays']=[]
|
d.sched._cardConf(c)["lapse"]["delays"] = []
|
||||||
assert ni(c, 1) == 1*86400
|
assert ni(c, 1) == 1 * 86400
|
||||||
# (* 100 1.2 86400)10368000.0
|
# (* 100 1.2 86400)10368000.0
|
||||||
assert ni(c, 2) == 10368000
|
assert ni(c, 2) == 10368000
|
||||||
# (* 100 2.5 86400)21600000.0
|
# (* 100 2.5 86400)21600000.0
|
||||||
|
@ -568,14 +603,15 @@ def test_nextIvl():
|
||||||
assert ni(c, 4) == 28080000
|
assert ni(c, 4) == 28080000
|
||||||
assert d.sched.nextIvlStr(c, 4) == "10.8 months"
|
assert d.sched.nextIvlStr(c, 4) == "10.8 months"
|
||||||
|
|
||||||
|
|
||||||
def test_bury():
|
def test_bury():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "two"
|
f["Front"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c2 = f.cards()[0]
|
c2 = f.cards()[0]
|
||||||
# burying
|
# burying
|
||||||
|
@ -590,11 +626,14 @@ def test_bury():
|
||||||
assert not d.sched.getCard()
|
assert not d.sched.getCard()
|
||||||
|
|
||||||
d.sched.unburyCardsForDeck(type="manual")
|
d.sched.unburyCardsForDeck(type="manual")
|
||||||
c.load(); assert c.queue == 0
|
c.load()
|
||||||
c2.load(); assert c2.queue == -2
|
assert c.queue == 0
|
||||||
|
c2.load()
|
||||||
|
assert c2.queue == -2
|
||||||
|
|
||||||
d.sched.unburyCardsForDeck(type="siblings")
|
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.buryCards([c.id, c2.id])
|
||||||
d.sched.unburyCardsForDeck(type="all")
|
d.sched.unburyCardsForDeck(type="all")
|
||||||
|
@ -603,10 +642,11 @@ def test_bury():
|
||||||
|
|
||||||
assert d.sched.counts() == (2, 0, 0)
|
assert d.sched.counts() == (2, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_suspend():
|
def test_suspend():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
# suspending
|
# suspending
|
||||||
|
@ -620,7 +660,11 @@ def test_suspend():
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.getCard()
|
assert d.sched.getCard()
|
||||||
# should cope with rev cards being relearnt
|
# 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()
|
d.reset()
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
|
@ -648,10 +692,11 @@ def test_suspend():
|
||||||
assert c.did != 1
|
assert c.did != 1
|
||||||
assert c.odue == 1
|
assert c.odue == 1
|
||||||
|
|
||||||
|
|
||||||
def test_filt_reviewing_early_normal():
|
def test_filt_reviewing_early_normal():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
c.ivl = 100
|
c.ivl = 100
|
||||||
|
@ -663,7 +708,7 @@ def test_filt_reviewing_early_normal():
|
||||||
c.startTimer()
|
c.startTimer()
|
||||||
c.flush()
|
c.flush()
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (0,0,0)
|
assert d.sched.counts() == (0, 0, 0)
|
||||||
# create a dynamic deck and refresh it
|
# create a dynamic deck and refresh it
|
||||||
did = d.decks.newDyn("Cram")
|
did = d.decks.newDyn("Cram")
|
||||||
d.sched.rebuildDyn(did)
|
d.sched.rebuildDyn(did)
|
||||||
|
@ -671,14 +716,14 @@ def test_filt_reviewing_early_normal():
|
||||||
# should appear as normal in the deck list
|
# should appear as normal in the deck list
|
||||||
assert sorted(d.sched.deckDueList())[0][2] == 1
|
assert sorted(d.sched.deckDueList())[0][2] == 1
|
||||||
# and should appear in the counts
|
# 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
|
# grab it and check estimates
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
assert d.sched.answerButtons(c) == 4
|
assert d.sched.answerButtons(c) == 4
|
||||||
assert d.sched.nextIvl(c, 1) == 600
|
assert d.sched.nextIvl(c, 1) == 600
|
||||||
assert d.sched.nextIvl(c, 2) == int(75*1.2)*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, 3) == int(75 * 2.5) * 86400
|
||||||
assert d.sched.nextIvl(c, 4) == int(75*2.5*1.15)*86400
|
assert d.sched.nextIvl(c, 4) == int(75 * 2.5 * 1.15) * 86400
|
||||||
|
|
||||||
# answer 'good'
|
# answer 'good'
|
||||||
d.sched.answerCard(c, 3)
|
d.sched.answerCard(c, 3)
|
||||||
|
@ -688,8 +733,7 @@ def test_filt_reviewing_early_normal():
|
||||||
# should not be in learning
|
# should not be in learning
|
||||||
assert c.queue == 2
|
assert c.queue == 2
|
||||||
# should be logged as a cram rep
|
# should be logged as a cram rep
|
||||||
assert d.db.scalar(
|
assert d.db.scalar("select type from revlog order by id desc limit 1") == 3
|
||||||
"select type from revlog order by id desc limit 1") == 3
|
|
||||||
|
|
||||||
# due in 75 days, so it's been waiting 25 days
|
# due in 75 days, so it's been waiting 25 days
|
||||||
c.ivl = 100
|
c.ivl = 100
|
||||||
|
@ -699,20 +743,21 @@ def test_filt_reviewing_early_normal():
|
||||||
d.reset()
|
d.reset()
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
|
|
||||||
assert d.sched.nextIvl(c, 2) == 60*86400
|
assert d.sched.nextIvl(c, 2) == 60 * 86400
|
||||||
assert d.sched.nextIvl(c, 3) == 100*86400
|
assert d.sched.nextIvl(c, 3) == 100 * 86400
|
||||||
assert d.sched.nextIvl(c, 4) == 114*86400
|
assert d.sched.nextIvl(c, 4) == 114 * 86400
|
||||||
|
|
||||||
|
|
||||||
def test_filt_keep_lrn_state():
|
def test_filt_keep_lrn_state():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
|
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
|
|
||||||
# fail the card outside filtered deck
|
# fail the card outside filtered deck
|
||||||
c = d.sched.getCard()
|
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.decks.save()
|
||||||
|
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
|
@ -736,30 +781,31 @@ def test_filt_keep_lrn_state():
|
||||||
# should be able to advance learning steps
|
# should be able to advance learning steps
|
||||||
d.sched.answerCard(c, 3)
|
d.sched.answerCard(c, 3)
|
||||||
# should be due at least an hour in the future
|
# 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
|
# emptying the deck preserves learning state
|
||||||
d.sched.emptyDyn(did)
|
d.sched.emptyDyn(did)
|
||||||
c.load()
|
c.load()
|
||||||
assert c.type == c.queue == 1
|
assert c.type == c.queue == 1
|
||||||
assert c.left == 1001
|
assert c.left == 1001
|
||||||
assert c.due - intTime() > 60*60
|
assert c.due - intTime() > 60 * 60
|
||||||
|
|
||||||
|
|
||||||
def test_preview():
|
def test_preview():
|
||||||
# add cards
|
# add cards
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
orig = copy.copy(c)
|
orig = copy.copy(c)
|
||||||
f2 = d.newNote()
|
f2 = d.newNote()
|
||||||
f2['Front'] = "two"
|
f2["Front"] = "two"
|
||||||
d.addNote(f2)
|
d.addNote(f2)
|
||||||
# cram deck
|
# cram deck
|
||||||
did = d.decks.newDyn("Cram")
|
did = d.decks.newDyn("Cram")
|
||||||
cram = d.decks.get(did)
|
cram = d.decks.get(did)
|
||||||
cram['resched'] = False
|
cram["resched"] = False
|
||||||
d.sched.rebuildDyn(did)
|
d.sched.rebuildDyn(did)
|
||||||
d.reset()
|
d.reset()
|
||||||
# grab the first card
|
# grab the first card
|
||||||
|
@ -793,22 +839,25 @@ def test_preview():
|
||||||
assert c.reps == 0
|
assert c.reps == 0
|
||||||
assert c.type == 0
|
assert c.type == 0
|
||||||
|
|
||||||
|
|
||||||
def test_ordcycle():
|
def test_ordcycle():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add two more templates and set second active
|
# 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 = mm.newTemplate("Reverse")
|
||||||
t['qfmt'] = "{{Back}}"
|
t["qfmt"] = "{{Back}}"
|
||||||
t['afmt'] = "{{Front}}"
|
t["afmt"] = "{{Front}}"
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
t = mm.newTemplate("f2")
|
t = mm.newTemplate("f2")
|
||||||
t['qfmt'] = "{{Front}}"
|
t["qfmt"] = "{{Front}}"
|
||||||
t['afmt'] = "{{Back}}"
|
t["afmt"] = "{{Back}}"
|
||||||
mm.addTemplate(m, t)
|
mm.addTemplate(m, t)
|
||||||
mm.save(m)
|
mm.save(m)
|
||||||
# create a new note; it should have 3 cards
|
# create a new note; it should have 3 cards
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "1"; f['Back'] = "1"
|
f["Front"] = "1"
|
||||||
|
f["Back"] = "1"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
assert d.cardCount() == 3
|
assert d.cardCount() == 3
|
||||||
d.reset()
|
d.reset()
|
||||||
|
@ -817,10 +866,12 @@ def test_ordcycle():
|
||||||
assert d.sched.getCard().ord == 1
|
assert d.sched.getCard().ord == 1
|
||||||
assert d.sched.getCard().ord == 2
|
assert d.sched.getCard().ord == 2
|
||||||
|
|
||||||
|
|
||||||
def test_counts_idx():
|
def test_counts_idx():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (1, 0, 0)
|
assert d.sched.counts() == (1, 0, 0)
|
||||||
|
@ -839,10 +890,11 @@ def test_counts_idx():
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert d.sched.counts() == (0, 1, 0)
|
assert d.sched.counts() == (0, 1, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_repCounts():
|
def test_repCounts():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
# lrnReps should be accurate on pass/fail
|
# lrnReps should be accurate on pass/fail
|
||||||
|
@ -860,7 +912,7 @@ def test_repCounts():
|
||||||
d.sched.answerCard(d.sched.getCard(), 3)
|
d.sched.answerCard(d.sched.getCard(), 3)
|
||||||
assert d.sched.counts() == (0, 0, 0)
|
assert d.sched.counts() == (0, 0, 0)
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "two"
|
f["Front"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
# initial pass should be correct too
|
# initial pass should be correct too
|
||||||
|
@ -872,14 +924,14 @@ def test_repCounts():
|
||||||
assert d.sched.counts() == (0, 0, 0)
|
assert d.sched.counts() == (0, 0, 0)
|
||||||
# immediate graduate should work
|
# immediate graduate should work
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "three"
|
f["Front"] = "three"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
d.sched.answerCard(d.sched.getCard(), 4)
|
d.sched.answerCard(d.sched.getCard(), 4)
|
||||||
assert d.sched.counts() == (0, 0, 0)
|
assert d.sched.counts() == (0, 0, 0)
|
||||||
# and failing a review should too
|
# and failing a review should too
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "three"
|
f["Front"] = "three"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
c.type = 2
|
c.type = 2
|
||||||
|
@ -891,12 +943,13 @@ def test_repCounts():
|
||||||
d.sched.answerCard(d.sched.getCard(), 1)
|
d.sched.answerCard(d.sched.getCard(), 1)
|
||||||
assert d.sched.counts() == (0, 1, 0)
|
assert d.sched.counts() == (0, 1, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_timing():
|
def test_timing():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a few review cards, due today
|
# add a few review cards, due today
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "num"+str(i)
|
f["Front"] = "num" + str(i)
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
c.type = 2
|
c.type = 2
|
||||||
|
@ -917,11 +970,12 @@ def test_timing():
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
assert c.queue == 1
|
assert c.queue == 1
|
||||||
|
|
||||||
|
|
||||||
def test_collapse():
|
def test_collapse():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
# test collapsing
|
# test collapsing
|
||||||
|
@ -931,16 +985,17 @@ def test_collapse():
|
||||||
d.sched.answerCard(c, 4)
|
d.sched.answerCard(c, 4)
|
||||||
assert not d.sched.getCard()
|
assert not d.sched.getCard()
|
||||||
|
|
||||||
|
|
||||||
def test_deckDue():
|
def test_deckDue():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note with default deck
|
# add a note with default deck
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# and one that's a child
|
# and one that's a child
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "two"
|
f["Front"] = "two"
|
||||||
default1 = f.model()['did'] = d.decks.id("Default::1")
|
default1 = f.model()["did"] = d.decks.id("Default::1")
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# make it a review card
|
# make it a review card
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
|
@ -949,13 +1004,13 @@ def test_deckDue():
|
||||||
c.flush()
|
c.flush()
|
||||||
# add one more with a new deck
|
# add one more with a new deck
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "two"
|
f["Front"] = "two"
|
||||||
foobar = f.model()['did'] = d.decks.id("foo::bar")
|
foobar = f.model()["did"] = d.decks.id("foo::bar")
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# and one that's a sibling
|
# and one that's a sibling
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "three"
|
f["Front"] = "three"
|
||||||
foobaz = f.model()['did'] = d.decks.id("foo::baz")
|
foobaz = f.model()["did"] = d.decks.id("foo::baz")
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
assert len(d.decks.decks) == 5
|
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][2] == 1
|
||||||
assert tree[0][5][0][4] == 0
|
assert tree[0][5][0][4] == 0
|
||||||
# code should not fail if a card has an invalid deck
|
# 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.deckDueList()
|
||||||
d.sched.deckDueTree()
|
d.sched.deckDueTree()
|
||||||
|
|
||||||
|
|
||||||
def test_deckTree():
|
def test_deckTree():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
d.decks.id("new::b::c")
|
d.decks.id("new::b::c")
|
||||||
|
@ -990,75 +1047,80 @@ def test_deckTree():
|
||||||
names.remove("new")
|
names.remove("new")
|
||||||
assert "new" not in names
|
assert "new" not in names
|
||||||
|
|
||||||
|
|
||||||
def test_deckFlow():
|
def test_deckFlow():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note with default deck
|
# add a note with default deck
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# and one that's a child
|
# and one that's a child
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "two"
|
f["Front"] = "two"
|
||||||
default1 = f.model()['did'] = d.decks.id("Default::2")
|
default1 = f.model()["did"] = d.decks.id("Default::2")
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# and another that's higher up
|
# and another that's higher up
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "three"
|
f["Front"] = "three"
|
||||||
default1 = f.model()['did'] = d.decks.id("Default::1")
|
default1 = f.model()["did"] = d.decks.id("Default::1")
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
# should get top level one first, then ::1, then ::2
|
# should get top level one first, then ::1, then ::2
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (3,0,0)
|
assert d.sched.counts() == (3, 0, 0)
|
||||||
for i in "one", "three", "two":
|
for i in "one", "three", "two":
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
assert c.note()['Front'] == i
|
assert c.note()["Front"] == i
|
||||||
d.sched.answerCard(c, 3)
|
d.sched.answerCard(c, 3)
|
||||||
|
|
||||||
|
|
||||||
def test_reorder():
|
def test_reorder():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note with default deck
|
# add a note with default deck
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
f2 = d.newNote()
|
f2 = d.newNote()
|
||||||
f2['Front'] = "two"
|
f2["Front"] = "two"
|
||||||
d.addNote(f2)
|
d.addNote(f2)
|
||||||
assert f2.cards()[0].due == 2
|
assert f2.cards()[0].due == 2
|
||||||
found=False
|
found = False
|
||||||
# 50/50 chance of being reordered
|
# 50/50 chance of being reordered
|
||||||
for i in range(20):
|
for i in range(20):
|
||||||
d.sched.randomizeCards(1)
|
d.sched.randomizeCards(1)
|
||||||
if f.cards()[0].due != f.id:
|
if f.cards()[0].due != f.id:
|
||||||
found=True
|
found = True
|
||||||
break
|
break
|
||||||
assert found
|
assert found
|
||||||
d.sched.orderCards(1)
|
d.sched.orderCards(1)
|
||||||
assert f.cards()[0].due == 1
|
assert f.cards()[0].due == 1
|
||||||
# shifting
|
# shifting
|
||||||
f3 = d.newNote()
|
f3 = d.newNote()
|
||||||
f3['Front'] = "three"
|
f3["Front"] = "three"
|
||||||
d.addNote(f3)
|
d.addNote(f3)
|
||||||
f4 = d.newNote()
|
f4 = d.newNote()
|
||||||
f4['Front'] = "four"
|
f4["Front"] = "four"
|
||||||
d.addNote(f4)
|
d.addNote(f4)
|
||||||
assert f.cards()[0].due == 1
|
assert f.cards()[0].due == 1
|
||||||
assert f2.cards()[0].due == 2
|
assert f2.cards()[0].due == 2
|
||||||
assert f3.cards()[0].due == 3
|
assert f3.cards()[0].due == 3
|
||||||
assert f4.cards()[0].due == 4
|
assert f4.cards()[0].due == 4
|
||||||
d.sched.sortCards([
|
d.sched.sortCards([f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
|
||||||
f3.cards()[0].id, f4.cards()[0].id], start=1, shift=True)
|
|
||||||
assert f.cards()[0].due == 3
|
assert f.cards()[0].due == 3
|
||||||
assert f2.cards()[0].due == 4
|
assert f2.cards()[0].due == 4
|
||||||
assert f3.cards()[0].due == 1
|
assert f3.cards()[0].due == 1
|
||||||
assert f4.cards()[0].due == 2
|
assert f4.cards()[0].due == 2
|
||||||
|
|
||||||
|
|
||||||
def test_forget():
|
def test_forget():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
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()
|
c.flush()
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (0, 0, 1)
|
assert d.sched.counts() == (0, 0, 1)
|
||||||
|
@ -1066,10 +1128,11 @@ def test_forget():
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (1, 0, 0)
|
assert d.sched.counts() == (1, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_resched():
|
def test_resched():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
d.sched.reschedCards([c.id], 0, 0)
|
d.sched.reschedCards([c.id], 0, 0)
|
||||||
|
@ -1079,14 +1142,15 @@ def test_resched():
|
||||||
assert c.queue == c.type == 2
|
assert c.queue == c.type == 2
|
||||||
d.sched.reschedCards([c.id], 1, 1)
|
d.sched.reschedCards([c.id], 1, 1)
|
||||||
c.load()
|
c.load()
|
||||||
assert c.due == d.sched.today+1
|
assert c.due == d.sched.today + 1
|
||||||
assert c.ivl == +1
|
assert c.ivl == +1
|
||||||
|
|
||||||
|
|
||||||
def test_norelearn():
|
def test_norelearn():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
# add a note
|
# add a note
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
c.type = 2
|
c.type = 2
|
||||||
|
@ -1100,13 +1164,15 @@ def test_norelearn():
|
||||||
c.flush()
|
c.flush()
|
||||||
d.reset()
|
d.reset()
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
d.sched._cardConf(c)['lapse']['delays'] = []
|
d.sched._cardConf(c)["lapse"]["delays"] = []
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
|
|
||||||
|
|
||||||
def test_failmult():
|
def test_failmult():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
c.type = 2
|
c.type = 2
|
||||||
|
@ -1118,19 +1184,20 @@ def test_failmult():
|
||||||
c.lapses = 1
|
c.lapses = 1
|
||||||
c.startTimer()
|
c.startTimer()
|
||||||
c.flush()
|
c.flush()
|
||||||
d.sched._cardConf(c)['lapse']['mult'] = 0.5
|
d.sched._cardConf(c)["lapse"]["mult"] = 0.5
|
||||||
c = d.sched.getCard()
|
c = d.sched.getCard()
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert c.ivl == 50
|
assert c.ivl == 50
|
||||||
d.sched.answerCard(c, 1)
|
d.sched.answerCard(c, 1)
|
||||||
assert c.ivl == 25
|
assert c.ivl == 25
|
||||||
|
|
||||||
|
|
||||||
def test_moveVersions():
|
def test_moveVersions():
|
||||||
col = getEmptyCol()
|
col = getEmptyCol()
|
||||||
col.changeSchedulerVer(1)
|
col.changeSchedulerVer(1)
|
||||||
|
|
||||||
n = col.newNote()
|
n = col.newNote()
|
||||||
n['Front'] = "one"
|
n["Front"] = "one"
|
||||||
col.addNote(n)
|
col.addNote(n)
|
||||||
|
|
||||||
# make it a learning card
|
# make it a learning card
|
||||||
|
@ -1168,8 +1235,10 @@ def test_moveVersions():
|
||||||
col.changeSchedulerVer(2)
|
col.changeSchedulerVer(2)
|
||||||
# card with 100 day interval, answering again
|
# card with 100 day interval, answering again
|
||||||
col.sched.reschedCards([c.id], 100, 100)
|
col.sched.reschedCards([c.id], 100, 100)
|
||||||
c.load(); c.due = 0; c.flush()
|
c.load()
|
||||||
col.sched._cardConf(c)['lapse']['mult'] = 0.5
|
c.due = 0
|
||||||
|
c.flush()
|
||||||
|
col.sched._cardConf(c)["lapse"]["mult"] = 0.5
|
||||||
col.sched.reset()
|
col.sched.reset()
|
||||||
c = col.sched.getCard()
|
c = col.sched.getCard()
|
||||||
col.sched.answerCard(c, 1)
|
col.sched.answerCard(c, 1)
|
||||||
|
@ -1178,6 +1247,7 @@ def test_moveVersions():
|
||||||
c.load()
|
c.load()
|
||||||
assert c.due == 50
|
assert c.due == 50
|
||||||
|
|
||||||
|
|
||||||
# cards with a due date earlier than the collection should retain
|
# cards with a due date earlier than the collection should retain
|
||||||
# their due date when removed
|
# their due date when removed
|
||||||
def test_negativeDueFilter():
|
def test_negativeDueFilter():
|
||||||
|
@ -1185,7 +1255,8 @@ def test_negativeDueFilter():
|
||||||
|
|
||||||
# card due prior to collection date
|
# card due prior to collection date
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"; f['Back'] = "two"
|
f["Front"] = "one"
|
||||||
|
f["Back"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
c.due = -5
|
c.due = -5
|
||||||
|
@ -1201,4 +1272,3 @@ def test_negativeDueFilter():
|
||||||
|
|
||||||
c.load()
|
c.load()
|
||||||
assert c.due == -5
|
assert c.due == -5
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from tests.shared import getEmptyCol
|
import tempfile
|
||||||
|
|
||||||
|
from tests.shared import getEmptyCol
|
||||||
|
|
||||||
|
|
||||||
def test_stats():
|
def test_stats():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "foo"
|
f["Front"] = "foo"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
c = f.cards()[0]
|
c = f.cards()[0]
|
||||||
# card stats
|
# card stats
|
||||||
|
@ -17,15 +20,20 @@ def test_stats():
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
assert d.cardStats(c)
|
assert d.cardStats(c)
|
||||||
|
|
||||||
|
|
||||||
def test_graphs_empty():
|
def test_graphs_empty():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
assert d.stats().report()
|
assert d.stats().report()
|
||||||
|
|
||||||
|
|
||||||
def test_graphs():
|
def test_graphs():
|
||||||
from anki import Collection as aopen
|
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()
|
g = d.stats()
|
||||||
rep = g.report()
|
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)
|
f.write(rep)
|
||||||
return
|
return
|
||||||
|
|
|
@ -2,15 +2,18 @@ from anki.template import Template
|
||||||
|
|
||||||
|
|
||||||
def test_remove_formatting_from_mathjax():
|
def test_remove_formatting_from_mathjax():
|
||||||
t = Template('')
|
t = Template("")
|
||||||
assert t._removeFormattingFromMathjax(r'\(2^{{c3::2}}\)', 3) == r'\(2^{{C3::2}}\)'
|
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\) '
|
txt = (
|
||||||
r'{{c4::blah}} {{c5::text with \(x^2\) jax}}')
|
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
|
# Cloze 2 is not in MathJax, so it should not get protected against
|
||||||
# formatting.
|
# formatting.
|
||||||
assert t._removeFormattingFromMathjax(txt, 2) == txt
|
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) == (
|
assert t._removeFormattingFromMathjax(txt, 1) == (
|
||||||
r'\(a\) {{c1::b}} \[ {{C1::c}} \]')
|
r"\(a\) {{c1::b}} \[ {{C1::c}} \]"
|
||||||
|
)
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from tests.shared import getEmptyCol
|
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
|
from tests.shared import getEmptyCol
|
||||||
|
|
||||||
|
|
||||||
def test_op():
|
def test_op():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
|
@ -10,7 +12,7 @@ def test_op():
|
||||||
assert not d.undoName()
|
assert not d.undoName()
|
||||||
# let's adjust a study option
|
# let's adjust a study option
|
||||||
d.save("studyopts")
|
d.save("studyopts")
|
||||||
d.conf['abc'] = 5
|
d.conf["abc"] = 5
|
||||||
# it should be listed as undoable
|
# it should be listed as undoable
|
||||||
assert d.undoName() == "studyopts"
|
assert d.undoName() == "studyopts"
|
||||||
# with about 5 minutes until it's clobbered
|
# with about 5 minutes until it's clobbered
|
||||||
|
@ -18,7 +20,7 @@ def test_op():
|
||||||
# undoing should restore the old value
|
# undoing should restore the old value
|
||||||
d.undo()
|
d.undo()
|
||||||
assert not d.undoName()
|
assert not d.undoName()
|
||||||
assert 'abc' not in d.conf
|
assert "abc" not in d.conf
|
||||||
# an (auto)save will clear the undo
|
# an (auto)save will clear the undo
|
||||||
d.save("foo")
|
d.save("foo")
|
||||||
assert d.undoName() == "foo"
|
assert d.undoName() == "foo"
|
||||||
|
@ -27,7 +29,7 @@ def test_op():
|
||||||
# and a review will, too
|
# and a review will, too
|
||||||
d.save("add")
|
d.save("add")
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.undoName() == "add"
|
assert d.undoName() == "add"
|
||||||
|
@ -35,11 +37,12 @@ def test_op():
|
||||||
d.sched.answerCard(c, 2)
|
d.sched.answerCard(c, 2)
|
||||||
assert d.undoName() == "Review"
|
assert d.undoName() == "Review"
|
||||||
|
|
||||||
|
|
||||||
def test_review():
|
def test_review():
|
||||||
d = getEmptyCol()
|
d = getEmptyCol()
|
||||||
d.conf['counts'] = COUNT_REMAINING
|
d.conf["counts"] = COUNT_REMAINING
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "one"
|
f["Front"] = "one"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
assert not d.undoName()
|
assert not d.undoName()
|
||||||
|
@ -62,7 +65,7 @@ def test_review():
|
||||||
assert not d.undoName()
|
assert not d.undoName()
|
||||||
# we should be able to undo multiple answers too
|
# we should be able to undo multiple answers too
|
||||||
f = d.newNote()
|
f = d.newNote()
|
||||||
f['Front'] = "two"
|
f["Front"] = "two"
|
||||||
d.addNote(f)
|
d.addNote(f)
|
||||||
d.reset()
|
d.reset()
|
||||||
assert d.sched.counts() == (2, 0, 0)
|
assert d.sched.counts() == (2, 0, 0)
|
||||||
|
@ -85,5 +88,3 @@ def test_review():
|
||||||
assert d.undoName() == "foo"
|
assert d.undoName() == "foo"
|
||||||
d.undo()
|
d.undo()
|
||||||
assert not d.undoName()
|
assert not d.undoName()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
from anki.utils import fmtTimeSpan
|
from anki.utils import fmtTimeSpan
|
||||||
|
|
||||||
|
|
||||||
def test_fmtTimeSpan():
|
def test_fmtTimeSpan():
|
||||||
assert fmtTimeSpan(5) == "5 seconds"
|
assert fmtTimeSpan(5) == "5 seconds"
|
||||||
assert fmtTimeSpan(5, inTime=True) == "in 5 seconds"
|
assert fmtTimeSpan(5, inTime=True) == "in 5 seconds"
|
||||||
|
|
|
@ -10,11 +10,7 @@ set -e
|
||||||
BIN="$(cd "`dirname "$0"`"; pwd)"
|
BIN="$(cd "`dirname "$0"`"; pwd)"
|
||||||
export PYTHONPATH=${BIN}/..:${PYTHONPATH}
|
export PYTHONPATH=${BIN}/..:${PYTHONPATH}
|
||||||
|
|
||||||
# favour nosetests3 if available
|
nose="python -m nose2 --plugin=nose2.plugins.mp -N 16"
|
||||||
nose=nosetests
|
|
||||||
if which nosetests3 >/dev/null 2>&1; then
|
|
||||||
nose=nosetests3
|
|
||||||
fi
|
|
||||||
|
|
||||||
dir=.
|
dir=.
|
||||||
|
|
||||||
|
@ -24,7 +20,4 @@ else
|
||||||
lim="tests.test_$1"
|
lim="tests.test_$1"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ x$coverage != x ]; then
|
(cd $dir && $nose $lim $args)
|
||||||
args="--with-coverage"
|
|
||||||
fi
|
|
||||||
(cd $dir && $nose -s --processes=16 --process-timeout=300 $lim $args --cover-package=anki)
|
|
||||||
|
|
4
ts/package-lock.json
generated
4
ts/package-lock.json
generated
|
@ -10,7 +10,7 @@
|
||||||
"integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==",
|
"integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/sizzle": "*"
|
"@types/sizzle": "2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/jqueryui": {
|
"@types/jqueryui": {
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
"integrity": "sha512-bHE7BiG+5Sviy/eA9Npz5HHF3hv40XjaEbpYtSJPaNwuyxhSJ0qWlE8C5DgNMfobVOZ2aSTrM1iGDCGmvlbxOg==",
|
"integrity": "sha512-bHE7BiG+5Sviy/eA9Npz5HHF3hv40XjaEbpYtSJPaNwuyxhSJ0qWlE8C5DgNMfobVOZ2aSTrM1iGDCGmvlbxOg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/jquery": "*"
|
"@types/jquery": "3.3.31"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/mathjax": {
|
"@types/mathjax": {
|
||||||
|
|
Loading…
Reference in a new issue