start port to python 3

unit tests pass and main screens of GUI load
This commit is contained in:
Damien Elmes 2016-05-12 14:45:35 +10:00
parent 1dce3eaaff
commit 15b349e3a8
67 changed files with 510 additions and 759 deletions

View File

@ -5,30 +5,12 @@
import sys import sys
import os import os
import platform import platform
import json
if sys.version_info[0] > 2: if sys.version_info[0] < 3:
raise Exception("Anki should be run with Python 2") raise Exception("Anki should be run with Python 3")
elif sys.version_info[1] < 6: elif sys.version_info[1] < 4:
raise Exception("Anki requires Python 2.6+") raise Exception("Anki requires Python 3.4+")
elif sys.getfilesystemencoding().lower() in ("ascii", "ansi_x3.4-1968"):
raise Exception("Anki requires a UTF-8 locale.")
try:
import simplejson as json
except:
import json as json
if json.__version__ < "1.7.3":
raise Exception("SimpleJSON must be 1.7.3 or later.")
# add path to bundled third party libs
ext = os.path.realpath(os.path.join(
os.path.dirname(__file__), "../thirdparty"))
sys.path.insert(0, ext)
arch = platform.architecture()
if arch[1] == "ELF":
# add arch-dependent libs
sys.path.insert(0, os.path.join(ext, "py2.%d-%s" % (
sys.version_info[1], arch[0][0:2])))
version="2.0.36" # build scripts grep this line, so preserve formatting version="2.0.36" # build scripts grep this line, so preserve formatting
from anki.storage import Collection from anki.storage import Collection

View File

@ -1,15 +0,0 @@
#!/usr/bin/env python
import os, sys
# system-wide install
sys.path.insert(0, "/usr/share/anki")
sys.path.insert(0, "/usr/share/anki/libanki")
# running from extracted folder
base = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, base)
sys.path.insert(0, os.path.join(base, "libanki"))
# or git
sys.path.insert(0, os.path.join(base, "..", "libanki"))
# start
import aqt
aqt.run()

View File

@ -351,7 +351,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
ts += 1 ts += 1
# note any cards that need removing # note any cards that need removing
if nid in have: if nid in have:
for ord, id in have[nid].items(): for ord, id in list(have[nid].items()):
if ord not in avail: if ord not in avail:
rem.append(id) rem.append(id)
# bulk update # bulk update
@ -383,7 +383,7 @@ insert into cards values (?,?,?,?,?,?,0,0,?,0,0,0,0,0,0,0,0,"")""",
card.nid = note.id card.nid = note.id
card.ord = template['ord'] card.ord = template['ord']
# Use template did (deck override) if valid, otherwise model did # Use template did (deck override) if valid, otherwise model did
if template['did'] and unicode(template['did']) in self.decks.decks: if template['did'] and str(template['did']) in self.decks.decks:
card.did = template['did'] card.did = template['did']
else: else:
card.did = note.model()['did'] card.did = note.model()['did']
@ -500,7 +500,7 @@ where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)):
flist = splitFields(data[6]) flist = splitFields(data[6])
fields = {} fields = {}
model = self.models.get(data[2]) model = self.models.get(data[2])
for (name, (idx, conf)) in self.models.fieldMap(model).items(): for (name, (idx, conf)) in list(self.models.fieldMap(model).items()):
fields[name] = flist[idx] fields[name] = flist[idx]
fields['Tags'] = data[5].strip() fields['Tags'] = data[5].strip()
fields['Type'] = model['name'] fields['Type'] = model['name']
@ -820,16 +820,16 @@ and queue = 0""", intTime(), self.usn())
if not self._debugLog: if not self._debugLog:
return return
def customRepr(x): def customRepr(x):
if isinstance(x, basestring): if isinstance(x, str):
return x return x
return pprint.pformat(x) return pprint.pformat(x)
path, num, fn, y = traceback.extract_stack( path, num, fn, y = traceback.extract_stack(
limit=2+kwargs.get("stack", 0))[0] limit=2+kwargs.get("stack", 0))[0]
buf = u"[%s] %s:%s(): %s" % (intTime(), os.path.basename(path), fn, buf = "[%s] %s:%s(): %s" % (intTime(), os.path.basename(path), fn,
", ".join([customRepr(x) for x in args])) ", ".join([customRepr(x) for x in args]))
self._logHnd.write(buf.encode("utf8") + "\n") self._logHnd.write(buf + "\n")
if os.environ.get("ANKIDEV"): if os.environ.get("ANKIDEV"):
print buf print(buf)
def _openLog(self): def _openLog(self):
if not self._debugLog: if not self._debugLog:
@ -840,7 +840,7 @@ and queue = 0""", intTime(), self.usn())
if os.path.exists(lpath2): if os.path.exists(lpath2):
os.unlink(lpath2) os.unlink(lpath2)
os.rename(lpath, lpath2) os.rename(lpath, lpath2)
self._logHnd = open(lpath, "ab") self._logHnd = open(lpath, "a")
def _closeLog(self): def _closeLog(self):
self._logHnd = None self._logHnd = None

View File

@ -18,10 +18,7 @@ Error = sqlite.Error
class DB(object): class DB(object):
def __init__(self, path, timeout=0): def __init__(self, path, timeout=0):
encpath = path self._db = sqlite.connect(path, timeout=timeout)
if isinstance(encpath, unicode):
encpath = path.encode("utf-8")
self._db = sqlite.connect(encpath, timeout=timeout)
self._path = path self._path = path
self.echo = os.environ.get("DBECHO") self.echo = os.environ.get("DBECHO")
self.mod = False self.mod = False
@ -41,9 +38,9 @@ class DB(object):
res = self._db.execute(sql, a) res = self._db.execute(sql, a)
if self.echo: if self.echo:
#print a, ka #print a, ka
print sql, "%0.3fms" % ((time.time() - t)*1000) print(sql, "%0.3fms" % ((time.time() - t)*1000))
if self.echo == "2": if self.echo == "2":
print a, ka print(a, ka)
return res return res
def executemany(self, sql, l): def executemany(self, sql, l):
@ -51,20 +48,20 @@ class DB(object):
t = time.time() t = time.time()
self._db.executemany(sql, l) self._db.executemany(sql, l)
if self.echo: if self.echo:
print sql, "%0.3fms" % ((time.time() - t)*1000) print(sql, "%0.3fms" % ((time.time() - t)*1000))
if self.echo == "2": if self.echo == "2":
print l print(l)
def commit(self): def commit(self):
t = time.time() t = time.time()
self._db.commit() self._db.commit()
if self.echo: if self.echo:
print "commit %0.3fms" % ((time.time() - t)*1000) print("commit %0.3fms" % ((time.time() - t)*1000))
def executescript(self, sql): def executescript(self, sql):
self.mod = True self.mod = True
if self.echo: if self.echo:
print sql print(sql)
self._db.executescript(sql) self._db.executescript(sql)
def rollback(self): def rollback(self):

View File

@ -95,7 +95,7 @@ class DeckManager(object):
self.dconf = json.loads(dconf) self.dconf = json.loads(dconf)
# set limits to within bounds # set limits to within bounds
found = False found = False
for c in self.dconf.values(): for c in list(self.dconf.values()):
for t in ('rev', 'new'): for t in ('rev', 'new'):
pd = 'perDay' pd = 'perDay'
if c[t][pd] > 999999: if c[t][pd] > 999999:
@ -125,7 +125,7 @@ class DeckManager(object):
def id(self, name, create=True, type=defaultDeck): def id(self, name, create=True, type=defaultDeck):
"Add a deck with NAME. Reuse deck if already exists. Return id as int." "Add a deck with NAME. Reuse deck if already exists. Return id as int."
name = name.replace('"', '') name = name.replace('"', '')
for id, g in self.decks.items(): for id, g in list(self.decks.items()):
if g['name'].lower() == name.lower(): if g['name'].lower() == name.lower():
return int(id) return int(id)
if not create: if not create:
@ -185,22 +185,22 @@ class DeckManager(object):
del self.decks[str(did)] del self.decks[str(did)]
# ensure we have an active deck # ensure we have an active deck
if did in self.active(): if did in self.active():
self.select(int(self.decks.keys()[0])) self.select(int(list(self.decks.keys())[0]))
self.save() self.save()
def allNames(self, dyn=True): def allNames(self, dyn=True):
"An unsorted list of all deck names." "An unsorted list of all deck names."
if dyn: if dyn:
return [x['name'] for x in self.decks.values()] return [x['name'] for x in list(self.decks.values())]
else: else:
return [x['name'] for x in self.decks.values() if not x['dyn']] return [x['name'] for x in list(self.decks.values()) if not x['dyn']]
def all(self): def all(self):
"A list of all decks." "A list of all decks."
return self.decks.values() return list(self.decks.values())
def allIds(self): def allIds(self):
return self.decks.keys() return list(self.decks.keys())
def collapse(self, did): def collapse(self, did):
deck = self.get(did) deck = self.get(did)
@ -225,7 +225,7 @@ class DeckManager(object):
def byName(self, name): def byName(self, name):
"Get deck with NAME." "Get deck with NAME."
for m in self.decks.values(): for m in list(self.decks.values()):
if m['name'] == name: if m['name'] == name:
return m return m
@ -319,7 +319,7 @@ class DeckManager(object):
def allConf(self): def allConf(self):
"A list of all deck config." "A list of all deck config."
return self.dconf.values() return list(self.dconf.values())
def confForDid(self, did): def confForDid(self, did):
deck = self.get(did, default=False) deck = self.get(did, default=False)
@ -370,7 +370,7 @@ class DeckManager(object):
def didsForConf(self, conf): def didsForConf(self, conf):
dids = [] dids = []
for deck in self.decks.values(): for deck in list(self.decks.values()):
if 'conf' in deck and deck['conf'] == conf['id']: if 'conf' in deck and deck['conf'] == conf['id']:
dids.append(deck['id']) dids.append(deck['id'])
return dids return dids
@ -421,7 +421,7 @@ class DeckManager(object):
ids2str(dids)) ids2str(dids))
def recoverOrphans(self): def recoverOrphans(self):
dids = self.decks.keys() dids = list(self.decks.keys())
mod = self.col.db.mod mod = self.col.db.mod
self.col.db.execute("update cards set did = 1 where did not in "+ self.col.db.execute("update cards set did = 1 where did not in "+
ids2str(dids)) ids2str(dids))

View File

@ -137,7 +137,7 @@ class AnkiExporter(Exporter):
"insert into cards values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", "insert into cards values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
data) data)
# notes # notes
strnids = ids2str(nids.keys()) strnids = ids2str(list(nids.keys()))
notedata = [] notedata = []
for row in self.src.db.all( for row in self.src.db.all(
"select * from notes where id in "+strnids): "select * from notes where id in "+strnids):
@ -209,7 +209,7 @@ class AnkiExporter(Exporter):
if self._modelHasMedia(m, fname): if self._modelHasMedia(m, fname):
media[fname] = True media[fname] = True
break break
self.mediaFiles = media.keys() self.mediaFiles = list(media.keys())
self.dst.crt = self.src.crt self.dst.crt = self.src.crt
# todo: tags? # todo: tags?
self.count = self.dst.cardCount() self.count = self.dst.cardCount()

View File

@ -92,7 +92,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
else: else:
inQuote = c inQuote = c
# separator (space and ideographic space) # separator (space and ideographic space)
elif c in (" ", u'\u3000'): elif c in (" ", '\u3000'):
if inQuote: if inQuote:
token += c token += c
elif token: elif token:
@ -239,7 +239,8 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
# Commands # Commands
###################################################################### ######################################################################
def _findTag(self, (val, args)): def _findTag(self, args):
(val, args) = args
if val == "none": if val == "none":
return 'n.tags = ""' return 'n.tags = ""'
val = val.replace("*", "%") val = val.replace("*", "%")
@ -250,7 +251,8 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
args.append(val) args.append(val)
return "n.tags like ?" return "n.tags like ?"
def _findCardState(self, (val, args)): def _findCardState(self, args):
(val, args) = args
if val in ("review", "new", "learn"): if val in ("review", "new", "learn"):
if val == "review": if val == "review":
n = 2 n = 2
@ -269,8 +271,9 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
(c.queue = 1 and c.due <= %d)""" % ( (c.queue = 1 and c.due <= %d)""" % (
self.col.sched.today, self.col.sched.dayCutoff) self.col.sched.today, self.col.sched.dayCutoff)
def _findRated(self, (val, args)): def _findRated(self, args):
# days(:optional_ease) # days(:optional_ease)
(val, args) = args
r = val.split(":") r = val.split(":")
try: try:
days = int(r[0]) days = int(r[0])
@ -287,7 +290,8 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
return ("c.id in (select cid from revlog where id>%d %s)" % return ("c.id in (select cid from revlog where id>%d %s)" %
(cutoff, ease)) (cutoff, ease))
def _findAdded(self, (val, args)): def _findAdded(self, args):
(val, args) = args
try: try:
days = int(val) days = int(val)
except ValueError: except ValueError:
@ -295,8 +299,9 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
cutoff = (self.col.sched.dayCutoff - 86400*days)*1000 cutoff = (self.col.sched.dayCutoff - 86400*days)*1000
return "c.id > %d" % cutoff return "c.id > %d" % cutoff
def _findProp(self, (val, args)): def _findProp(self, args):
# extract # extract
(val, args) = args
m = re.match("(^.+?)(<=|>=|!=|=|<|>)(.+?$)", val) m = re.match("(^.+?)(<=|>=|!=|=|<|>)(.+?$)", val)
if not m: if not m:
return return
@ -331,22 +336,26 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
args.append("%"+val+"%") args.append("%"+val+"%")
return "(n.sfld like ? escape '\\' or n.flds like ? escape '\\')" return "(n.sfld like ? escape '\\' or n.flds like ? escape '\\')"
def _findNids(self, (val, args)): def _findNids(self, args):
(val, args) = args
if re.search("[^0-9,]", val): if re.search("[^0-9,]", val):
return return
return "n.id in (%s)" % val return "n.id in (%s)" % val
def _findCids(self, (val, args)): def _findCids(self, args):
(val, args) = args
if re.search("[^0-9,]", val): if re.search("[^0-9,]", val):
return return
return "c.id in (%s)" % val return "c.id in (%s)" % val
def _findMid(self, (val, args)): def _findMid(self, args):
(val, args) = args
if re.search("[^0-9]", val): if re.search("[^0-9]", val):
return return
return "n.mid = %s" % val return "n.mid = %s" % val
def _findModel(self, (val, args)): def _findModel(self, args):
(val, args) = args
ids = [] ids = []
val = val.lower() val = val.lower()
for m in self.col.models.all(): for m in self.col.models.all():
@ -354,8 +363,9 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
ids.append(m['id']) ids.append(m['id'])
return "n.mid in %s" % ids2str(ids) return "n.mid in %s" % ids2str(ids)
def _findDeck(self, (val, args)): def _findDeck(self, args):
# if searching for all decks, skip # if searching for all decks, skip
(val, args) = args
if val == "*": if val == "*":
return "skip" return "skip"
# deck types # deck types
@ -386,8 +396,9 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
sids = ids2str(ids) sids = ids2str(ids)
return "c.did in %s or c.odid in %s" % (sids, sids) return "c.did in %s or c.odid in %s" % (sids, sids)
def _findTemplate(self, (val, args)): def _findTemplate(self, args):
# were we given an ordinal number? # were we given an ordinal number?
(val, args) = args
try: try:
num = int(val) - 1 num = int(val) - 1
except: except:
@ -427,7 +438,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds
for (id,mid,flds) in self.col.db.execute(""" for (id,mid,flds) in self.col.db.execute("""
select id, mid, flds from notes select id, mid, flds from notes
where mid in %s and flds like ? escape '\\'""" % ( where mid in %s and flds like ? escape '\\'""" % (
ids2str(mods.keys())), ids2str(list(mods.keys()))),
"%"+val+"%"): "%"+val+"%"):
flds = splitFields(flds) flds = splitFields(flds)
ord = mods[str(mid)][1] ord = mods[str(mid)][1]
@ -441,8 +452,9 @@ where mid in %s and flds like ? escape '\\'""" % (
return "0" return "0"
return "n.id in %s" % ids2str(nids) return "n.id in %s" % ids2str(nids)
def _findDupes(self, (val, args)): def _findDupes(self, args):
# caller must call stripHTMLMedia on passed val # caller must call stripHTMLMedia on passed val
(val, args) = args
try: try:
mid, val = val.split(",", 1) mid, val = val.split(",", 1)
except OSError: except OSError:

View File

@ -404,7 +404,7 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""", revlog)
###################################################################### ######################################################################
def _postImport(self): def _postImport(self):
for did in self._decks.values(): for did in list(self._decks.values()):
self.col.sched.maybeRandomizeDeck(did) self.col.sched.maybeRandomizeDeck(did)
# make sure new position is correct # make sure new position is correct
self.dst.conf['nextPos'] = self.dst.db.scalar( self.dst.conf['nextPos'] = self.dst.db.scalar(

View File

@ -19,12 +19,12 @@ class AnkiPackageImporter(Anki2Importer):
# we need the media dict in advance, and we'll need a map of fname -> # we need the media dict in advance, and we'll need a map of fname ->
# number to use during the import # number to use during the import
self.nameToNum = {} self.nameToNum = {}
for k, v in json.loads(z.read("media")).items(): for k, v in list(json.loads(z.read("media").decode("utf8")).items()):
self.nameToNum[v] = k self.nameToNum[v] = k
# run anki2 importer # run anki2 importer
Anki2Importer.run(self) Anki2Importer.run(self)
# import static media # import static media
for file, c in self.nameToNum.items(): for file, c in list(self.nameToNum.items()):
if not file.startswith("_") and not file.startswith("latex-"): if not file.startswith("_") and not file.startswith("latex-"):
continue continue
path = os.path.join(self.col.media.dir(), path = os.path.join(self.col.media.dir(),

View File

@ -35,13 +35,12 @@ class TextImporter(NoteImporter):
reader = csv.reader(self.data, self.dialect, doublequote=True) reader = csv.reader(self.data, self.dialect, doublequote=True)
try: try:
for row in reader: for row in reader:
row = [unicode(x, "utf-8") for x in row]
if len(row) != self.numFields: if len(row) != self.numFields:
if row: if row:
log.append(_( log.append(_(
"'%(row)s' had %(num1)d fields, " "'%(row)s' had %(num1)d fields, "
"expected %(num2)d") % { "expected %(num2)d") % {
"row": u" ".join(row), "row": " ".join(row),
"num1": len(row), "num1": len(row),
"num2": self.numFields, "num2": self.numFields,
}) })
@ -49,7 +48,7 @@ class TextImporter(NoteImporter):
continue continue
note = self.noteFromFields(row) note = self.noteFromFields(row)
notes.append(note) notes.append(note)
except (csv.Error), e: except (csv.Error) as e:
log.append(_("Aborted: %s") % str(e)) log.append(_("Aborted: %s") % str(e))
self.log = log self.log = log
self.ignored = ignored self.ignored = ignored
@ -68,16 +67,14 @@ class TextImporter(NoteImporter):
def openFile(self): def openFile(self):
self.dialect = None self.dialect = None
self.fileobj = open(self.file, "rbU") self.fileobj = open(self.file, "r", encoding='utf-8-sig')
self.data = self.fileobj.read() self.data = self.fileobj.read()
if self.data.startswith(codecs.BOM_UTF8):
self.data = self.data[len(codecs.BOM_UTF8):]
def sub(s): def sub(s):
return re.sub("^\#.*$", "__comment", s) return re.sub("^\#.*$", "__comment", s)
self.data = [sub(x)+"\n" for x in self.data.split("\n") if sub(x) != "__comment"] self.data = [sub(x)+"\n" for x in self.data.split("\n") if sub(x) != "__comment"]
if self.data: if self.data:
if self.data[0].startswith("tags:"): if self.data[0].startswith("tags:"):
tags = unicode(self.data[0][5:], "utf8").strip() tags = str(self.data[0][5:], "utf8").strip()
self.tagsToAdd = tags.split(" ") self.tagsToAdd = tags.split(" ")
del self.data[0] del self.data[0]
self.updateDelimiter() self.updateDelimiter()
@ -117,7 +114,7 @@ class TextImporter(NoteImporter):
reader = csv.reader(self.data, delimiter=self.delimiter, doublequote=True) reader = csv.reader(self.data, delimiter=self.delimiter, doublequote=True)
try: try:
while True: while True:
row = reader.next() row = next(reader)
if row: if row:
self.numFields = len(row) self.numFields = len(row)
break break

View File

@ -158,7 +158,7 @@ acq_reps+ret_reps, lapses, card_type_id from cards"""):
def _addCloze(self, notes): def _addCloze(self, notes):
data = [] data = []
notes = notes.values() notes = list(notes.values())
for orig in notes: for orig in notes:
# create a foreign note object # create a foreign note object
n = ForeignNote() n = ForeignNote()

View File

@ -222,7 +222,7 @@ content in the text file to the correct fields."""))
if not self.processFields(n): if not self.processFields(n):
return return
# note id for card updates later # note id for card updates later
for ord, c in n.cards.items(): for ord, c in list(n.cards.items()):
self._cards.append((id, ord, c)) self._cards.append((id, ord, c))
self.col.tags.register(n.tags) self.col.tags.register(n.tags)
return [id, guid64(), self.model['id'], return [id, guid64(), self.model['id'],

View File

@ -10,7 +10,6 @@ from anki.lang import _
from anki.lang import ngettext from anki.lang import ngettext
from xml.dom import minidom from xml.dom import minidom
from types import DictType, InstanceType
from string import capwords from string import capwords
import re, unicodedata, time import re, unicodedata, time
@ -27,9 +26,9 @@ class SmartDict(dict):
def __init__(self, *a, **kw): def __init__(self, *a, **kw):
if a: if a:
if type(a[0]) is DictType: if isinstance(type(a[0]), dict):
kw.update(a[0]) kw.update(a[0])
elif type(a[0]) is InstanceType: elif isinstance(type(a[0]), object):
kw.update(a[0].__dict__) kw.update(a[0].__dict__)
elif hasattr(a[0], '__class__') and a[0].__class__.__name__=='SmartDict': elif hasattr(a[0], '__class__') and a[0].__class__.__name__=='SmartDict':
kw.update(a[0].__dict__) kw.update(a[0].__dict__)
@ -121,26 +120,26 @@ class SupermemoXmlImporter(NoteImporter):
def _fudgeText(self, text): def _fudgeText(self, text):
"Replace sm syntax to Anki syntax" "Replace sm syntax to Anki syntax"
text = text.replace("\n\r", u"<br>") text = text.replace("\n\r", "<br>")
text = text.replace("\n", u"<br>") text = text.replace("\n", "<br>")
return text return text
def _unicode2ascii(self,str): def _unicode2ascii(self,str):
"Remove diacritic punctuation from strings (titles)" "Remove diacritic punctuation from strings (titles)"
return u"".join([ c for c in unicodedata.normalize('NFKD', str) if not unicodedata.combining(c)]) return "".join([ c for c in unicodedata.normalize('NFKD', str) if not unicodedata.combining(c)])
def _decode_htmlescapes(self,s): def _decode_htmlescapes(self,s):
"""Unescape HTML code.""" """Unescape HTML code."""
#In case of bad formated html you can import MinimalSoup etc.. see btflsoup source code #In case of bad formated html you can import MinimalSoup etc.. see btflsoup source code
from BeautifulSoup import BeautifulStoneSoup as btflsoup from bs4 import BeautifulSoup as btflsoup
#my sm2004 also ecaped & char in escaped sequences. #my sm2004 also ecaped & char in escaped sequences.
s = re.sub(u'&amp;',u'&',s) s = re.sub('&amp;','&',s)
#unescaped solitary chars < or > that were ok for minidom confuse btfl soup #unescaped solitary chars < or > that were ok for minidom confuse btfl soup
#s = re.sub(u'>',u'&gt;',s) #s = re.sub(u'>',u'&gt;',s)
#s = re.sub(u'<',u'&lt;',s) #s = re.sub(u'<',u'&lt;',s)
return unicode(btflsoup(s, selfClosingTags=['br','hr','img','wbr'], convertEntities=btflsoup.HTML_ENTITIES)) return str(btflsoup(s, "html.parser"))
def _afactor2efactor(self, af): def _afactor2efactor(self, af):
# Adapted from <http://www.supermemo.com/beta/xml/xml-core.htm> # Adapted from <http://www.supermemo.com/beta/xml/xml-core.htm>
@ -173,9 +172,9 @@ class SupermemoXmlImporter(NoteImporter):
# Migrating content / time consuming part # Migrating content / time consuming part
# addItemToCards is called for each sm element # addItemToCards is called for each sm element
self.logger(u'Parsing started.') self.logger('Parsing started.')
self.parse() self.parse()
self.logger(u'Parsing done.') self.logger('Parsing done.')
# Return imported cards # Return imported cards
self.total = len(self.notes) self.total = len(self.notes)
@ -201,7 +200,7 @@ class SupermemoXmlImporter(NoteImporter):
# pre-process scheduling data # pre-process scheduling data
# convert learning data # convert learning data
if (not self.META.resetLearningData if (not self.META.resetLearningData
and item.Interval >= 1 and int(item.Interval) >= 1
and getattr(item, "LastRepetition", None)): and getattr(item, "LastRepetition", None)):
# migration of LearningData algorithm # migration of LearningData algorithm
tLastrep = time.mktime(time.strptime(item.LastRepetition, '%d.%m.%Y')) tLastrep = time.mktime(time.strptime(item.LastRepetition, '%d.%m.%Y'))
@ -221,7 +220,7 @@ class SupermemoXmlImporter(NoteImporter):
# you can deceide if you are going to tag all toppics or just that containing some pattern # you can deceide if you are going to tag all toppics or just that containing some pattern
tTaggTitle = False tTaggTitle = False
for pattern in self.META.pathsToBeTagged: for pattern in self.META.pathsToBeTagged:
if item.lTitle != None and pattern.lower() in u" ".join(item.lTitle).lower(): if item.lTitle != None and pattern.lower() in " ".join(item.lTitle).lower():
tTaggTitle = True tTaggTitle = True
break break
if tTaggTitle or self.META.tagAllTopics: if tTaggTitle or self.META.tagAllTopics:
@ -236,26 +235,26 @@ class SupermemoXmlImporter(NoteImporter):
tmp = list(set([ re.sub('(\W)',' ', i ) for i in tmp ])) tmp = list(set([ re.sub('(\W)',' ', i ) for i in tmp ]))
tmp = list(set([ re.sub( '^[0-9 ]+$','',i) for i in tmp ])) tmp = list(set([ re.sub( '^[0-9 ]+$','',i) for i in tmp ]))
tmp = list(set([ capwords(i).replace(' ','') for i in tmp ])) tmp = list(set([ capwords(i).replace(' ','') for i in tmp ]))
tags = [ j[0].lower() + j[1:] for j in tmp if j.strip() <> ''] tags = [ j[0].lower() + j[1:] for j in tmp if j.strip() != '']
note.tags += tags note.tags += tags
if self.META.tagMemorizedItems and item.Interval >0: if self.META.tagMemorizedItems and int(item.Interval) >0:
note.tags.append("Memorized") note.tags.append("Memorized")
self.logger(u'Element tags\t- ' + `note.tags`, level=3) self.logger('Element tags\t- ' + repr(note.tags), level=3)
self.notes.append(note) self.notes.append(note)
def logger(self,text,level=1): def logger(self,text,level=1):
"Wrapper for Anki logger" "Wrapper for Anki logger"
dLevels={0:'',1:u'Info',2:u'Verbose',3:u'Debug'} dLevels={0:'',1:'Info',2:'Verbose',3:'Debug'}
if level<=self.META.loggerLevel: if level<=self.META.loggerLevel:
#self.deck.updateProgress(_(text)) #self.deck.updateProgress(_(text))
if self.META.logToStdOutput: if self.META.logToStdOutput:
print self.__class__.__name__+ u" - " + dLevels[level].ljust(9) +u' -\t'+ _(text) print(self.__class__.__name__+ " - " + dLevels[level].ljust(9) +' -\t'+ _(text))
# OPEN AND LOAD # OPEN AND LOAD
@ -266,9 +265,9 @@ class SupermemoXmlImporter(NoteImporter):
return sys.stdin return sys.stdin
# try to open with urllib (if source is http, ftp, or file URL) # try to open with urllib (if source is http, ftp, or file URL)
import urllib import urllib.request, urllib.parse, urllib.error
try: try:
return urllib.urlopen(source) return urllib.request.urlopen(source)
except (IOError, OSError): except (IOError, OSError):
pass pass
@ -279,24 +278,24 @@ class SupermemoXmlImporter(NoteImporter):
pass pass
# treat source as string # treat source as string
import StringIO import io
return StringIO.StringIO(str(source)) return io.StringIO(str(source))
def loadSource(self, source): def loadSource(self, source):
"""Load source file and parse with xml.dom.minidom""" """Load source file and parse with xml.dom.minidom"""
self.source = source self.source = source
self.logger(u'Load started...') self.logger('Load started...')
sock = open(self.source) sock = open(self.source)
self.xmldoc = minidom.parse(sock).documentElement self.xmldoc = minidom.parse(sock).documentElement
sock.close() sock.close()
self.logger(u'Load done.') self.logger('Load done.')
# PARSE # PARSE
def parse(self, node=None): def parse(self, node=None):
"Parse method - parses document elements" "Parse method - parses document elements"
if node==None and self.xmldoc<>None: if node==None and self.xmldoc!=None:
node = self.xmldoc node = self.xmldoc
_method = "parse_%s" % node.__class__.__name__ _method = "parse_%s" % node.__class__.__name__
@ -304,7 +303,7 @@ class SupermemoXmlImporter(NoteImporter):
parseMethod = getattr(self, _method) parseMethod = getattr(self, _method)
parseMethod(node) parseMethod(node)
else: else:
self.logger(u'No handler for method %s' % _method, level=3) self.logger('No handler for method %s' % _method, level=3)
def parse_Document(self, node): def parse_Document(self, node):
"Parse XML document" "Parse XML document"
@ -319,7 +318,7 @@ class SupermemoXmlImporter(NoteImporter):
handlerMethod = getattr(self, _method) handlerMethod = getattr(self, _method)
handlerMethod(node) handlerMethod(node)
else: else:
self.logger(u'No handler for method %s' % _method, level=3) self.logger('No handler for method %s' % _method, level=3)
#print traceback.print_exc() #print traceback.print_exc()
def parse_Text(self, node): def parse_Text(self, node):
@ -353,7 +352,7 @@ class SupermemoXmlImporter(NoteImporter):
for child in node.childNodes: self.parse(child) for child in node.childNodes: self.parse(child)
#strip all saved strings, just for sure #strip all saved strings, just for sure
for key in self.cntElm[-1].keys(): for key in list(self.cntElm[-1].keys()):
if hasattr(self.cntElm[-1][key], 'strip'): if hasattr(self.cntElm[-1][key], 'strip'):
self.cntElm[-1][key]=self.cntElm[-1][key].strip() self.cntElm[-1][key]=self.cntElm[-1][key].strip()
@ -367,18 +366,18 @@ class SupermemoXmlImporter(NoteImporter):
# migrate only memorized otherway skip/continue # migrate only memorized otherway skip/continue
if self.META.onlyMemorizedItems and not(int(smel.Interval) > 0): if self.META.onlyMemorizedItems and not(int(smel.Interval) > 0):
self.logger(u'Element skiped \t- not memorized ...', level=3) self.logger('Element skiped \t- not memorized ...', level=3)
else: else:
#import sm element data to Anki #import sm element data to Anki
self.addItemToCards(smel) self.addItemToCards(smel)
self.logger(u"Import element \t- " + smel['Question'], level=3) self.logger("Import element \t- " + smel['Question'], level=3)
#print element #print element
self.logger('-'*45, level=3) self.logger('-'*45, level=3)
for key in smel.keys(): for key in list(smel.keys()):
self.logger('\t%s %s' % ((key+':').ljust(15),smel[key]), level=3 ) self.logger('\t%s %s' % ((key+':').ljust(15),smel[key]), level=3 )
else: else:
self.logger(u'Element skiped \t- no valid Q and A ...', level=3) self.logger('Element skiped \t- no valid Q and A ...', level=3)
else: else:
@ -389,7 +388,7 @@ class SupermemoXmlImporter(NoteImporter):
if smel.Title != None: if smel.Title != None:
# remove topic from title list # remove topic from title list
t = self.cntMeta['title'].pop() t = self.cntMeta['title'].pop()
self.logger(u'End of topic \t- %s' % (t), level=2) self.logger('End of topic \t- %s' % (t), level=2)
def do_Content(self, node): def do_Content(self, node):
"Process SM element Content" "Process SM element Content"
@ -422,7 +421,7 @@ class SupermemoXmlImporter(NoteImporter):
self.cntElm[-1][node.tagName] = t self.cntElm[-1][node.tagName] = t
self.cntMeta['title'].append(t) self.cntMeta['title'].append(t)
self.cntElm[-1]['lTitle'] = self.cntMeta['title'] self.cntElm[-1]['lTitle'] = self.cntMeta['title']
self.logger(u'Start of topic \t- ' + u" / ".join(self.cntMeta['title']), level=2) self.logger('Start of topic \t- ' + " / ".join(self.cntMeta['title']), level=2)
def do_Type(self, node): def do_Type(self, node):

View File

@ -7,48 +7,48 @@ import gettext
import threading import threading
langs = [ langs = [
(u"Afrikaans", "af"), ("Afrikaans", "af"),
(u"Bahasa Melayu", "ms"), ("Bahasa Melayu", "ms"),
(u"Dansk", "da"), ("Dansk", "da"),
(u"Deutsch", "de"), ("Deutsch", "de"),
(u"Eesti", "et"), ("Eesti", "et"),
(u"English", "en"), ("English", "en"),
(u"Español", "es"), ("Español", "es"),
(u"Esperanto", "eo"), ("Esperanto", "eo"),
(u"Français", "fr"), ("Français", "fr"),
(u"Galego", "gl"), ("Galego", "gl"),
(u"Italiano", "it"), ("Italiano", "it"),
(u"Lenga d'òc", "oc"), ("Lenga d'òc", "oc"),
(u"Magyar", "hu"), ("Magyar", "hu"),
(u"Nederlands","nl"), ("Nederlands","nl"),
(u"Norsk","nb"), ("Norsk","nb"),
(u"Occitan","oc"), ("Occitan","oc"),
(u"Plattdüütsch", "nds"), ("Plattdüütsch", "nds"),
(u"Polski", "pl"), ("Polski", "pl"),
(u"Português Brasileiro", "pt_BR"), ("Português Brasileiro", "pt_BR"),
(u"Português", "pt"), ("Português", "pt"),
(u"Româneşte", "ro"), ("Româneşte", "ro"),
(u"Slovenščina", "sl"), ("Slovenščina", "sl"),
(u"Suomi", "fi"), ("Suomi", "fi"),
(u"Svenska", "sv"), ("Svenska", "sv"),
(u"Tiếng Việt", "vi"), ("Tiếng Việt", "vi"),
(u"Türkçe", "tr"), ("Türkçe", "tr"),
(u"Čeština", "cs"), ("Čeština", "cs"),
(u"Ελληνικά", "el"), ("Ελληνικά", "el"),
(u"босански", "bs"), ("босански", "bs"),
(u"Български", "bg"), ("Български", "bg"),
(u"Монгол хэл","mn"), ("Монгол хэл","mn"),
(u"русский язык", "ru"), ("русский язык", "ru"),
(u"Српски", "sr"), ("Српски", "sr"),
(u"українська мова", "uk"), ("українська мова", "uk"),
(u"עִבְרִית", "he"), ("עִבְרִית", "he"),
(u"العربية", "ar"), ("العربية", "ar"),
(u"فارسی", "fa"), ("فارسی", "fa"),
(u"ภาษาไทย", "th"), ("ภาษาไทย", "th"),
(u"日本語", "ja"), ("日本語", "ja"),
(u"简体中文", "zh_CN"), ("简体中文", "zh_CN"),
(u"繁體中文", "zh_TW"), ("繁體中文", "zh_TW"),
(u"한국어", "ko"), ("한국어", "ko"),
] ]
threadLocal = threading.local() threadLocal = threading.local()
@ -65,10 +65,10 @@ def localTranslation():
return currentTranslation return currentTranslation
def _(str): def _(str):
return localTranslation().ugettext(str) return localTranslation().gettext(str)
def ngettext(single, plural, n): def ngettext(single, plural, n):
return localTranslation().ungettext(single, plural, n) return localTranslation().ngettext(single, plural, n)
def langDir(): def langDir():
dir = os.path.join(os.path.dirname( dir = os.path.join(os.path.dirname(

View File

@ -56,7 +56,7 @@ def _imgLink(col, latex, model):
if os.path.exists(fname): if os.path.exists(fname):
return link return link
elif not build: elif not build:
return u"[latex]%s[/latex]" % latex return "[latex]%s[/latex]" % latex
else: else:
err = _buildImg(col, txt, fname, model) err = _buildImg(col, txt, fname, model)
if err: if err:
@ -71,11 +71,10 @@ def _latexFromHtml(col, latex):
return latex return latex
def _buildImg(col, latex, fname, model): def _buildImg(col, latex, fname, model):
# add header/footer & convert to utf8 # add header/footer
latex = (model["latexPre"] + "\n" + latex = (model["latexPre"] + "\n" +
latex + "\n" + latex + "\n" +
model["latexPost"]) model["latexPost"])
latex = latex.encode("utf8")
# it's only really secure if run in a jail, but these are the most common # it's only really secure if run in a jail, but these are the most common
tmplatex = latex.replace("\\includegraphics", "") tmplatex = latex.replace("\\includegraphics", "")
for bad in ("\\write18", "\\readline", "\\input", "\\include", for bad in ("\\write18", "\\readline", "\\input", "\\include",
@ -91,7 +90,7 @@ package in the LaTeX header instead.""") % bad
# write into a temp file # write into a temp file
log = open(namedtmp("latex_log.txt"), "w") log = open(namedtmp("latex_log.txt"), "w")
texpath = namedtmp("tmp.tex") texpath = namedtmp("tmp.tex")
texfile = file(texpath, "w") texfile = open(texpath, "w")
texfile.write(latex) texfile.write(latex)
texfile.close() texfile.close()
mdir = col.media.dir() mdir = col.media.dir()

View File

@ -4,11 +4,11 @@
import re import re
import traceback import traceback
import urllib import urllib.request, urllib.parse, urllib.error
import unicodedata import unicodedata
import sys import sys
import zipfile import zipfile
from cStringIO import StringIO from io import StringIO
from anki.utils import checksum, isWin, isMac, json from anki.utils import checksum, isWin, isMac, json
from anki.db import DB from anki.db import DB
@ -33,9 +33,6 @@ class MediaManager(object):
return return
# media directory # media directory
self._dir = re.sub("(?i)\.(anki2)$", ".media", self.col.path) self._dir = re.sub("(?i)\.(anki2)$", ".media", self.col.path)
# convert dir to unicode if it's not already
if isinstance(self._dir, str):
self._dir = unicode(self._dir, sys.getfilesystemencoding())
if not os.path.exists(self._dir): if not os.path.exists(self._dir):
os.makedirs(self._dir) os.makedirs(self._dir)
try: try:
@ -92,7 +89,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
insert into meta select dirMod, usn from old.meta insert into meta select dirMod, usn from old.meta
""") """)
self.db.commit() self.db.commit()
except Exception, e: except Exception as e:
# if we couldn't import the old db for some reason, just start # if we couldn't import the old db for some reason, just start
# anew # anew
self.col.log("failed to import old media db:"+traceback.format_exc()) self.col.log("failed to import old media db:"+traceback.format_exc())
@ -223,16 +220,15 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
def escapeImages(self, string, unescape=False): def escapeImages(self, string, unescape=False):
if unescape: if unescape:
fn = urllib.unquote fn = urllib.parse.unquote
else: else:
fn = urllib.quote fn = urllib.parse.quote
def repl(match): def repl(match):
tag = match.group(0) tag = match.group(0)
fname = match.group("fname") fname = match.group("fname")
if re.match("(https?|ftp)://", fname): if re.match("(https?|ftp)://", fname):
return tag return tag
return tag.replace( return tag.replace(fname, fn(fname))
fname, unicode(fn(fname.encode("utf-8")), "utf8"))
for reg in self.imgRegexps: for reg in self.imgRegexps:
string = re.sub(reg, repl, string) string = re.sub(reg, repl, string)
return string return string
@ -271,9 +267,6 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
if file.startswith("_"): if file.startswith("_"):
# leading _ says to ignore file # leading _ says to ignore file
continue continue
if not isinstance(file, unicode):
invalid.append(unicode(file, sys.getfilesystemencoding(), "replace"))
continue
nfcFile = unicodedata.normalize("NFC", file) nfcFile = unicodedata.normalize("NFC", file)
# we enforce NFC fs encoding on non-macs; on macs we'll have gotten # we enforce NFC fs encoding on non-macs; on macs we'll have gotten
# NFD so we use the above variable for comparing references # NFD so we use the above variable for comparing references
@ -322,9 +315,6 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
return re.sub(self._illegalCharReg, "", str) return re.sub(self._illegalCharReg, "", str)
def hasIllegal(self, str): def hasIllegal(self, str):
# a file that couldn't be decoded to unicode is considered invalid
if not isinstance(str, unicode):
return True
return not not re.search(self._illegalCharReg, str) return not not re.search(self._illegalCharReg, str)
# Tracking changes # Tracking changes
@ -413,7 +403,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
# mark as used # mark as used
self.cache[f][2] = True self.cache[f][2] = True
# look for any entries in the cache that no longer exist on disk # look for any entries in the cache that no longer exist on disk
for (k, v) in self.cache.items(): for (k, v) in list(self.cache.items()):
if not v[2]: if not v[2]:
removed.append(k) removed.append(k)
return added, removed return added, removed
@ -510,8 +500,6 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0);
data = z.read(i) data = z.read(i)
csum = checksum(data) csum = checksum(data)
name = meta[i.filename] name = meta[i.filename]
if not isinstance(name, unicode):
name = unicode(name, "utf8")
# normalize name for platform # normalize name for platform
if isMac: if isMac:
name = unicodedata.normalize("NFD", name) name = unicodedata.normalize("NFD", name)

View File

@ -108,7 +108,7 @@ class ModelManager(object):
m = self.get(self.col.decks.current().get('mid')) m = self.get(self.col.decks.current().get('mid'))
if not forDeck or not m: if not forDeck or not m:
m = self.get(self.col.conf['curModel']) m = self.get(self.col.conf['curModel'])
return m or self.models.values()[0] return m or list(self.models.values())[0]
def setCurrent(self, m): def setCurrent(self, m):
self.col.conf['curModel'] = m['id'] self.col.conf['curModel'] = m['id']
@ -122,14 +122,14 @@ class ModelManager(object):
def all(self): def all(self):
"Get all models." "Get all models."
return self.models.values() return list(self.models.values())
def allNames(self): def allNames(self):
return [m['name'] for m in self.all()] return [m['name'] for m in self.all()]
def byName(self, name): def byName(self, name):
"Get model with NAME." "Get model with NAME."
for m in self.models.values(): for m in list(self.models.values()):
if m['name'] == name: if m['name'] == name:
return m return m
@ -158,7 +158,7 @@ select id from cards where nid in (select id from notes where mid = ?)""",
self.save() self.save()
# GUI should ensure last model is not deleted # GUI should ensure last model is not deleted
if current: if current:
self.setCurrent(self.models.values()[0]) self.setCurrent(list(self.models.values())[0])
def add(self, m): def add(self, m):
self._setID(m) self._setID(m)
@ -191,7 +191,7 @@ select id from cards where nid in (select id from notes where mid = ?)""",
return str(id) in self.models return str(id) in self.models
def ids(self): def ids(self):
return self.models.keys() return list(self.models.keys())
# Tools # Tools
################################################## ##################################################
@ -429,7 +429,7 @@ select id from notes where mid = ?)""" % " ".join(map),
"select id, flds from notes where id in "+ids2str(nids)): "select id, flds from notes where id in "+ids2str(nids)):
newflds = {} newflds = {}
flds = splitFields(flds) flds = splitFields(flds)
for old, new in map.items(): for old, new in list(map.items()):
newflds[new] = flds[old] newflds[new] = flds[old]
flds = [] flds = []
for c in range(nfields): for c in range(nfields):

View File

@ -79,7 +79,7 @@ insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""",
################################################## ##################################################
def keys(self): def keys(self):
return self._fmap.keys() return list(self._fmap.keys())
def values(self): def values(self):
return self.fields return self.fields
@ -101,7 +101,7 @@ insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""",
self.fields[self._fieldOrd(key)] = value self.fields[self._fieldOrd(key)] = value
def __contains__(self, key): def __contains__(self, key):
return key in self._fmap.keys() return key in list(self._fmap.keys())
# Tags # Tags
################################################## ##################################################

View File

@ -2,7 +2,6 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import division
import time import time
import random import random
import itertools import itertools

View File

@ -24,8 +24,8 @@ def hasSound(text):
########################################################################## ##########################################################################
processingSrc = u"rec.wav" processingSrc = "rec.wav"
processingDst = u"rec.mp3" processingDst = "rec.mp3"
processingChain = [] processingChain = []
recFiles = [] recFiles = []
@ -229,7 +229,7 @@ class _Recorder(object):
if ret: if ret:
raise Exception(_( raise Exception(_(
"Error running %s") % "Error running %s") %
u" ".join(c)) " ".join(c))
class PyAudioThreadedRecorder(threading.Thread): class PyAudioThreadedRecorder(threading.Thread):
@ -258,7 +258,7 @@ class PyAudioThreadedRecorder(threading.Thread):
while not self.finish: while not self.finish:
try: try:
data = stream.read(chunk) data = stream.read(chunk)
except IOError, e: except IOError as e:
if e[1] == pyaudio.paInputOverflowed: if e[1] == pyaudio.paInputOverflowed:
data = None data = None
else: else:
@ -295,7 +295,7 @@ class PyAudioRecorder(_Recorder):
def file(self): def file(self):
if self.encode: if self.encode:
tgt = u"rec%d.mp3" % time.time() tgt = "rec%d.mp3" % time.time()
os.rename(processingDst, tgt) os.rename(processingDst, tgt)
return tgt return tgt
else: else:

View File

@ -2,7 +2,6 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import division
import time import time
import datetime import datetime
import json import json
@ -112,7 +111,7 @@ class CollectionStats(object):
def report(self, type=0): def report(self, type=0):
# 0=days, 1=weeks, 2=months # 0=days, 1=weeks, 2=months
self.type = type self.type = type
from statsbg import bg from .statsbg import bg
txt = self.css % bg txt = self.css % bg
txt += self.todayStats() txt += self.todayStats()
txt += self.dueGraph() txt += self.dueGraph()
@ -160,7 +159,7 @@ from revlog where id > ? """+lim, (self.col.sched.dayCutoff-86400)*1000)
filt = filt or 0 filt = filt or 0
# studied # studied
def bold(s): def bold(s):
return "<b>"+unicode(s)+"</b>" return "<b>"+str(s)+"</b>"
msgp1 = ngettext("<!--studied-->%d card", "<!--studied-->%d cards", cards) % cards msgp1 = ngettext("<!--studied-->%d card", "<!--studied-->%d cards", cards) % cards
b += _("Studied %(a)s in %(b)s today.") % dict( b += _("Studied %(a)s in %(b)s today.") % dict(
a=bold(msgp1), b=bold(fmtTimeSpan(thetime, unit=1))) a=bold(msgp1), b=bold(fmtTimeSpan(thetime, unit=1)))

View File

@ -2,19 +2,19 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import urllib import urllib.request, urllib.parse, urllib.error
import sys import sys
import gzip import gzip
import random import random
from cStringIO import StringIO from io import StringIO
import httplib2 import httplib2
from anki.db import DB from anki.db import DB
from anki.utils import ids2str, intTime, json, isWin, isMac, platDesc, checksum from anki.utils import ids2str, intTime, json, isWin, isMac, platDesc, checksum
from anki.consts import * from anki.consts import *
from hooks import runHook from .hooks import runHook
import anki import anki
from lang import ngettext from .lang import ngettext
# syncing vars # syncing vars
HTTP_TIMEOUT = 90 HTTP_TIMEOUT = 90
@ -64,17 +64,19 @@ def _setupProxy():
# platform-specific fetch # platform-specific fetch
url = None url = None
if isWin: if isWin:
r = urllib.getproxies_registry() print("fixme: win proxy support")
if 'https' in r: # r = urllib.getproxies_registry()
url = r['https'] # if 'https' in r:
elif 'http' in r: # url = r['https']
url = r['http'] # elif 'http' in r:
# url = r['http']
elif isMac: elif isMac:
r = urllib.getproxies_macosx_sysconf() print("fixme: mac proxy support")
if 'https' in r: # r = urllib.getproxies_macosx_sysconf()
url = r['https'] # if 'https' in r:
elif 'http' in r: # url = r['https']
url = r['http'] # elif 'http' in r:
# url = r['http']
if url: if url:
p = _proxy_info_from_url(url, _proxyMethod(url)) p = _proxy_info_from_url(url, _proxyMethod(url))
if p: if p:
@ -556,7 +558,7 @@ class HttpSyncer(object):
buf = StringIO() buf = StringIO()
# post vars # post vars
self.postVars['c'] = 1 if comp else 0 self.postVars['c'] = 1 if comp else 0
for (key, value) in self.postVars.items(): for (key, value) in list(self.postVars.items()):
buf.write(bdry + "\r\n") buf.write(bdry + "\r\n")
buf.write( buf.write(
'Content-Disposition: form-data; name="%s"\r\n\r\n%s\r\n' % 'Content-Disposition: form-data; name="%s"\r\n\r\n%s\r\n' %

View File

@ -47,7 +47,7 @@ class TagManager(object):
runHook("newTag") runHook("newTag")
def all(self): def all(self):
return self.tags.keys() return list(self.tags.keys())
def registerNotes(self, nids=None): def registerNotes(self, nids=None):
"Add any missing tags from notes to the tags list." "Add any missing tags from notes to the tags list."
@ -62,7 +62,7 @@ class TagManager(object):
" ".join(self.col.db.list("select distinct tags from notes"+lim))))) " ".join(self.col.db.list("select distinct tags from notes"+lim)))))
def allItems(self): def allItems(self):
return self.tags.items() return list(self.tags.items())
def save(self): def save(self):
self.changed = True self.changed = True
@ -122,13 +122,13 @@ class TagManager(object):
def split(self, tags): def split(self, tags):
"Parse a string and return a list of tags." "Parse a string and return a list of tags."
return [t for t in tags.replace(u'\u3000', ' ').split(" ") if t] return [t for t in tags.replace('\u3000', ' ').split(" ") if t]
def join(self, tags): def join(self, tags):
"Join tags into a single string, with leading and trailing spaces." "Join tags into a single string, with leading and trailing spaces."
if not tags: if not tags:
return u"" return ""
return u" %s " % u" ".join(tags) return " %s " % " ".join(tags)
def addToStr(self, addtags, tags): def addToStr(self, addtags, tags):
"Add tags if they don't exist, and canonify." "Add tags if they don't exist, and canonify."
@ -174,6 +174,6 @@ class TagManager(object):
########################################################################## ##########################################################################
def beforeUpload(self): def beforeUpload(self):
for k in self.tags.keys(): for k in list(self.tags.keys()):
self.tags[k] = 0 self.tags[k] = 0
self.save() self.save()

View File

@ -1,78 +0,0 @@
========
Pystache
========
Inspired by ctemplate_ and et_, Mustache_ is a
framework-agnostic way to render logic-free views.
As ctemplates says, "It emphasizes separating logic from presentation:
it is impossible to embed application logic in this template language."
Pystache is a Python implementation of Mustache. Pystache requires
Python 2.6.
Documentation
=============
The different Mustache tags are documented at `mustache(5)`_.
Install It
==========
::
pip install pystache
Use It
======
::
>>> import pystache
>>> pystache.render('Hi {{person}}!', {'person': 'Mom'})
'Hi Mom!'
You can also create dedicated view classes to hold your view logic.
Here's your simple.py::
import pystache
class Simple(pystache.View):
def thing(self):
return "pizza"
Then your template, simple.mustache::
Hi {{thing}}!
Pull it together::
>>> Simple().render()
'Hi pizza!'
Test It
=======
nose_ works great! ::
pip install nose
cd pystache
nosetests
Author
======
::
context = { 'author': 'Chris Wanstrath', 'email': 'chris@ozmm.org' }
pystache.render("{{author}} :: {{email}}", context)
.. _ctemplate: http://code.google.com/p/google-ctemplate/
.. _et: http://www.ivan.fomichev.name/2008/05/erlang-template-engine-prototype.html
.. _Mustache: http://defunkt.github.com/mustache/
.. _mustache(5): http://defunkt.github.com/mustache/mustache.5.html
.. _nose: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/testing.html

View File

@ -99,7 +99,7 @@ class Template(object):
replacer = '' replacer = ''
# if it and isinstance(it, collections.Callable): # if it and isinstance(it, collections.Callable):
# replacer = it(inner) # replacer = it(inner)
if isinstance(it, basestring): if isinstance(it, str):
it = stripHTMLMedia(it).strip() it = stripHTMLMedia(it).strip()
if it and not hasattr(it, '__iter__'): if it and not hasattr(it, '__iter__'):
if section[2] != '^': if section[2] != '^':
@ -133,7 +133,7 @@ class Template(object):
replacement = func(self, tag_name, context) replacement = func(self, tag_name, context)
template = template.replace(tag, replacement) template = template.replace(tag, replacement)
except (SyntaxError, KeyError): except (SyntaxError, KeyError):
return u"{{invalid template}}" return "{{invalid template}}"
return template return template

View File

@ -53,7 +53,7 @@ class View(object):
name = self.get_template_name() + '.' + self.template_extension name = self.get_template_name() + '.' + self.template_extension
if isinstance(self.template_path, basestring): if isinstance(self.template_path, str):
self.template_file = os.path.join(self.template_path, name) self.template_file = os.path.join(self.template_path, name)
return self._load_template() return self._load_template()
@ -70,7 +70,7 @@ class View(object):
try: try:
template = f.read() template = f.read()
if self.template_encoding: if self.template_encoding:
template = unicode(template, self.template_encoding) template = str(template, self.template_encoding)
finally: finally:
f.close() f.close()
return template return template

View File

@ -2,13 +2,12 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import division
import re import re
import os import os
import random import random
import time import time
import math import math
import htmlentitydefs import html.entities
import subprocess import subprocess
import tempfile import tempfile
import shutil import shutil
@ -18,28 +17,10 @@ import locale
from hashlib import sha1 from hashlib import sha1
import platform import platform
import traceback import traceback
import json
from anki.lang import _, ngettext from anki.lang import _, ngettext
if sys.version_info[1] < 5:
def format_string(a, b):
return a % b
locale.format_string = format_string
try:
import simplejson as json
# make sure simplejson's loads() always returns unicode
# we don't try to support .load()
origLoads = json.loads
def loads(s, *args, **kwargs):
if not isinstance(s, unicode):
s = unicode(s, "utf8")
return origLoads(s, *args, **kwargs)
json.loads = loads
except ImportError:
import json
# Time handling # Time handling
############################################################################## ##############################################################################
@ -182,15 +163,15 @@ def entsToTxt(html):
# character reference # character reference
try: try:
if text[:3] == "&#x": if text[:3] == "&#x":
return unichr(int(text[3:-1], 16)) return chr(int(text[3:-1], 16))
else: else:
return unichr(int(text[2:-1])) return chr(int(text[2:-1]))
except ValueError: except ValueError:
pass pass
else: else:
# named entity # named entity
try: try:
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) text = chr(html.entities.name2codepoint[text[1:-1]])
except KeyError: except KeyError:
pass pass
return text # leave as is return text # leave as is
@ -222,8 +203,7 @@ def maxID(db):
"Return the first safe ID to use." "Return the first safe ID to use."
now = intTime(1000) now = intTime(1000)
for tbl in "cards", "notes": for tbl in "cards", "notes":
now = max(now, db.scalar( now = max(now, db.scalar("select max(id) from %s" % tbl) or 0)
"select max(id) from %s" % tbl))
return now + 1 return now + 1
# used in ankiweb # used in ankiweb
@ -271,7 +251,7 @@ def splitFields(string):
############################################################################## ##############################################################################
def checksum(data): def checksum(data):
if isinstance(data, unicode): if isinstance(data, str):
data = data.encode("utf-8") data = data.encode("utf-8")
return sha1(data).hexdigest() return sha1(data).hexdigest()
@ -292,8 +272,7 @@ def tmpdir():
shutil.rmtree(_tmpdir) shutil.rmtree(_tmpdir)
import atexit import atexit
atexit.register(cleanup) atexit.register(cleanup)
_tmpdir = unicode(os.path.join(tempfile.gettempdir(), "anki_temp"), \ _tmpdir = os.path.join(tempfile.gettempdir(), "anki_temp")
sys.getfilesystemencoding())
if not os.path.exists(_tmpdir): if not os.path.exists(_tmpdir):
os.mkdir(_tmpdir) os.mkdir(_tmpdir)
return _tmpdir return _tmpdir

View File

@ -5,7 +5,7 @@ import os
import sys import sys
import optparse import optparse
import tempfile import tempfile
import __builtin__ import builtins
import locale import locale
import gettext import gettext
@ -29,16 +29,16 @@ moduleDir = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0]
try: try:
import aqt.forms import aqt.forms
except ImportError, e: except ImportError as e:
if "forms" in str(e): if "forms" in str(e):
print "If you're running from git, did you run build_ui.sh?" print("If you're running from git, did you run build_ui.sh?")
print print()
raise raise
from anki.utils import checksum from anki.utils import checksum
# Dialog manager - manages modeless windows # Dialog manager - manages modeless windows
########################################################################## ##########################################################################emacs
class DialogManager(object): class DialogManager(object):
@ -67,7 +67,7 @@ class DialogManager(object):
def closeAll(self): def closeAll(self):
"True if all closed successfully." "True if all closed successfully."
for (n, (creator, instance)) in self._dialogs.items(): for (n, (creator, instance)) in list(self._dialogs.items()):
if instance: if instance:
if not instance.canClose(): if not instance.canClose():
return False return False
@ -98,8 +98,8 @@ def setupLang(pm, app, force=None):
# gettext # gettext
_gtrans = gettext.translation( _gtrans = gettext.translation(
'anki', dir, languages=[lang], fallback=True) 'anki', dir, languages=[lang], fallback=True)
__builtin__.__dict__['_'] = _gtrans.ugettext builtins.__dict__['_'] = _gtrans.gettext
__builtin__.__dict__['ngettext'] = _gtrans.ungettext builtins.__dict__['ngettext'] = _gtrans.ngettext
anki.lang.setLang(lang, local=False) anki.lang.setLang(lang, local=False)
if lang in ("he","ar","fa"): if lang in ("he","ar","fa"):
app.setLayoutDirection(Qt.RightToLeft) app.setLayoutDirection(Qt.RightToLeft)
@ -133,7 +133,7 @@ class AnkiApp(QApplication):
if args and args[0]: if args and args[0]:
buf = os.path.abspath(args[0]) buf = os.path.abspath(args[0])
if self.sendMsg(buf): if self.sendMsg(buf):
print "Already running; reusing existing instance." print("Already running; reusing existing instance.")
return True return True
else: else:
# send failed, so we're the first instance or the # send failed, so we're the first instance or the
@ -163,7 +163,7 @@ class AnkiApp(QApplication):
sys.stderr.write(sock.errorString()) sys.stderr.write(sock.errorString())
return return
buf = sock.readAll() buf = sock.readAll()
buf = unicode(buf, sys.getfilesystemencoding(), "ignore") buf = str(buf, sys.getfilesystemencoding(), "ignore")
self.emit(SIGNAL("appMsg"), buf) self.emit(SIGNAL("appMsg"), buf)
sock.disconnectFromServer() sock.disconnectFromServer()
@ -192,7 +192,7 @@ def parseArgs(argv):
def run(): def run():
try: try:
_run() _run()
except Exception, e: except Exception as e:
QMessageBox.critical(None, "Startup Error", QMessageBox.critical(None, "Startup Error",
"Please notify support of this error:\n\n"+ "Please notify support of this error:\n\n"+
traceback.format_exc()) traceback.format_exc())
@ -202,8 +202,8 @@ def _run():
# parse args # parse args
opts, args = parseArgs(sys.argv) opts, args = parseArgs(sys.argv)
opts.base = unicode(opts.base or "", sys.getfilesystemencoding()) opts.base = opts.base or ""
opts.profile = unicode(opts.profile or "", sys.getfilesystemencoding()) opts.profile = opts.profile or ""
# on osx we'll need to add the qt plugins to the search path # on osx we'll need to add the qt plugins to the search path
if isMac and getattr(sys, 'frozen', None): if isMac and getattr(sys, 'frozen', None):

View File

@ -27,7 +27,7 @@ system. It's free and open source.")
abouttext += (_("<a href='%s'>Visit website</a>") % aqt.appWebsite) + \ abouttext += (_("<a href='%s'>Visit website</a>") % aqt.appWebsite) + \
"</span>" "</span>"
abouttext += '<p>' + _("Written by Damien Elmes, with patches, translation,\ abouttext += '<p>' + _("Written by Damien Elmes, with patches, translation,\
testing and design from:<p>%(cont)s") % {'cont': u"""Aaron Harsh, Ádám Szegi, testing and design from:<p>%(cont)s") % {'cont': """Aaron Harsh, Ádám Szegi,
Alex Fraser, Andreas Klauer, Andrew Wright, Bernhard Ibertsberger, C. van Rooyen, Charlene Barina, Alex Fraser, Andreas Klauer, Andrew Wright, Bernhard Ibertsberger, C. van Rooyen, Charlene Barina,
Christian Krause, Christian Rusche, David Smith, Dave Druelinger, Dotan Cohen, Christian Krause, Christian Rusche, David Smith, Dave Druelinger, Dotan Cohen,
Emilio Wuerges, Emmanuel Jarri, Frank Harper, Gregor Skumavc, H. Mijail, Emilio Wuerges, Emmanuel Jarri, Frank Harper, Gregor Skumavc, H. Mijail,

View File

@ -69,7 +69,7 @@ class AddCards(QDialog):
self.connect(self.helpButton, SIGNAL("clicked()"), self.helpRequested) self.connect(self.helpButton, SIGNAL("clicked()"), self.helpRequested)
# history # history
b = bb.addButton( b = bb.addButton(
_("History")+ u" "+downArrow(), ar) _("History")+ " "+downArrow(), ar)
if isMac: if isMac:
sc = "Ctrl+Shift+H" sc = "Ctrl+Shift+H"
else: else:
@ -90,8 +90,8 @@ class AddCards(QDialog):
oldNote = self.editor.note oldNote = self.editor.note
note = self.setupNewNote(set=False) note = self.setupNewNote(set=False)
if oldNote: if oldNote:
oldFields = oldNote.keys() oldFields = list(oldNote.keys())
newFields = note.keys() newFields = list(note.keys())
for n, f in enumerate(note.model()['flds']): for n, f in enumerate(note.model()['flds']):
fieldName = f['name'] fieldName = f['name']
try: try:

View File

@ -3,7 +3,7 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import sys, os, traceback import sys, os, traceback
from cStringIO import StringIO from io import StringIO
import zipfile import zipfile
from aqt.qt import * from aqt.qt import *
from aqt.utils import showInfo, openFolder, isWin, openLink, \ from aqt.utils import showInfo, openFolder, isWin, openLink, \
@ -73,7 +73,7 @@ class AddonManager(object):
frm = aqt.forms.editaddon.Ui_Dialog() frm = aqt.forms.editaddon.Ui_Dialog()
frm.setupUi(d) frm.setupUi(d)
d.setWindowTitle(os.path.basename(path)) d.setWindowTitle(os.path.basename(path))
frm.text.setPlainText(unicode(open(path).read(), "utf8")) frm.text.setPlainText(open(path).read())
d.connect(frm.buttonBox, SIGNAL("accepted()"), d.connect(frm.buttonBox, SIGNAL("accepted()"),
lambda: self.onAcceptEdit(path, frm)) lambda: self.onAcceptEdit(path, frm))
d.exec_() d.exec_()

View File

@ -278,10 +278,10 @@ class DataModel(QAbstractTableModel):
return a return a
def formatQA(self, txt): def formatQA(self, txt):
s = txt.replace("<br>", u" ") s = txt.replace("<br>", " ")
s = s.replace("<br />", u" ") s = s.replace("<br />", " ")
s = s.replace("<div>", u" ") s = s.replace("<div>", " ")
s = s.replace("\n", u" ") s = s.replace("\n", " ")
s = re.sub("\[sound:[^]]+\]", "", s) s = re.sub("\[sound:[^]]+\]", "", s)
s = re.sub("\[\[type:[^]]+\]\]", "", s) s = re.sub("\[\[type:[^]]+\]\]", "", s)
s = stripHTMLMedia(s) s = stripHTMLMedia(s)
@ -518,7 +518,7 @@ class Browser(QMainWindow):
def onSearch(self, reset=True): def onSearch(self, reset=True):
"Careful: if reset is true, the current note is saved." "Careful: if reset is true, the current note is saved."
txt = unicode(self.form.searchEdit.lineEdit().text()).strip() txt = str(self.form.searchEdit.lineEdit().text()).strip()
prompt = _("<type here to search; hit enter to show current deck>") prompt = _("<type here to search; hit enter to show current deck>")
sh = self.mw.pm.profile['searchHistory'] sh = self.mw.pm.profile['searchHistory']
# update search history # update search history
@ -788,12 +788,12 @@ by clicking on one on the left."""))
if self.mw.app.keyboardModifiers() & Qt.AltModifier: if self.mw.app.keyboardModifiers() & Qt.AltModifier:
txt = "-"+txt txt = "-"+txt
if self.mw.app.keyboardModifiers() & Qt.ControlModifier: if self.mw.app.keyboardModifiers() & Qt.ControlModifier:
cur = unicode(self.form.searchEdit.lineEdit().text()) cur = str(self.form.searchEdit.lineEdit().text())
if cur and cur != \ if cur and cur != \
_("<type here to search; hit enter to show current deck>"): _("<type here to search; hit enter to show current deck>"):
txt = cur + " " + txt txt = cur + " " + txt
elif self.mw.app.keyboardModifiers() & Qt.ShiftModifier: elif self.mw.app.keyboardModifiers() & Qt.ShiftModifier:
cur = unicode(self.form.searchEdit.lineEdit().text()) cur = str(self.form.searchEdit.lineEdit().text())
if cur: if cur:
txt = cur + " or " + txt txt = cur + " or " + txt
self.form.searchEdit.lineEdit().setText(txt) self.form.searchEdit.lineEdit().setText(txt)
@ -1365,8 +1365,8 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
self.model.beginReset() self.model.beginReset()
try: try:
changed = self.col.findReplace(sf, changed = self.col.findReplace(sf,
unicode(frm.find.text()), str(frm.find.text()),
unicode(frm.replace.text()), str(frm.replace.text()),
frm.re.isChecked(), frm.re.isChecked(),
field, field,
frm.ignoreCase.isChecked()) frm.ignoreCase.isChecked())
@ -1679,7 +1679,7 @@ class ChangeModel(QDialog):
# check maps # check maps
fmap = self.getFieldMap() fmap = self.getFieldMap()
cmap = self.getTemplateMap() cmap = self.getTemplateMap()
if any(True for c in cmap.values() if c is None): if any(True for c in list(cmap.values()) if c is None):
if not askUser(_("""\ if not askUser(_("""\
Any cards mapped to nothing will be deleted. \ Any cards mapped to nothing will be deleted. \
If a note has no remaining cards, it will be lost. \ If a note has no remaining cards, it will be lost. \
@ -1786,7 +1786,7 @@ class FavouritesLineEdit(QLineEdit):
self.mw = mw self.mw = mw
self.browser = browser self.browser = browser
# add conf if missing # add conf if missing
if not self.mw.col.conf.has_key('savedFilters'): if 'savedFilters' not in self.mw.col.conf:
self.mw.col.conf['savedFilters'] = {} self.mw.col.conf['savedFilters'] = {}
self.button = QToolButton(self) self.button = QToolButton(self)
self.button.setStyleSheet('border: 0px;') self.button.setStyleSheet('border: 0px;')
@ -1818,8 +1818,8 @@ class FavouritesLineEdit(QLineEdit):
def updateButton(self, reset=True): def updateButton(self, reset=True):
# If search text is a saved query, switch to the delete button. # If search text is a saved query, switch to the delete button.
# Otherwise show save button. # Otherwise show save button.
txt = unicode(self.text()).strip() txt = str(self.text()).strip()
for key, value in self.mw.col.conf['savedFilters'].items(): for key, value in list(self.mw.col.conf['savedFilters'].items()):
if txt == value: if txt == value:
self.doSave = False self.doSave = False
self.name = key self.name = key
@ -1835,7 +1835,7 @@ class FavouritesLineEdit(QLineEdit):
self.deleteClicked() self.deleteClicked()
def saveClicked(self): def saveClicked(self):
txt = unicode(self.text()).strip() txt = str(self.text()).strip()
dlg = QInputDialog(self) dlg = QInputDialog(self)
dlg.setInputMode(QInputDialog.TextInput) dlg.setInputMode(QInputDialog.TextInput)
dlg.setLabelText(_("The current search terms will be added as a new " dlg.setLabelText(_("The current search terms will be added as a new "

View File

@ -32,7 +32,7 @@ class CardLayout(QDialog):
if addMode: if addMode:
# save it to DB temporarily # save it to DB temporarily
self.emptyFields = [] self.emptyFields = []
for name, val in note.items(): for name, val in list(note.items()):
if val.strip(): if val.strip():
continue continue
self.emptyFields.append(name) self.emptyFields.append(name)
@ -90,10 +90,10 @@ class CardLayout(QDialog):
# template area # template area
tform = aqt.forms.template.Ui_Form() tform = aqt.forms.template.Ui_Form()
tform.setupUi(left) tform.setupUi(left)
tform.label1.setText(u"") tform.label1.setText("")
tform.label2.setText(u"") tform.label2.setText("")
tform.labelc1.setText(u"") tform.labelc1.setText("")
tform.labelc2.setText(u"") tform.labelc2.setText("")
if self.style().objectName() == "gtk+": if self.style().objectName() == "gtk+":
# gtk+ requires margins in inner layout # gtk+ requires margins in inner layout
tform.tlayout1.setContentsMargins(0, 11, 0, 0) tform.tlayout1.setContentsMargins(0, 11, 0, 0)
@ -167,7 +167,7 @@ Please create a new card type first."""))
flip.setAutoDefault(False) flip.setAutoDefault(False)
l.addWidget(flip) l.addWidget(flip)
c(flip, SIGNAL("clicked()"), self.onFlip) c(flip, SIGNAL("clicked()"), self.onFlip)
more = QPushButton(_("More") + u" "+downArrow()) more = QPushButton(_("More") + " "+downArrow())
more.setAutoDefault(False) more.setAutoDefault(False)
l.addWidget(more) l.addWidget(more)
c(more, SIGNAL("clicked()"), lambda: self.onMore(more)) c(more, SIGNAL("clicked()"), lambda: self.onMore(more))
@ -251,7 +251,7 @@ Please create a new card type first."""))
txt = txt.replace("<hr id=answer>", "") txt = txt.replace("<hr id=answer>", "")
hadHR = origLen != len(txt) hadHR = origLen != len(txt)
def answerRepl(match): def answerRepl(match):
res = self.mw.reviewer.correct(u"exomple", u"an example") res = self.mw.reviewer.correct("exomple", "an example")
if hadHR: if hadHR:
res = "<hr id=answer>" + res res = "<hr id=answer>" + res
return res return res

View File

@ -63,7 +63,7 @@ class DeckBrowser(object):
def _keyHandler(self, evt): def _keyHandler(self, evt):
# currently does nothing # currently does nothing
key = unicode(evt.text()) key = str(evt.text())
def _selDeck(self, did): def _selDeck(self, did):
self.scrollPos = self.web.page().mainFrame().scrollPosition() self.scrollPos = self.web.page().mainFrame().scrollPosition()
@ -287,7 +287,7 @@ where id > ?""", (self.mw.col.sched.dayCutoff-86400)*1000)
return return
try: try:
self.mw.col.decks.rename(deck, newName) self.mw.col.decks.rename(deck, newName)
except DeckRenameError, e: except DeckRenameError as e:
return showWarning(e.description) return showWarning(e.description)
self.show() self.show()
@ -304,7 +304,7 @@ where id > ?""", (self.mw.col.sched.dayCutoff-86400)*1000)
def _dragDeckOnto(self, draggedDeckDid, ontoDeckDid): def _dragDeckOnto(self, draggedDeckDid, ontoDeckDid):
try: try:
self.mw.col.decks.renameForDragAndDrop(draggedDeckDid, ontoDeckDid) self.mw.col.decks.renameForDragAndDrop(draggedDeckDid, ontoDeckDid)
except DeckRenameError, e: except DeckRenameError as e:
return showWarning(e.description) return showWarning(e.description)
self.show() self.show()

View File

@ -42,7 +42,7 @@ class DeckConf(QDialog):
def setupCombos(self): def setupCombos(self):
import anki.consts as cs import anki.consts as cs
f = self.form f = self.form
f.newOrder.addItems(cs.newCardOrderLabels().values()) f.newOrder.addItems(list(cs.newCardOrderLabels().values()))
self.connect(f.newOrder, SIGNAL("currentIndexChanged(int)"), self.connect(f.newOrder, SIGNAL("currentIndexChanged(int)"),
self.onNewOrderChanged) self.onNewOrderChanged)
@ -230,7 +230,7 @@ class DeckConf(QDialog):
################################################## ##################################################
def updateList(self, conf, key, w, minSize=1): def updateList(self, conf, key, w, minSize=1):
items = unicode(w.text()).split(" ") items = str(w.text()).split(" ")
ret = [] ret = []
for i in items: for i in items:
if not i: if not i:

View File

@ -65,12 +65,12 @@ class Downloader(QThread):
try: try:
resp, cont = con.request( resp, cont = con.request(
aqt.appShared + "download/%d" % self.code) aqt.appShared + "download/%d" % self.code)
except Exception, e: except Exception as e:
exc = traceback.format_exc() exc = traceback.format_exc()
try: try:
self.error = unicode(e[0], "utf8", "ignore") self.error = str(e[0])
except: except:
self.error = unicode(exc, "utf8", "ignore") self.error = str(exc)
return return
finally: finally:
remHook("httpRecv", recvEvent) remHook("httpRecv", recvEvent)

View File

@ -38,7 +38,7 @@ class DeckConf(QDialog):
def setupOrder(self): def setupOrder(self):
import anki.consts as cs import anki.consts as cs
self.form.order.addItems(cs.dynOrderLabels().values()) self.form.order.addItems(list(cs.dynOrderLabels().values()))
def loadConf(self): def loadConf(self):
f = self.form f = self.form
@ -94,7 +94,7 @@ it?""")):
return " ".join([str(x) for x in l]) return " ".join([str(x) for x in l])
def userToList(self, w, minSize=1): def userToList(self, w, minSize=1):
items = unicode(w.text()).split(" ") items = str(w.text()).split(" ")
ret = [] ret = []
for i in items: for i in items:
if not i: if not i:

View File

@ -3,9 +3,9 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import re import re
import os import os
import urllib2 import urllib.request, urllib.error, urllib.parse
import ctypes import ctypes
import urllib import urllib.request, urllib.parse, urllib.error
from anki.lang import _ from anki.lang import _
from aqt.qt import * from aqt.qt import *
@ -18,7 +18,7 @@ from aqt.utils import shortcut, showInfo, showWarning, getBase, getFile, \
openHelp, tooltip, downArrow openHelp, tooltip, downArrow
import aqt import aqt
import anki.js import anki.js
from BeautifulSoup import BeautifulSoup from bs4 import BeautifulSoup
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp") pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp")
audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv", "m4a", "3gp", "spx", "oga") audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv", "m4a", "3gp", "spx", "oga")
@ -407,7 +407,7 @@ class Editor(object):
runHook("setupEditorButtons", self) runHook("setupEditorButtons", self)
def enableButtons(self, val=True): def enableButtons(self, val=True):
for b in self._buttons.values(): for b in list(self._buttons.values()):
b.setEnabled(val) b.setEnabled(val)
def disableButtons(self): def disableButtons(self):
@ -491,7 +491,7 @@ class Editor(object):
elif str.startswith("dupes"): elif str.startswith("dupes"):
self.showDupes() self.showDupes()
else: else:
print str print(str)
def mungeHTML(self, txt): def mungeHTML(self, txt):
if txt == "<br>": if txt == "<br>":
@ -536,7 +536,7 @@ class Editor(object):
# will be loaded when page is ready # will be loaded when page is ready
return return
data = [] data = []
for fld, val in self.note.items(): for fld, val in list(self.note.items()):
data.append((fld, self.mw.col.media.escapeImages(val))) data.append((fld, self.mw.col.media.escapeImages(val)))
self.web.eval("setFields(%s, %d);" % ( self.web.eval("setFields(%s, %d);" % (
json.dumps(data), field)) json.dumps(data), field))
@ -614,7 +614,7 @@ class Editor(object):
html = form.textEdit.toPlainText() html = form.textEdit.toPlainText()
# filter html through beautifulsoup so we can strip out things like a # filter html through beautifulsoup so we can strip out things like a
# leading </div> # leading </div>
html = unicode(BeautifulSoup(html)) html = str(BeautifulSoup(html, "html.parser"))
self.note.fields[self.currentField] = html self.note.fields[self.currentField] = html
self.loadNote() self.loadNote()
# focus field so it's saved # focus field so it's saved
@ -702,7 +702,7 @@ to a cloze type first, via Edit>Change Note Type."""))
return return
# find the highest existing cloze # find the highest existing cloze
highest = 0 highest = 0
for name, val in self.note.items(): for name, val in list(self.note.items()):
m = re.findall("\{\{c(\d+)::", val) m = re.findall("\{\{c(\d+)::", val)
if m: if m:
highest = max(highest, sorted([int(x) for x in m])[-1]) highest = max(highest, sorted([int(x) for x in m])[-1])
@ -785,7 +785,7 @@ to a cloze type first, via Edit>Change Note Type."""))
def onRecSound(self): def onRecSound(self):
try: try:
file = getAudio(self.widget) file = getAudio(self.widget)
except Exception, e: except Exception as e:
showWarning(_( showWarning(_(
"Couldn't record audio. Have you installed lame and sox?") + "Couldn't record audio. Have you installed lame and sox?") +
"\n\n" + repr(str(e))) "\n\n" + repr(str(e)))
@ -804,7 +804,7 @@ to a cloze type first, via Edit>Change Note Type."""))
def fnameToLink(self, fname): def fnameToLink(self, fname):
ext = fname.split(".")[-1].lower() ext = fname.split(".")[-1].lower()
if ext in pics: if ext in pics:
name = urllib.quote(fname.encode("utf8")) name = urllib.parse.quote(fname.encode("utf8"))
return '<img src="%s">' % name return '<img src="%s">' % name
else: else:
anki.sound.play(fname) anki.sound.play(fname)
@ -837,22 +837,22 @@ to a cloze type first, via Edit>Change Note Type."""))
self.mw.progress.start( self.mw.progress.start(
immediate=True, parent=self.parentWindow) immediate=True, parent=self.parentWindow)
try: try:
req = urllib2.Request(url, None, { req = urllib.request.Request(url, None, {
'User-Agent': 'Mozilla/5.0 (compatible; Anki)'}) 'User-Agent': 'Mozilla/5.0 (compatible; Anki)'})
filecontents = urllib2.urlopen(req).read() filecontents = urllib.request.urlopen(req).read()
except urllib2.URLError, e: except urllib.error.URLError as e:
showWarning(_("An error occurred while opening %s") % e) showWarning(_("An error occurred while opening %s") % e)
return return
finally: finally:
self.mw.progress.finish() self.mw.progress.finish()
path = unicode(urllib2.unquote(url.encode("utf8")), "utf8") path = urllib.parse.unquote(url)
return self.mw.col.media.writeData(path, filecontents) return self.mw.col.media.writeData(path, filecontents)
# HTML filtering # HTML filtering
###################################################################### ######################################################################
def _filterHTML(self, html, localize=False): def _filterHTML(self, html, localize=False):
doc = BeautifulSoup(html) doc = BeautifulSoup(html, "html.parser")
# remove implicit regular font style from outermost element # remove implicit regular font style from outermost element
if doc.span: if doc.span:
try: try:
@ -919,7 +919,7 @@ to a cloze type first, via Edit>Change Note Type."""))
for elem in "html", "head", "body", "meta": for elem in "html", "head", "body", "meta":
for tag in doc(elem): for tag in doc(elem):
tag.replaceWithChildren() tag.replaceWithChildren()
html = unicode(doc) html = str(doc)
return html return html
# Advanced menu # Advanced menu
@ -1136,7 +1136,7 @@ class EditorWebView(AnkiWebView):
# be URL-encoded, and shouldn't be a file:// url unless they're browsing # be URL-encoded, and shouldn't be a file:// url unless they're browsing
# locally, which we don't support # locally, which we don't support
def _processText(self, mime): def _processText(self, mime):
txt = unicode(mime.text()) txt = str(mime.text())
html = None html = None
# if the user is pasting an image or sound link, convert it to local # if the user is pasting an image or sound link, convert it to local
if self.editor.isURL(txt): if self.editor.isURL(txt):

View File

@ -21,11 +21,8 @@ class ErrorHandler(QObject):
sys.stderr = self sys.stderr = self
def write(self, data): def write(self, data):
# make sure we have unicode
if not isinstance(data, unicode):
data = unicode(data, "utf8", "replace")
# dump to stdout # dump to stdout
sys.stdout.write(data.encode("utf-8")) sys.stdout.write(data)
# save in buffer # save in buffer
self.pool += data self.pool += data
# and update timer # and update timer

View File

@ -88,7 +88,7 @@ class ExportDialog(QDialog):
deck_name = self.decks[self.frm.deck.currentIndex()] deck_name = self.decks[self.frm.deck.currentIndex()]
deck_name = re.sub('[\\\\/?<>:*|"^]', '_', deck_name) deck_name = re.sub('[\\\\/?<>:*|"^]', '_', deck_name)
filename = os.path.join(aqt.mw.pm.base, filename = os.path.join(aqt.mw.pm.base,
u'{0}{1}'.format(deck_name, self.exporter.ext)) '{0}{1}'.format(deck_name, self.exporter.ext))
while 1: while 1:
file = getSaveFile(self, _("Export"), "export", file = getSaveFile(self, _("Export"), "export",
self.exporter.key, self.exporter.ext, self.exporter.key, self.exporter.ext,
@ -104,8 +104,8 @@ class ExportDialog(QDialog):
try: try:
f = open(file, "wb") f = open(file, "wb")
f.close() f.close()
except (OSError, IOError), e: except (OSError, IOError) as e:
showWarning(_("Couldn't save file: %s") % unicode(e)) showWarning(_("Couldn't save file: %s") % str(e))
else: else:
os.unlink(file) os.unlink(file)
exportedMedia = lambda cnt: self.mw.progress.update( exportedMedia = lambda cnt: self.mw.progress.update(

View File

@ -138,7 +138,7 @@ you can enter it here. Use \\t to represent tab."""),
elif d == ":": elif d == ":":
d = _("Colon") d = _("Colon")
else: else:
d = `d` d = repr(d)
txt = _("Fields separated by: %s") % d txt = _("Fields separated by: %s") % d
self.frm.autoDetect.setText(txt) self.frm.autoDetect.setText(txt)
@ -164,7 +164,7 @@ you can enter it here. Use \\t to represent tab."""),
except UnicodeDecodeError: except UnicodeDecodeError:
showUnicodeWarning() showUnicodeWarning()
return return
except Exception, e: except Exception as e:
msg = _("Import failed.\n") msg = _("Import failed.\n")
err = repr(str(e)) err = repr(str(e))
if "1-character string" in err: if "1-character string" in err:
@ -172,7 +172,7 @@ you can enter it here. Use \\t to represent tab."""),
elif "invalidTempFolder" in err: elif "invalidTempFolder" in err:
msg += self.mw.errorHandler.tempFolderMsg() msg += self.mw.errorHandler.tempFolderMsg()
else: else:
msg += unicode(traceback.format_exc(), "ascii", "replace") msg += str(traceback.format_exc(), "ascii", "replace")
showText(msg) showText(msg)
return return
finally: finally:
@ -268,7 +268,7 @@ def onImport(mw):
filter=filt) filter=filt)
if not file: if not file:
return return
file = unicode(file) file = str(file)
importFile(mw, file) importFile(mw, file)
def importFile(mw, file): def importFile(mw, file):
@ -295,7 +295,7 @@ def importFile(mw, file):
except UnicodeDecodeError: except UnicodeDecodeError:
showUnicodeWarning() showUnicodeWarning()
return return
except Exception, e: except Exception as e:
msg = repr(str(e)) msg = repr(str(e))
if msg == "'unknownFormat'": if msg == "'unknownFormat'":
if file.endswith(".anki2"): if file.endswith(".anki2"):
@ -306,7 +306,7 @@ backup, please see the 'Backups' section of the user manual."""))
showWarning(_("Unknown file format.")) showWarning(_("Unknown file format."))
else: else:
msg = _("Import failed. Debugging info:\n") msg = _("Import failed. Debugging info:\n")
msg += unicode(traceback.format_exc(), "ascii", "replace") msg += str(traceback.format_exc())
showText(msg) showText(msg)
return return
finally: finally:
@ -329,7 +329,7 @@ backup, please see the 'Backups' section of the user manual."""))
importer.run() importer.run()
except zipfile.BadZipfile: except zipfile.BadZipfile:
showWarning(invalidZipMsg()) showWarning(invalidZipMsg())
except Exception, e: except Exception as e:
err = repr(str(e)) err = repr(str(e))
if "invalidFile" in err: if "invalidFile" in err:
msg = _("""\ msg = _("""\
@ -342,7 +342,7 @@ Invalid file. Please restore from backup.""")
Unable to import from a read-only file.""")) Unable to import from a read-only file."""))
else: else:
msg = _("Import failed.\n") msg = _("Import failed.\n")
msg += unicode(traceback.format_exc(), "ascii", "replace") msg += str(traceback.format_exc())
showText(msg) showText(msg)
else: else:
log = "\n".join(importer.log) log = "\n".join(importer.log)

View File

@ -56,7 +56,7 @@ class AnkiQt(QMainWindow):
"syncing and add-on loading.")) "syncing and add-on loading."))
# were we given a file to import? # were we given a file to import?
if args and args[0]: if args and args[0]:
self.onAppMsg(unicode(args[0], sys.getfilesystemencoding(), "ignore")) self.onAppMsg(args[0])
# Load profile in a timer so we can let the window finish init and not # Load profile in a timer so we can let the window finish init and not
# close on profile load error. # close on profile load error.
if isMac and qtmajor >= 5: if isMac and qtmajor >= 5:
@ -272,7 +272,7 @@ see the manual for how to restore from an automatic backup.
Debug info: Debug info:
""")+traceback.format_exc()) """)+traceback.format_exc())
self.unloadProfile() self.unloadProfile()
except Exception, e: except Exception as e:
# the custom exception handler won't catch this if we immediately # the custom exception handler won't catch this if we immediately
# unload, so we have to manually handle it # unload, so we have to manually handle it
if "invalidTempFolder" in repr(str(e)): if "invalidTempFolder" in repr(str(e)):
@ -627,7 +627,7 @@ title="%s">%s</button>''' % (
# run standard handler # run standard handler
QMainWindow.keyPressEvent(self, evt) QMainWindow.keyPressEvent(self, evt)
# check global keys # check global keys
key = unicode(evt.text()) key = str(evt.text())
if key == "d": if key == "d":
self.moveToState("deckBrowser") self.moveToState("deckBrowser")
elif key == "s": elif key == "s":
@ -1060,7 +1060,7 @@ will be lost. Continue?"""))
pp = pprint.pprint pp = pprint.pprint
self._captureOutput(True) self._captureOutput(True)
try: try:
exec text exec(text)
except: except:
self._output += traceback.format_exc() self._output += traceback.format_exc()
self._captureOutput(False) self._captureOutput(False)
@ -1109,7 +1109,7 @@ will be lost. Continue?"""))
return return
tgt = tgt or self tgt = tgt or self
for action in tgt.findChildren(QAction): for action in tgt.findChildren(QAction):
txt = unicode(action.text()) txt = str(action.text())
m = re.match("^(.+)\(&.+\)(.+)?", txt) m = re.match("^(.+)\(&.+\)(.+)?", txt)
if m: if m:
action.setText(m.group(1) + (m.group(2) or "")) action.setText(m.group(1) + (m.group(2) or ""))
@ -1157,7 +1157,4 @@ Please ensure a profile is open and Anki is not busy, then try again."""),
if buf == "raise": if buf == "raise":
return return
# import # import
if not isinstance(buf, unicode):
buf = unicode(buf, "utf8", "ignore")
self.handleImport(buf) self.handleImport(buf)

View File

@ -7,6 +7,7 @@ from aqt.utils import showInfo, askUser, getText, maybeHideClose, openHelp
import aqt.modelchooser, aqt.clayout import aqt.modelchooser, aqt.clayout
from anki import stdmodels from anki import stdmodels
from aqt.utils import saveGeom, restoreGeom from aqt.utils import saveGeom, restoreGeom
import collections
class Models(QDialog): class Models(QDialog):
def __init__(self, mw, parent=None, fromMain=False): def __init__(self, mw, parent=None, fromMain=False):
@ -118,8 +119,8 @@ class Models(QDialog):
restoreGeom(d, "modelopts") restoreGeom(d, "modelopts")
d.exec_() d.exec_()
saveGeom(d, "modelopts") saveGeom(d, "modelopts")
self.model['latexPre'] = unicode(frm.latexHeader.toPlainText()) self.model['latexPre'] = str(frm.latexHeader.toPlainText())
self.model['latexPost'] = unicode(frm.latexFooter.toPlainText()) self.model['latexPost'] = str(frm.latexFooter.toPlainText())
def saveModel(self): def saveModel(self):
self.mm.save(self.model) self.mm.save(self.model)
@ -127,7 +128,7 @@ class Models(QDialog):
def _tmpNote(self): def _tmpNote(self):
self.mm.setCurrent(self.model) self.mm.setCurrent(self.model)
n = self.col.newNote(forDeck=False) n = self.col.newNote(forDeck=False)
for name in n.keys(): for name in list(n.keys()):
n[name] = "("+name+")" n[name] = "("+name+")"
try: try:
if "{{cloze:Text}}" in self.model['tmpls'][0]['qfmt']: if "{{cloze:Text}}" in self.model['tmpls'][0]['qfmt']:
@ -171,7 +172,7 @@ class AddModel(QDialog):
# standard models # standard models
self.models = [] self.models = []
for (name, func) in stdmodels.models: for (name, func) in stdmodels.models:
if callable(name): if isinstance(name, collections.Callable):
name = name() name = name()
item = QListWidgetItem(_("Add: %s") % name) item = QListWidgetItem(_("Add: %s") % name)
self.dialog.models.addItem(item) self.dialog.models.addItem(item)

View File

@ -38,7 +38,7 @@ class Overview(object):
if self.mw.state == "overview": if self.mw.state == "overview":
tooltip(_("No cards are due yet.")) tooltip(_("No cards are due yet."))
elif url == "anki": elif url == "anki":
print "anki menu" print("anki menu")
elif url == "opts": elif url == "opts":
self.mw.onDeckConf() self.mw.onDeckConf()
elif url == "cram": elif url == "cram":
@ -64,7 +64,7 @@ class Overview(object):
def _keyHandler(self, evt): def _keyHandler(self, evt):
cram = self.mw.col.decks.current()['dyn'] cram = self.mw.col.decks.current()['dyn']
key = unicode(evt.text()) key = str(evt.text())
if key == "o": if key == "o":
self.mw.onDeckConf() self.mw.onDeckConf()
if key == "r" and cram: if key == "r" and cram:

View File

@ -80,7 +80,7 @@ class Preferences(QDialog):
f.timeLimit.setValue(qc['timeLim']/60.0) f.timeLimit.setValue(qc['timeLim']/60.0)
f.showEstimates.setChecked(qc['estTimes']) f.showEstimates.setChecked(qc['estTimes'])
f.showProgress.setChecked(qc['dueCounts']) f.showProgress.setChecked(qc['dueCounts'])
f.newSpread.addItems(c.newCardSchedulingLabels().values()) f.newSpread.addItems(list(c.newCardSchedulingLabels().values()))
f.newSpread.setCurrentIndex(qc['newSpread']) f.newSpread.setCurrentIndex(qc['newSpread'])
f.useCurrent.setCurrentIndex(int(not qc.get("addToCur", True))) f.useCurrent.setCurrentIndex(int(not qc.get("addToCur", True)))

View File

@ -8,7 +8,7 @@
import os import os
import random import random
import cPickle import pickle
import locale import locale
import re import re
@ -21,7 +21,6 @@ from aqt import appHelpSite
import aqt.forms import aqt.forms
from send2trash import send2trash from send2trash import send2trash
metaConf = dict( metaConf = dict(
ver=0, ver=0,
updates=True, updates=True,
@ -108,7 +107,8 @@ a flash drive.""" % self.base)
def load(self, name, passwd=None): def load(self, name, passwd=None):
data = self.db.scalar("select cast(data as blob) from profiles where name = ?", name) data = self.db.scalar("select cast(data as blob) from profiles where name = ?", name)
prof = cPickle.loads(str(data)) # some profiles created in python2 may not decode properly
prof = pickle.loads(data, errors="ignore")
if prof['key'] and prof['key'] != self._pwhash(passwd): if prof['key'] and prof['key'] != self._pwhash(passwd):
self.name = None self.name = None
return False return False
@ -119,14 +119,14 @@ a flash drive.""" % self.base)
def save(self): def save(self):
sql = "update profiles set data = ? where name = ?" sql = "update profiles set data = ? where name = ?"
self.db.execute(sql, buffer(cPickle.dumps(self.profile)), self.name) self.db.execute(sql, pickle.dumps(self.profile), self.name)
self.db.execute(sql, buffer(cPickle.dumps(self.meta)), "_global") self.db.execute(sql, pickle.dumps(self.meta), "_global")
self.db.commit() self.db.commit()
def create(self, name): def create(self, name):
prof = profileConf.copy() prof = profileConf.copy()
self.db.execute("insert into profiles values (?, ?)", self.db.execute("insert into profiles values (?, ?)",
name, buffer(cPickle.dumps(prof))) name, pickle.dumps(prof))
self.db.commit() self.db.commit()
def remove(self, name): def remove(self, name):
@ -262,9 +262,9 @@ create table if not exists profiles
if not new: if not new:
# load previously created # load previously created
try: try:
data = self.db.scalar( self.meta = pickle.loads(
"select cast(data as blob) from profiles where name = '_global'") self.db.scalar(
self.meta = cPickle.loads(str(data)) "select cast(data as blob) from profiles where name = '_global'"))
return return
except: except:
recover() recover()
@ -272,7 +272,7 @@ create table if not exists profiles
# create a default global profile # create a default global profile
self.meta = metaConf.copy() self.meta = metaConf.copy()
self.db.execute("insert or replace into profiles values ('_global', ?)", self.db.execute("insert or replace into profiles values ('_global', ?)",
buffer(cPickle.dumps(metaConf))) pickle.dumps(metaConf))
self._setDefaultLang() self._setDefaultLang()
return True return True
@ -281,16 +281,16 @@ create table if not exists profiles
if self.firstRun: if self.firstRun:
self.create(_("User 1")) self.create(_("User 1"))
p = os.path.join(self.base, "README.txt") p = os.path.join(self.base, "README.txt")
open(p, "w").write((_("""\ open(p, "w").write(_("""\
This folder stores all of your Anki data in a single location, This folder stores all of your Anki data in a single location,
to make backups easy. To tell Anki to use a different location, to make backups easy. To tell Anki to use a different location,
please see: please see:
%s %s
""") % (appHelpSite + "#startupopts")).encode("utf8")) """) % (appHelpSite + "#startupopts"))
def _pwhash(self, passwd): def _pwhash(self, passwd):
return checksum(unicode(self.meta['id'])+unicode(passwd)) return checksum(str(self.meta['id'])+str(passwd))
# Default language # Default language
###################################################################### ######################################################################
@ -299,8 +299,8 @@ please see:
def _setDefaultLang(self): def _setDefaultLang(self):
# the dialog expects _ to be defined, but we're running before # the dialog expects _ to be defined, but we're running before
# setupLang() has been called. so we create a dummy op for now # setupLang() has been called. so we create a dummy op for now
import __builtin__ import builtins
__builtin__.__dict__['_'] = lambda x: x builtins.__dict__['_'] = lambda x: x
# create dialog # create dialog
class NoCloseDiag(QDialog): class NoCloseDiag(QDialog):
def reject(self): def reject(self):
@ -350,6 +350,6 @@ please see:
def setLang(self, code): def setLang(self, code):
self.meta['defaultLang'] = code self.meta['defaultLang'] = code
sql = "update profiles set data = ? where name = ?" sql = "update profiles set data = ? where name = ?"
self.db.execute(sql, buffer(cPickle.dumps(self.meta)), "_global") self.db.execute(sql, pickle.dumps(self.meta), "_global")
self.db.commit() self.db.commit()
anki.lang.setLang(code, local=False) anki.lang.setLang(code, local=False)

View File

@ -32,8 +32,8 @@ class ProgressManager(object):
try: try:
db.set_progress_handler(self._dbProgress, 10000) db.set_progress_handler(self._dbProgress, 10000)
except: except:
print """\ print("""\
Your pysqlite2 is too old. Anki will appear frozen during long operations.""" Your pysqlite2 is too old. Anki will appear frozen during long operations.""")
def _dbProgress(self): def _dbProgress(self):
"Called from SQLite." "Called from SQLite."

View File

@ -2,12 +2,11 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import division
import difflib import difflib
import re import re
import cgi import cgi
import unicodedata as ucd import unicodedata as ucd
import HTMLParser import html.parser
from anki.lang import _, ngettext from anki.lang import _, ngettext
from aqt.qt import * from aqt.qt import *
@ -285,7 +284,7 @@ The front of this card is empty. Please run Tools>Empty Cards.""")
self.bottom.web.eval("py.link('ans');") self.bottom.web.eval("py.link('ans');")
def _keyHandler(self, evt): def _keyHandler(self, evt):
key = unicode(evt.text()) key = str(evt.text())
if key == "e": if key == "e":
self.mw.onEditCurrent() self.mw.onEditCurrent()
elif (key == " " or evt.key() in (Qt.Key_Return, Qt.Key_Enter)): elif (key == " " or evt.key() in (Qt.Key_Return, Qt.Key_Enter)):
@ -409,12 +408,12 @@ Please run Tools>Empty Cards""")
buf = buf.replace("<hr id=answer>", "") buf = buf.replace("<hr id=answer>", "")
hadHR = len(buf) != origSize hadHR = len(buf) != origSize
# munge correct value # munge correct value
parser = HTMLParser.HTMLParser() parser = html.parser.HTMLParser()
cor = stripHTML(self.mw.col.media.strip(self.typeCorrect)) cor = stripHTML(self.mw.col.media.strip(self.typeCorrect))
# ensure we don't chomp multiple whitespace # ensure we don't chomp multiple whitespace
cor = cor.replace(" ", "&nbsp;") cor = cor.replace(" ", "&nbsp;")
cor = parser.unescape(cor) cor = parser.unescape(cor)
cor = cor.replace(u"\xa0", " ") cor = cor.replace("\xa0", " ")
given = self.typedAnswer given = self.typedAnswer
# compare with typed answer # compare with typed answer
res = self.correct(given, cor, showBad=False) res = self.correct(given, cor, showBad=False)

View File

@ -1,7 +1,6 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import division
import socket import socket
import time import time
import traceback import traceback
@ -324,8 +323,6 @@ class SyncThread(QThread):
self._sync() self._sync()
except: except:
err = traceback.format_exc() err = traceback.format_exc()
if not isinstance(err, unicode):
err = unicode(err, "utf8", "replace")
self.fireEvent("error", err) self.fireEvent("error", err)
finally: finally:
# don't bump mod time unless we explicitly save # don't bump mod time unless we explicitly save
@ -348,7 +345,7 @@ class SyncThread(QThread):
# run sync and check state # run sync and check state
try: try:
ret = self.client.sync() ret = self.client.sync()
except Exception, e: except Exception as e:
log = traceback.format_exc() log = traceback.format_exc()
err = repr(str(e)) err = repr(str(e))
if ("Unable to find the server" in err or if ("Unable to find the server" in err or
@ -357,8 +354,6 @@ class SyncThread(QThread):
else: else:
if not err: if not err:
err = log err = log
if not isinstance(err, unicode):
err = unicode(err, "utf8", "replace")
self.fireEvent("error", err) self.fireEvent("error", err)
return return
if ret == "badAuth": if ret == "badAuth":
@ -429,22 +424,21 @@ class SyncThread(QThread):
###################################################################### ######################################################################
CHUNK_SIZE = 65536 CHUNK_SIZE = 65536
import httplib, httplib2 import http.client, httplib2
from cStringIO import StringIO from io import StringIO
from anki.hooks import runHook from anki.hooks import runHook
# sending in httplib # sending in httplib
def _incrementalSend(self, data): def _incrementalSend(self, data):
print("fixme: _incrementalSend needs updating for python3")
"""Send `data' to the server.""" """Send `data' to the server."""
if self.sock is None: if self.sock is None:
if self.auto_open: if self.auto_open:
self.connect() self.connect()
else: else:
raise httplib.NotConnected() raise http.client.NotConnected()
# if it's not a file object, make it one # if it's not a file object, make it one
if not hasattr(data, 'read'): if not hasattr(data, 'read'):
if isinstance(data, unicode):
data = data.encode("utf8")
data = StringIO(data) data = StringIO(data)
while 1: while 1:
block = data.read(CHUNK_SIZE) block = data.read(CHUNK_SIZE)
@ -453,7 +447,7 @@ def _incrementalSend(self, data):
self.sock.sendall(block) self.sock.sendall(block)
runHook("httpSend", len(block)) runHook("httpSend", len(block))
httplib.HTTPConnection.send = _incrementalSend http.client.HTTPConnection.send = _incrementalSend
# receiving in httplib2 # receiving in httplib2
# this is an augmented version of httplib's request routine that: # this is an augmented version of httplib's request routine that:
@ -461,6 +455,7 @@ httplib.HTTPConnection.send = _incrementalSend
# - calls a hook for each chunk of data so we can update the gui # - calls a hook for each chunk of data so we can update the gui
# - retries only when keep-alive connection is closed # - retries only when keep-alive connection is closed
def _conn_request(self, conn, request_uri, method, body, headers): def _conn_request(self, conn, request_uri, method, body, headers):
print("fixme: _conn_request updating for python3")
for i in range(2): for i in range(2):
try: try:
if conn.sock is None: if conn.sock is None:
@ -475,20 +470,20 @@ def _conn_request(self, conn, request_uri, method, body, headers):
except httplib2.ssl_SSLError: except httplib2.ssl_SSLError:
conn.close() conn.close()
raise raise
except socket.error, e: except socket.error as e:
conn.close() conn.close()
raise raise
except httplib.HTTPException: except http.client.HTTPException:
conn.close() conn.close()
raise raise
try: try:
response = conn.getresponse() response = conn.getresponse()
except httplib.BadStatusLine: except http.client.BadStatusLine:
print "retry bad line" print("retry bad line")
conn.close() conn.close()
conn.connect() conn.connect()
continue continue
except (socket.error, httplib.HTTPException): except (socket.error, http.client.HTTPException):
raise raise
else: else:
content = "" content = ""

View File

@ -68,10 +68,10 @@ class TagCompleter(QCompleter):
self.cursor = None self.cursor = None
def splitPath(self, str): def splitPath(self, str):
str = unicode(str).strip() str = str(str).strip()
str = re.sub(" +", " ", str) str = re.sub(" +", " ", str)
self.tags = self.edit.col.tags.split(str) self.tags = self.edit.col.tags.split(str)
self.tags.append(u"") self.tags.append("")
p = self.edit.cursorPosition() p = self.edit.cursorPosition()
self.cursor = str.count(" ", 0, p) self.cursor = str.count(" ", 0, p)
return [self.tags[self.cursor]] return [self.tags[self.cursor]]
@ -80,9 +80,9 @@ class TagCompleter(QCompleter):
if self.cursor is None: if self.cursor is None:
return self.edit.text() return self.edit.text()
ret = QCompleter.pathFromIndex(self, idx) ret = QCompleter.pathFromIndex(self, idx)
self.tags[self.cursor] = unicode(ret) self.tags[self.cursor] = str(ret)
try: try:
self.tags.remove(u"") self.tags.remove("")
except ValueError: except ValueError:
pass pass
return " ".join(self.tags) return " ".join(self.tags)

View File

@ -1,8 +1,8 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import urllib import urllib.request, urllib.parse, urllib.error
import urllib2 import urllib.request, urllib.error, urllib.parse
import time import time
from aqt.qt import * from aqt.qt import *
@ -32,9 +32,9 @@ class LatestVersionFinder(QThread):
return return
d = self._data() d = self._data()
d['proto'] = 1 d['proto'] = 1
d = urllib.urlencode(d) d = urllib.parse.urlencode(d)
try: try:
f = urllib2.urlopen(aqt.appUpdate, d) f = urllib.request.urlopen(aqt.appUpdate, d)
resp = f.read() resp = f.read()
if not resp: if not resp:
return return

View File

@ -3,7 +3,7 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from aqt.qt import * from aqt.qt import *
import re, os, sys, urllib, subprocess import re, os, sys, urllib.request, urllib.parse, urllib.error, subprocess
import aqt import aqt
from anki.sound import stripSounds from anki.sound import stripSounds
from anki.utils import isWin, isMac, invalidFilename from anki.utils import isWin, isMac, invalidFilename
@ -143,7 +143,7 @@ def askUserDialog(text, buttons, parent=None, help="", title="Anki"):
class GetTextDialog(QDialog): class GetTextDialog(QDialog):
def __init__(self, parent, question, help=None, edit=None, default=u"", \ def __init__(self, parent, question, help=None, edit=None, default="", \
title="Anki", minWidth=400): title="Anki", minWidth=400):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.setWindowTitle(title) self.setWindowTitle(title)
@ -183,21 +183,21 @@ class GetTextDialog(QDialog):
def helpRequested(self): def helpRequested(self):
openHelp(self.help) openHelp(self.help)
def getText(prompt, parent=None, help=None, edit=None, default=u"", title="Anki"): def getText(prompt, parent=None, help=None, edit=None, default="", title="Anki"):
if not parent: if not parent:
parent = aqt.mw.app.activeWindow() or aqt.mw parent = aqt.mw.app.activeWindow() or aqt.mw
d = GetTextDialog(parent, prompt, help=help, edit=edit, d = GetTextDialog(parent, prompt, help=help, edit=edit,
default=default, title=title) default=default, title=title)
d.setWindowModality(Qt.WindowModal) d.setWindowModality(Qt.WindowModal)
ret = d.exec_() ret = d.exec_()
return (unicode(d.l.text()), ret) return (str(d.l.text()), ret)
def getOnlyText(*args, **kwargs): def getOnlyText(*args, **kwargs):
(s, r) = getText(*args, **kwargs) (s, r) = getText(*args, **kwargs)
if r: if r:
return s return s
else: else:
return u"" return ""
# fixme: these utilities could be combined into a single base class # fixme: these utilities could be combined into a single base class
def chooseList(prompt, choices, startrow=0, parent=None): def chooseList(prompt, choices, startrow=0, parent=None):
@ -251,7 +251,7 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None):
def accept(): def accept():
# work around an osx crash # work around an osx crash
#aqt.mw.app.processEvents() #aqt.mw.app.processEvents()
file = unicode(list(d.selectedFiles())[0]) file = str(list(d.selectedFiles())[0])
if dirkey: if dirkey:
dir = os.path.dirname(file) dir = os.path.dirname(file)
aqt.mw.pm.profile[dirkey] = dir aqt.mw.pm.profile[dirkey] = dir
@ -268,8 +268,8 @@ def getSaveFile(parent, title, dir_description, key, ext, fname=None):
config_key = dir_description + 'Directory' config_key = dir_description + 'Directory'
base = aqt.mw.pm.profile.get(config_key, aqt.mw.pm.base) base = aqt.mw.pm.profile.get(config_key, aqt.mw.pm.base)
path = os.path.join(base, fname) path = os.path.join(base, fname)
file = unicode(QFileDialog.getSaveFileName( file = str(QFileDialog.getSaveFileName(
parent, title, path, u"{0} (*{1})".format(key, ext), parent, title, path, "{0} (*{1})".format(key, ext),
options=QFileDialog.DontConfirmOverwrite)) options=QFileDialog.DontConfirmOverwrite))
if file: if file:
# add extension # add extension
@ -350,18 +350,16 @@ def getBase(col):
base = None base = None
mdir = col.media.dir() mdir = col.media.dir()
if isWin and not mdir.startswith("\\\\"): if isWin and not mdir.startswith("\\\\"):
prefix = u"file:///" prefix = "file:///"
else: else:
prefix = u"file://" prefix = "file://"
mdir = mdir.replace("\\", "/") mdir = mdir.replace("\\", "/")
base = prefix + unicode( base = prefix + urllib.parse.quote(mdir) + "/"
urllib.quote(mdir.encode("utf-8")),
"utf-8") + "/"
return '<base href="%s">' % base return '<base href="%s">' % base
def openFolder(path): def openFolder(path):
if isWin: if isWin:
if isinstance(path, unicode): if isinstance(path, str):
path = path.encode(sys.getfilesystemencoding()) path = path.encode(sys.getfilesystemencoding())
subprocess.Popen(["explorer", path]) subprocess.Popen(["explorer", path])
else: else:
@ -387,9 +385,9 @@ def addCloseShortcut(widg):
def downArrow(): def downArrow():
if isWin: if isWin:
return u"" return ""
# windows 10 is lacking the smaller arrow on English installs # windows 10 is lacking the smaller arrow on English installs
return u"" return ""
# Tooltips # Tooltips
###################################################################### ######################################################################

View File

@ -15,10 +15,10 @@ import anki.js
class Bridge(QObject): class Bridge(QObject):
@pyqtSlot(str, result=str) @pyqtSlot(str, result=str)
def run(self, str): def run(self, str):
return unicode(self._bridge(unicode(str))) return self._bridge(str)
@pyqtSlot(str) @pyqtSlot(str)
def link(self, str): def link(self, str):
self._linkHandler(unicode(str)) self._linkHandler(str)
def setBridge(self, func): def setBridge(self, func):
self._bridge = func self._bridge = func
def setLinkHandler(self, func): def setLinkHandler(self, func):
@ -146,7 +146,7 @@ button {
def _jsErr(self, msg, line, srcID): def _jsErr(self, msg, line, srcID):
sys.stdout.write( sys.stdout.write(
(_("JS error on line %(a)d: %(b)s") % (_("JS error on line %(a)d: %(b)s") %
dict(a=line, b=msg+"\n")).encode("utf8")) dict(a=line, b=msg+"\n")))
def _linkHandler(self, url): def _linkHandler(self, url):
self.linkHandler(url.toString()) self.linkHandler(url.toString())

View File

@ -37,6 +37,6 @@ 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 unicode(dst, "utf8") return dst
testDir = os.path.dirname(__file__) testDir = os.path.dirname(__file__)

View File

@ -5,8 +5,8 @@ from tests.shared import getEmptyCol
def test_previewCards(): def test_previewCards():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u'1' f['Front'] = '1'
f['Back'] = u'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
@ -25,8 +25,8 @@ def test_previewCards():
def test_delete(): def test_delete():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u'1' f['Front'] = '1'
f['Back'] = u'2' f['Back'] = '2'
deck.addNote(f) deck.addNote(f)
cid = f.cards()[0].id cid = f.cards()[0].id
deck.reset() deck.reset()
@ -41,8 +41,8 @@ def test_delete():
def test_misc(): def test_misc():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u'1' f['Front'] = '1'
f['Back'] = u'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']
@ -51,8 +51,8 @@ def test_misc():
def test_genrem(): def test_genrem():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u'1' f['Front'] = '1'
f['Back'] = u'' 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()
@ -80,7 +80,7 @@ def test_gendeck():
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'] = u'{{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
@ -89,11 +89,11 @@ def test_gendeck():
cloze['did'] = newId cloze['did'] = newId
d.models.save(cloze) d.models.save(cloze)
# a newly generated card should share the first card's deck # a newly generated card should share the first card's deck
f['Text'] += u'{{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'] += u'{{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,7 +101,7 @@ def test_gendeck():
c = f.cards()[1] c = f.cards()[1]
c.did = newId c.did = newId
c.flush() c.flush()
f['Text'] += u'{{c4::four}}' f['Text'] += '{{c4::four}}'
f.flush() f.flush()
assert f.cards()[3].did == newId assert f.cards()[3].did == newId

View File

@ -37,14 +37,14 @@ def test_openReadOnly():
os.chmod(newPath, 0) os.chmod(newPath, 0)
assertException(Exception, assertException(Exception,
lambda: aopen(newPath)) lambda: aopen(newPath))
os.chmod(newPath, 0666) 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'] = u"one"; f['Back'] = u"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
@ -62,7 +62,7 @@ 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'] = u"three"; f['Back'] = u"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
@ -73,7 +73,7 @@ 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'] = u"one"; f2['Back'] = u"" 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'] = " "
@ -82,12 +82,12 @@ def test_noteAddDelete():
def test_fieldChecksum(): def test_fieldChecksum():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u"new"; f['Back'] = u"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'] = u"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)
@ -95,10 +95,10 @@ def test_fieldChecksum():
def test_addDelTags(): def test_addDelTags():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u"1" f['Front'] = "1"
deck.addNote(f) deck.addNote(f)
f2 = deck.newNote() f2 = deck.newNote()
f2['Front'] = u"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")

View File

@ -46,7 +46,7 @@ def test_remove():
# 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'] = u"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]
@ -92,7 +92,7 @@ def test_renameForDragAndDrop():
d = getEmptyCol() d = getEmptyCol()
def deckNames(): def deckNames():
return [ name for name in sorted(d.decks.allNames()) if name <> u'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')

View File

@ -4,7 +4,7 @@ import nose, os, tempfile
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
@ -14,11 +14,11 @@ def setup1():
global deck global deck
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = ["tag", "tag2"] f['Front'] = "foo"; f['Back'] = "bar"; 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'] = u"baz"; f['Back'] = u"qux" f['Front'] = "baz"; f['Back'] = "qux"
f.model()['did'] = deck.decks.id("new deck") f.model()['did'] = deck.decks.id("new deck")
deck.addNote(f) deck.addNote(f)
@ -37,7 +37,7 @@ def test_export_anki():
# export # export
e = AnkiExporter(deck) e = AnkiExporter(deck)
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2") fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
newname = unicode(newname) newname = str(newname)
os.close(fd) os.close(fd)
os.unlink(newname) os.unlink(newname)
e.exportInto(newname) e.exportInto(newname)
@ -57,7 +57,7 @@ def test_export_anki():
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 = unicode(newname) newname = str(newname)
os.close(fd) os.close(fd)
os.unlink(newname) os.unlink(newname)
e.did = 1 e.did = 1
@ -68,13 +68,13 @@ def test_export_anki():
@nose.with_setup(setup1) @nose.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
open(os.path.join(deck.media.dir(), u"今日.mp3"), "w").write("test") open(os.path.join(deck.media.dir(), "今日.mp3"), "w").write("test")
n = deck.newNote() n = deck.newNote()
n['Front'] = u'[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")
newname = unicode(newname) newname = str(newname)
os.close(fd) os.close(fd)
os.unlink(newname) os.unlink(newname)
e.exportInto(newname) e.exportInto(newname)
@ -83,7 +83,7 @@ def test_export_ankipkg():
def test_export_anki_due(): def test_export_anki_due():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u"foo" f['Front'] = "foo"
deck.addNote(f) deck.addNote(f)
deck.crt -= 86400*10 deck.crt -= 86400*10
deck.sched.reset() deck.sched.reset()
@ -99,7 +99,7 @@ def test_export_anki_due():
e = AnkiExporter(deck) e = AnkiExporter(deck)
e.includeSched = True e.includeSched = True
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2") fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
newname = unicode(newname) newname = str(newname)
os.close(fd) os.close(fd)
os.unlink(newname) os.unlink(newname)
e.exportInto(newname) e.exportInto(newname)
@ -124,7 +124,7 @@ def test_export_anki_due():
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")
f = unicode(f) f = str(f)
os.close(fd) os.close(fd)
os.unlink(f) os.unlink(f)
e.exportInto(f) e.exportInto(f)

View File

@ -22,21 +22,21 @@ def test_parse():
def test_findCards(): def test_findCards():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u'dog' f['Front'] = 'dog'
f['Back'] = u'cat' f['Back'] = 'cat'
f.tags.append(u"monkey") f.tags.append("monkey")
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'] = u'goats are fun' f['Front'] = 'goats are fun'
f['Back'] = u'sheep' f['Back'] = 'sheep'
f.tags.append(u"sheep goat horse") f.tags.append("sheep goat horse")
deck.addNote(f) deck.addNote(f)
f2id = f.id f2id = f.id
f = deck.newNote() f = deck.newNote()
f['Front'] = u'cat' f['Front'] = 'cat'
f['Back'] = u'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
@ -46,8 +46,8 @@ def test_findCards():
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
f = deck.newNote() f = deck.newNote()
f['Front'] = u'test' f['Front'] = 'test'
f['Back'] = u'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
@ -131,8 +131,8 @@ def test_findCards():
assert len(deck.findCards("deck:*cefault")) == 0 assert len(deck.findCards("deck:*cefault")) == 0
# full search # full search
f = deck.newNote() f = deck.newNote()
f['Front'] = u'hello<b>world</b>' f['Front'] = 'hello<b>world</b>'
f['Back'] = u'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
@ -195,8 +195,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'] = u'' f['Front'] = ''
f['Back'] = u'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
@ -218,12 +218,12 @@ def test_findCards():
def test_findReplace(): def test_findReplace():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u'foo' f['Front'] = 'foo'
f['Back'] = u'bar' f['Back'] = 'bar'
deck.addNote(f) deck.addNote(f)
f2 = deck.newNote() f2 = deck.newNote()
f2['Front'] = u'baz' f2['Front'] = 'baz'
f2['Back'] = u'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
@ -245,20 +245,20 @@ def test_findReplace():
def test_findDupes(): def test_findDupes():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u'foo' f['Front'] = 'foo'
f['Back'] = u'bar' f['Back'] = 'bar'
deck.addNote(f) deck.addNote(f)
f2 = deck.newNote() f2 = deck.newNote()
f2['Front'] = u'baz' f2['Front'] = 'baz'
f2['Back'] = u'bar' f2['Back'] = 'bar'
deck.addNote(f2) deck.addNote(f2)
f3 = deck.newNote() f3 = deck.newNote()
f3['Front'] = u'quux' f3['Front'] = 'quux'
f3['Back'] = u'bar' f3['Back'] = 'bar'
deck.addNote(f3) deck.addNote(f3)
f4 = deck.newNote() f4 = deck.newNote()
f4['Front'] = u'quuux' f4['Front'] = 'quuux'
f4['Back'] = u'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"

View File

@ -2,9 +2,8 @@
import os import os
from tests.shared import getUpgradeDeckPath, getEmptyCol from tests.shared import getUpgradeDeckPath, getEmptyCol
from anki.upgrade import Upgrader
from anki.utils import ids2str from anki.utils import ids2str
from anki.importing import Anki1Importer, Anki2Importer, TextImporter, \ from anki.importing import Anki2Importer, TextImporter, \
SupermemoXmlImporter, MnemosyneImporter, AnkiPackageImporter SupermemoXmlImporter, MnemosyneImporter, AnkiPackageImporter
testDir = os.path.dirname(__file__) testDir = os.path.dirname(__file__)
@ -12,43 +11,6 @@ testDir = os.path.dirname(__file__)
srcNotes=None srcNotes=None
srcCards=None srcCards=None
def test_anki2():
global srcNotes, srcCards
# get the deck to import
tmp = getUpgradeDeckPath()
u = Upgrader()
u.check(tmp)
src = u.upgrade()
srcpath = src.path
srcNotes = src.noteCount()
srcCards = src.cardCount()
srcRev = src.db.scalar("select count() from revlog")
# add a media file for testing
open(os.path.join(src.media.dir(), "_foo.jpg"), "w").write("foo")
src.close()
# create a new empty deck
dst = getEmptyCol()
# import src into dst
imp = Anki2Importer(dst, srcpath)
imp.run()
def check():
assert dst.noteCount() == srcNotes
assert dst.cardCount() == srcCards
assert srcRev == dst.db.scalar("select count() from revlog")
mids = [int(x) for x in dst.models.models.keys()]
assert not dst.db.scalar(
"select count() from notes where mid not in "+ids2str(mids))
assert not dst.db.scalar(
"select count() from cards where nid not in (select id from notes)")
assert not dst.db.scalar(
"select count() from revlog where cid not in (select id from cards)")
assert dst.fixIntegrity()[0].startswith("Database rebuilt")
check()
# importing should be idempotent
imp.run()
check()
assert len(os.listdir(dst.media.dir())) == 1
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
@ -96,7 +58,7 @@ def test_anki2_mediadupes():
def test_apkg(): def test_apkg():
tmp = getEmptyCol() tmp = getEmptyCol()
apkg = unicode(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()
@ -113,65 +75,6 @@ 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_anki1():
# get the deck path to import
tmp = getUpgradeDeckPath()
# make sure media is imported properly through the upgrade
mdir = tmp.replace(".anki2", ".media")
if not os.path.exists(mdir):
os.mkdir(mdir)
open(os.path.join(mdir, "_foo.jpg"), "w").write("foo")
# create a new empty deck
dst = getEmptyCol()
# import src into dst
imp = Anki1Importer(dst, tmp)
imp.run()
def check():
assert dst.noteCount() == srcNotes
assert dst.cardCount() == srcCards
assert len(os.listdir(dst.media.dir())) == 1
check()
# importing should be idempotent
imp = Anki1Importer(dst, tmp)
imp.run()
check()
def test_anki1_diffmodels():
# create a new empty deck
dst = getEmptyCol()
# import the 1 card version of the model
tmp = getUpgradeDeckPath("diffmodels1.anki")
imp = Anki1Importer(dst, tmp)
imp.run()
before = dst.noteCount()
# repeating the process should do nothing
imp = Anki1Importer(dst, tmp)
imp.run()
assert before == dst.noteCount()
# then the 2 card version
tmp = getUpgradeDeckPath("diffmodels2.anki")
imp = Anki1Importer(dst, tmp)
imp.run()
after = dst.noteCount()
# as the model schemas differ, should have been imported as new model
assert after == before + 1
# repeating the process should do nothing
beforeModels = len(dst.models.all())
imp = Anki1Importer(dst, tmp)
imp.run()
after = dst.noteCount()
assert after == before + 1
assert beforeModels == len(dst.models.all())
def test_suspended():
# create a new empty deck
dst = getEmptyCol()
# import the 1 card version of the model
tmp = getUpgradeDeckPath("suspended12.anki")
imp = Anki1Importer(dst, tmp)
imp.run()
assert dst.db.scalar("select due from cards") < 0
def test_anki2_diffmodels(): def test_anki2_diffmodels():
# create a new empty deck # create a new empty deck
dst = getEmptyCol() dst = getEmptyCol()
@ -254,7 +157,7 @@ def test_anki2_updates():
def test_csv(): def test_csv():
deck = getEmptyCol() deck = getEmptyCol()
file = unicode(os.path.join(testDir, "support/text-2fields.txt")) file = str(os.path.join(testDir, "support/text-2fields.txt"))
i = TextImporter(deck, file) i = TextImporter(deck, file)
i.initMapping() i.initMapping()
i.run() i.run()
@ -299,7 +202,7 @@ def test_csv2():
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 = unicode(os.path.join(testDir, "support/text-update.txt")) file = str(os.path.join(testDir, "support/text-update.txt"))
i = TextImporter(deck, file) i = TextImporter(deck, file)
i.initMapping() i.initMapping()
i.run() i.run()
@ -311,7 +214,7 @@ def test_csv2():
def test_supermemo_xml_01_unicode(): def test_supermemo_xml_01_unicode():
deck = getEmptyCol() deck = getEmptyCol()
file = unicode(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()
@ -325,7 +228,7 @@ def test_supermemo_xml_01_unicode():
def test_mnemo(): def test_mnemo():
deck = getEmptyCol() deck = getEmptyCol()
file = unicode(os.path.join(testDir, "support/mnemo.db")) file = str(os.path.join(testDir, "support/mnemo.db"))
i = MnemosyneImporter(deck, file) i = MnemosyneImporter(deck, file)
i.run() i.run()
assert deck.cardCount() == 7 assert deck.cardCount() == 7

View File

@ -1,6 +1,9 @@
# coding: utf-8 # coding: utf-8
import os import os
import shutil
from tests.shared import getEmptyCol from tests.shared import getEmptyCol
from anki.utils import stripHTML from anki.utils import stripHTML
@ -11,7 +14,7 @@ def test_latex():
anki.latex.latexCmds[0][0] = "nolatex" anki.latex.latexCmds[0][0] = "nolatex"
# add a note with latex # add a note with latex
f = d.newNote() f = d.newNote()
f['Front'] = u"[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
@ -20,10 +23,8 @@ def test_latex():
assert "executing nolatex" in msg assert "executing nolatex" in msg
assert "installed" in msg assert "installed" in msg
# check if we have latex installed, and abort test if we don't # check if we have latex installed, and abort test if we don't
for cmd in ("latex", "dvipng"): if not shutil.which("latex") or not shutil.which("dvipng"):
if (not os.path.exists("/usr/bin/"+cmd) and print("aborting test; %s is not installed" % cmd)
not os.path.exists("/usr/texbin/"+cmd)):
print "aborting test; %s is not installed" % cmd
return return
# fix path # fix path
anki.latex.latexCmds[0][0] = "latex" anki.latex.latexCmds[0][0] = "latex"
@ -33,13 +34,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'] = u"[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'] = u" [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]
@ -48,7 +49,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'] = u"[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]"
@ -107,7 +108,7 @@ def test_good_latex_command_works():
def _test_includes_bad_command(bad): def _test_includes_bad_command(bad):
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u'[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)

View File

@ -4,14 +4,14 @@ import tempfile
import os import os
import time import time
from shared import getEmptyCol, testDir from .shared import getEmptyCol, testDir
# copying files to media folder # copying files to media folder
def test_add(): def test_add():
d = getEmptyCol() d = getEmptyCol()
dir = tempfile.mkdtemp(prefix="anki") dir = tempfile.mkdtemp(prefix="anki")
path = os.path.join(dir, u"foo.jpg") path = os.path.join(dir, "foo.jpg")
open(path, "w").write("hello") open(path, "w").write("hello")
# new file, should preserve name # new file, should preserve name
assert d.media.addFile(path) == "foo.jpg" assert d.media.addFile(path) == "foo.jpg"
@ -24,7 +24,7 @@ def test_add():
def test_strings(): def test_strings():
d = getEmptyCol() d = getEmptyCol()
mf = d.media.filesInStr mf = d.media.filesInStr
mid = d.models.models.keys()[0] mid = list(d.models.models.keys())[0]
assert mf(mid, "aoeu") == [] assert mf(mid, "aoeu") == []
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"]
@ -50,18 +50,18 @@ def test_deckIntegration():
# create a media dir # create a media dir
d.media.dir() d.media.dir()
# put a file into it # put a file into it
file = unicode(os.path.join(testDir, "support/fake.png")) file = str(os.path.join(testDir, "support/fake.png"))
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'] = u"one"; f['Back'] = u"<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'] = u"one"; f['Back'] = u"<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
open(os.path.join(d.media.dir(), "foo.jpg"), "wb").write("test") open(os.path.join(d.media.dir(), "foo.jpg"), "w").write("test")
# check media # check media
ret = d.media.check() ret = d.media.check()
assert ret[0] == ["fake2.png"] assert ret[0] == ["fake2.png"]
@ -78,7 +78,7 @@ def test_changes():
assert not list(removed()) assert not list(removed())
# add a file # add a file
dir = tempfile.mkdtemp(prefix="anki") dir = tempfile.mkdtemp(prefix="anki")
path = os.path.join(dir, u"foo.jpg") path = os.path.join(dir, "foo.jpg")
open(path, "w").write("hello") open(path, "w").write("hello")
time.sleep(1) time.sleep(1)
path = d.media.addFile(path) path = d.media.addFile(path)
@ -106,8 +106,8 @@ def test_changes():
def test_illegal(): def test_illegal():
d = getEmptyCol() d = getEmptyCol()
aString = u"a:b|cd\\e/f\0g*h" aString = "a:b|cd\\e/f\0g*h"
good = u"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")

View File

@ -6,8 +6,8 @@ from anki.utils import stripHTML, joinFields
def test_modelDelete(): def test_modelDelete():
deck = getEmptyCol() deck = getEmptyCol()
f = deck.newNote() f = deck.newNote()
f['Front'] = u'1' f['Front'] = '1'
f['Back'] = u'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())
@ -29,8 +29,8 @@ def test_modelCopy():
def test_fields(): def test_fields():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u'1' f['Front'] = '1'
f['Back'] = u'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
@ -82,8 +82,8 @@ def test_templates():
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
f = d.newNote() f = d.newNote()
f['Front'] = u'1' f['Front'] = '1'
f['Back'] = u'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()
@ -121,7 +121,7 @@ def test_cloze_ordinals():
d.models.remTemplate(m, m['tmpls'][0]) d.models.remTemplate(m, m['tmpls'][0])
f = d.newNote() f = d.newNote()
f['Text'] = u'{{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()
@ -136,7 +136,7 @@ def test_text():
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'] = u'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()
@ -146,7 +146,7 @@ def test_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'] = u'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()
@ -221,8 +221,8 @@ def test_modelChange():
mm.addTemplate(m, t) mm.addTemplate(m, t)
mm.save(m) mm.save(m)
f = deck.newNote() f = deck.newNote()
f['Front'] = u'f' f['Front'] = 'f'
f['Back'] = u'b123' f['Back'] = 'b123'
deck.addNote(f) deck.addNote(f)
# switch fields # switch fields
map = {0: 1, 1: 0} map = {0: 1, 1: 0}
@ -267,8 +267,8 @@ def test_modelChange():
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'] = u'f2' f['Front'] = 'f2'
f['Back'] = u'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

View File

@ -28,7 +28,7 @@ def test_new():
assert d.sched.newCount == 0 assert d.sched.newCount == 0
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"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
@ -99,7 +99,7 @@ def test_newLimits():
def test_newBoxes(): def test_newBoxes():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
c = d.sched.getCard() c = d.sched.getCard()
@ -113,7 +113,7 @@ def test_learn():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"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,7 +126,7 @@ def test_learn():
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
@ -135,7 +135,7 @@ def test_learn():
# 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
@ -146,7 +146,7 @@ def test_learn():
# 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
@ -187,10 +187,10 @@ def test_learn_collapsed():
d = getEmptyCol() d = getEmptyCol()
# add 2 notes # add 2 notes
f = d.newNote() f = d.newNote()
f['Front'] = u"1" f['Front'] = "1"
f = d.addNote(f) f = d.addNote(f)
f = d.newNote() f = d.newNote()
f['Front'] = u"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")
@ -213,7 +213,7 @@ def test_learn_day():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = u"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()
@ -222,7 +222,7 @@ def test_learn_day():
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
@ -271,7 +271,7 @@ def test_reviews():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"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]
@ -362,7 +362,7 @@ def test_reviews():
def test_button_spacing(): def test_button_spacing():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"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]
@ -385,7 +385,7 @@ def test_overdue_lapse():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = u"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]
@ -420,7 +420,7 @@ 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()
f = d.newNote() f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"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()
@ -436,7 +436,7 @@ def test_finished():
def test_nextIvl(): def test_nextIvl():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"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)
@ -492,7 +492,7 @@ def test_nextIvl():
def test_misc(): def test_misc():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
# burying # burying
@ -506,7 +506,7 @@ def test_misc():
def test_suspend(): def test_suspend():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
# suspending # suspending
@ -549,7 +549,7 @@ def test_suspend():
def test_cram(): def test_cram():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.ivl = 100 c.ivl = 100
@ -657,7 +657,7 @@ def test_cram():
def test_cram_rem(): def test_cram_rem():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"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")
@ -678,7 +678,7 @@ def test_cram_resched():
# add card # add card
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = "one"
d.addNote(f) d.addNote(f)
# cram deck # cram deck
did = d.decks.newDyn("Cram") did = d.decks.newDyn("Cram")
@ -806,7 +806,7 @@ def test_ordcycle():
def test_counts_idx(): def test_counts_idx():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"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)
@ -828,7 +828,7 @@ def test_counts_idx():
def test_repCounts(): def test_repCounts():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"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
@ -846,7 +846,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'] = u"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
@ -858,14 +858,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'] = u"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'] = u"three" f['Front'] = "three"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -907,7 +907,7 @@ def test_collapse():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
# test collapsing # test collapsing
@ -921,11 +921,11 @@ 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'] = u"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'] = u"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
@ -935,12 +935,12 @@ 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'] = u"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'] = u"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()
@ -980,16 +980,16 @@ 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'] = u"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'] = u"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'] = u"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
@ -1004,10 +1004,10 @@ 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'] = u"one" f['Front'] = "one"
d.addNote(f) d.addNote(f)
f2 = d.newNote() f2 = d.newNote()
f2['Front'] = u"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
@ -1022,10 +1022,10 @@ def test_reorder():
assert f.cards()[0].due == 1 assert f.cards()[0].due == 1
# shifting # shifting
f3 = d.newNote() f3 = d.newNote()
f3['Front'] = u"three" f3['Front'] = "three"
d.addNote(f3) d.addNote(f3)
f4 = d.newNote() f4 = d.newNote()
f4['Front'] = u"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
@ -1041,7 +1041,7 @@ def test_reorder():
def test_forget(): def test_forget():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"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
@ -1055,7 +1055,7 @@ def test_forget():
def test_resched(): def test_resched():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"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,7 +1072,7 @@ def test_norelearn():
d = getEmptyCol() d = getEmptyCol()
# add a note # add a note
f = d.newNote() f = d.newNote()
f['Front'] = u"one" f['Front'] = "one"
d.addNote(f) d.addNote(f)
c = f.cards()[0] c = f.cards()[0]
c.type = 2 c.type = 2
@ -1092,7 +1092,7 @@ def test_norelearn():
def test_failmult(): def test_failmult():
d = getEmptyCol() d = getEmptyCol()
f = d.newNote() f = d.newNote()
f['Front'] = u"one"; f['Back'] = u"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

View File

@ -21,14 +21,14 @@ def setup_basic():
deck1 = getEmptyCol() deck1 = getEmptyCol()
# add a note to deck 1 # add a note to deck 1
f = deck1.newNote() f = deck1.newNote()
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = [u"foo"] f['Front'] = "foo"; f['Back'] = "bar"; f.tags = ["foo"]
deck1.addNote(f) deck1.addNote(f)
# answer it # answer it
deck1.reset(); deck1.sched.answerCard(deck1.sched.getCard(), 4) deck1.reset(); deck1.sched.answerCard(deck1.sched.getCard(), 4)
# repeat for deck2 # repeat for deck2
deck2 = getEmptyDeckWith(server=True) deck2 = getEmptyDeckWith(server=True)
f = deck2.newNote() f = deck2.newNote()
f['Front'] = u"bar"; f['Back'] = u"bar"; f.tags = [u"bar"] f['Front'] = "bar"; f['Back'] = "bar"; f.tags = ["bar"]
deck2.addNote(f) deck2.addNote(f)
deck2.reset(); deck2.sched.answerCard(deck2.sched.getCard(), 4) deck2.reset(); deck2.sched.answerCard(deck2.sched.getCard(), 4)
# start with same schema and sync time # start with same schema and sync time
@ -223,7 +223,7 @@ def test_threeway():
# client 1 adds a card at time 1 # client 1 adds a card at time 1
time.sleep(1) time.sleep(1)
f = deck1.newNote() f = deck1.newNote()
f['Front'] = u"1"; f['Front'] = "1";
deck1.addNote(f) deck1.addNote(f)
deck1.save() deck1.save()
# at time 2, client 2 syncs to server # at time 2, client 2 syncs to server
@ -249,7 +249,7 @@ def test_threeway2():
# create collection 1 with a single note # create collection 1 with a single note
c1 = getEmptyCol() c1 = getEmptyCol()
f = c1.newNote() f = c1.newNote()
f['Front'] = u"startingpoint" f['Front'] = "startingpoint"
nid = f.id nid = f.id
c1.addNote(f) c1.addNote(f)
cid = f.cards()[0].id cid = f.cards()[0].id
@ -329,9 +329,9 @@ def _test_speed():
deck1.scm = deck2.scm = 0 deck1.scm = deck2.scm = 0
server = LocalServer(deck2) server = LocalServer(deck2)
client = Syncer(deck1, server) client = Syncer(deck1, server)
print "load %d" % ((time.time() - t)*1000); t = time.time() print("load %d" % ((time.time() - t)*1000)); t = time.time()
assert client.sync() == "success" assert client.sync() == "success"
print "sync %d" % ((time.time() - t)*1000); t = time.time() print("sync %d" % ((time.time() - t)*1000)); t = time.time()
@nose.with_setup(setup_modified) @nose.with_setup(setup_modified)
def test_filtered_delete(): def test_filtered_delete():

View File

@ -27,7 +27,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'] = u"one" f['Front'] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert d.undoName() == "add" assert d.undoName() == "add"
@ -39,7 +39,7 @@ 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'] = u"one" f['Front'] = "one"
d.addNote(f) d.addNote(f)
d.reset() d.reset()
assert not d.undoName() assert not d.undoName()
@ -62,7 +62,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'] = u"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)

View File

@ -22,5 +22,5 @@ else
args="" args=""
echo "Call with coverage=1 to run coverage tests" echo "Call with coverage=1 to run coverage tests"
fi fi
(cd $dir && nosetests -vs $lim $args --cover-package=anki) (cd $dir && nosetests3 -vs $lim $args --cover-package=anki)