From 15b349e3a8b34bf80c134b406c9b90f61250ee9e Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 12 May 2016 14:45:35 +1000 Subject: [PATCH] start port to python 3 unit tests pass and main screens of GUI load --- anki/__init__.py | 28 ++------ anki/anki | 15 ----- anki/collection.py | 16 ++--- anki/db.py | 17 ++--- anki/decks.py | 22 +++---- anki/exporting.py | 4 +- anki/find.py | 40 ++++++++---- anki/importing/anki2.py | 2 +- anki/importing/apkg.py | 4 +- anki/importing/csvfile.py | 13 ++-- anki/importing/mnemo.py | 2 +- anki/importing/noteimp.py | 2 +- anki/importing/supermemo_xml.py | 67 ++++++++++---------- anki/lang.py | 88 +++++++++++++------------- anki/latex.py | 7 +- anki/media.py | 26 ++------ anki/models.py | 12 ++-- anki/notes.py | 4 +- anki/sched.py | 1 - anki/sound.py | 10 +-- anki/stats.py | 5 +- anki/sync.py | 32 +++++----- anki/tags.py | 12 ++-- anki/template/README.rst | 78 ----------------------- anki/template/template.py | 4 +- anki/template/view.py | 4 +- anki/utils.py | 37 +++-------- aqt/__init__.py | 26 ++++---- aqt/about.py | 2 +- aqt/addcards.py | 6 +- aqt/addons.py | 4 +- aqt/browser.py | 28 ++++---- aqt/clayout.py | 14 ++-- aqt/deckbrowser.py | 6 +- aqt/deckconf.py | 4 +- aqt/downloader.py | 6 +- aqt/dyndeckconf.py | 4 +- aqt/editor.py | 34 +++++----- aqt/errors.py | 5 +- aqt/exporting.py | 6 +- aqt/importing.py | 16 ++--- aqt/main.py | 13 ++-- aqt/models.py | 9 +-- aqt/overview.py | 4 +- aqt/preferences.py | 2 +- aqt/profiles.py | 36 +++++------ aqt/progress.py | 4 +- aqt/reviewer.py | 9 ++- aqt/sync.py | 29 ++++----- aqt/tagedit.py | 8 +-- aqt/update.py | 8 +-- aqt/utils.py | 30 ++++----- aqt/webview.py | 6 +- tests/shared.py | 2 +- tests/test_cards.py | 24 +++---- tests/test_collection.py | 16 ++--- tests/test_decks.py | 4 +- tests/test_exporting.py | 22 +++---- tests/test_find.py | 52 +++++++-------- tests/test_importing.py | 109 ++------------------------------ tests/test_latex.py | 21 +++--- tests/test_media.py | 20 +++--- tests/test_models.py | 26 ++++---- tests/test_sched.py | 82 ++++++++++++------------ tests/test_sync.py | 12 ++-- tests/test_undo.py | 6 +- tools/tests.sh | 2 +- 67 files changed, 510 insertions(+), 759 deletions(-) delete mode 100755 anki/anki delete mode 100644 anki/template/README.rst diff --git a/anki/__init__.py b/anki/__init__.py index 312cfb84e..20d47bf58 100644 --- a/anki/__init__.py +++ b/anki/__init__.py @@ -5,30 +5,12 @@ import sys import os import platform +import json -if sys.version_info[0] > 2: - raise Exception("Anki should be run with Python 2") -elif sys.version_info[1] < 6: - raise Exception("Anki requires Python 2.6+") -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]))) +if sys.version_info[0] < 3: + raise Exception("Anki should be run with Python 3") +elif sys.version_info[1] < 4: + raise Exception("Anki requires Python 3.4+") version="2.0.36" # build scripts grep this line, so preserve formatting from anki.storage import Collection diff --git a/anki/anki b/anki/anki deleted file mode 100755 index a180ab664..000000000 --- a/anki/anki +++ /dev/null @@ -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() diff --git a/anki/collection.py b/anki/collection.py index 687b6baa7..dae76911d 100644 --- a/anki/collection.py +++ b/anki/collection.py @@ -351,7 +351,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""", ts += 1 # note any cards that need removing if nid in have: - for ord, id in have[nid].items(): + for ord, id in list(have[nid].items()): if ord not in avail: rem.append(id) # 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.ord = template['ord'] # 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'] else: 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]) fields = {} 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['Tags'] = data[5].strip() fields['Type'] = model['name'] @@ -820,16 +820,16 @@ and queue = 0""", intTime(), self.usn()) if not self._debugLog: return def customRepr(x): - if isinstance(x, basestring): + if isinstance(x, str): return x return pprint.pformat(x) path, num, fn, y = traceback.extract_stack( 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])) - self._logHnd.write(buf.encode("utf8") + "\n") + self._logHnd.write(buf + "\n") if os.environ.get("ANKIDEV"): - print buf + print(buf) def _openLog(self): if not self._debugLog: @@ -840,7 +840,7 @@ and queue = 0""", intTime(), self.usn()) if os.path.exists(lpath2): os.unlink(lpath2) os.rename(lpath, lpath2) - self._logHnd = open(lpath, "ab") + self._logHnd = open(lpath, "a") def _closeLog(self): self._logHnd = None diff --git a/anki/db.py b/anki/db.py index 13c8d3535..f9fe4a4d9 100644 --- a/anki/db.py +++ b/anki/db.py @@ -18,10 +18,7 @@ Error = sqlite.Error class DB(object): def __init__(self, path, timeout=0): - encpath = path - if isinstance(encpath, unicode): - encpath = path.encode("utf-8") - self._db = sqlite.connect(encpath, timeout=timeout) + self._db = sqlite.connect(path, timeout=timeout) self._path = path self.echo = os.environ.get("DBECHO") self.mod = False @@ -41,9 +38,9 @@ class DB(object): res = self._db.execute(sql, a) if self.echo: #print a, ka - print sql, "%0.3fms" % ((time.time() - t)*1000) + print(sql, "%0.3fms" % ((time.time() - t)*1000)) if self.echo == "2": - print a, ka + print(a, ka) return res def executemany(self, sql, l): @@ -51,20 +48,20 @@ class DB(object): t = time.time() self._db.executemany(sql, l) if self.echo: - print sql, "%0.3fms" % ((time.time() - t)*1000) + print(sql, "%0.3fms" % ((time.time() - t)*1000)) if self.echo == "2": - print l + print(l) def commit(self): t = time.time() self._db.commit() if self.echo: - print "commit %0.3fms" % ((time.time() - t)*1000) + print("commit %0.3fms" % ((time.time() - t)*1000)) def executescript(self, sql): self.mod = True if self.echo: - print sql + print(sql) self._db.executescript(sql) def rollback(self): diff --git a/anki/decks.py b/anki/decks.py index ace66563d..8717d832b 100644 --- a/anki/decks.py +++ b/anki/decks.py @@ -95,7 +95,7 @@ class DeckManager(object): self.dconf = json.loads(dconf) # set limits to within bounds found = False - for c in self.dconf.values(): + for c in list(self.dconf.values()): for t in ('rev', 'new'): pd = 'perDay' if c[t][pd] > 999999: @@ -125,7 +125,7 @@ class DeckManager(object): def id(self, name, create=True, type=defaultDeck): "Add a deck with NAME. Reuse deck if already exists. Return id as int." name = name.replace('"', '') - for id, g in self.decks.items(): + for id, g in list(self.decks.items()): if g['name'].lower() == name.lower(): return int(id) if not create: @@ -185,22 +185,22 @@ class DeckManager(object): del self.decks[str(did)] # ensure we have an active deck if did in self.active(): - self.select(int(self.decks.keys()[0])) + self.select(int(list(self.decks.keys())[0])) self.save() def allNames(self, dyn=True): "An unsorted list of all deck names." if dyn: - return [x['name'] for x in self.decks.values()] + return [x['name'] for x in list(self.decks.values())] 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): "A list of all decks." - return self.decks.values() + return list(self.decks.values()) def allIds(self): - return self.decks.keys() + return list(self.decks.keys()) def collapse(self, did): deck = self.get(did) @@ -225,7 +225,7 @@ class DeckManager(object): def byName(self, name): "Get deck with NAME." - for m in self.decks.values(): + for m in list(self.decks.values()): if m['name'] == name: return m @@ -319,7 +319,7 @@ class DeckManager(object): def allConf(self): "A list of all deck config." - return self.dconf.values() + return list(self.dconf.values()) def confForDid(self, did): deck = self.get(did, default=False) @@ -370,7 +370,7 @@ class DeckManager(object): def didsForConf(self, conf): dids = [] - for deck in self.decks.values(): + for deck in list(self.decks.values()): if 'conf' in deck and deck['conf'] == conf['id']: dids.append(deck['id']) return dids @@ -421,7 +421,7 @@ class DeckManager(object): ids2str(dids)) def recoverOrphans(self): - dids = self.decks.keys() + dids = list(self.decks.keys()) mod = self.col.db.mod self.col.db.execute("update cards set did = 1 where did not in "+ ids2str(dids)) diff --git a/anki/exporting.py b/anki/exporting.py index 996cf0c4f..8434fc23f 100644 --- a/anki/exporting.py +++ b/anki/exporting.py @@ -137,7 +137,7 @@ class AnkiExporter(Exporter): "insert into cards values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", data) # notes - strnids = ids2str(nids.keys()) + strnids = ids2str(list(nids.keys())) notedata = [] for row in self.src.db.all( "select * from notes where id in "+strnids): @@ -209,7 +209,7 @@ class AnkiExporter(Exporter): if self._modelHasMedia(m, fname): media[fname] = True break - self.mediaFiles = media.keys() + self.mediaFiles = list(media.keys()) self.dst.crt = self.src.crt # todo: tags? self.count = self.dst.cardCount() diff --git a/anki/find.py b/anki/find.py index 53d0a4f00..ae3c9d549 100644 --- a/anki/find.py +++ b/anki/find.py @@ -92,7 +92,7 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds else: inQuote = c # separator (space and ideographic space) - elif c in (" ", u'\u3000'): + elif c in (" ", '\u3000'): if inQuote: token += c elif token: @@ -239,7 +239,8 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds # Commands ###################################################################### - def _findTag(self, (val, args)): + def _findTag(self, args): + (val, args) = args if val == "none": return 'n.tags = ""' 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) return "n.tags like ?" - def _findCardState(self, (val, args)): + def _findCardState(self, args): + (val, args) = args if val in ("review", "new", "learn"): if val == "review": 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)""" % ( self.col.sched.today, self.col.sched.dayCutoff) - def _findRated(self, (val, args)): + def _findRated(self, args): # days(:optional_ease) + (val, args) = args r = val.split(":") try: 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)" % (cutoff, ease)) - def _findAdded(self, (val, args)): + def _findAdded(self, args): + (val, args) = args try: days = int(val) 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 return "c.id > %d" % cutoff - def _findProp(self, (val, args)): + def _findProp(self, args): # extract + (val, args) = args m = re.match("(^.+?)(<=|>=|!=|=|<|>)(.+?$)", val) if not m: return @@ -331,22 +336,26 @@ select distinct(n.id) from cards c, notes n where c.nid=n.id and """+preds args.append("%"+val+"%") 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): return return "n.id in (%s)" % val - def _findCids(self, (val, args)): + def _findCids(self, args): + (val, args) = args if re.search("[^0-9,]", val): return return "c.id in (%s)" % val - def _findMid(self, (val, args)): + def _findMid(self, args): + (val, args) = args if re.search("[^0-9]", val): return return "n.mid = %s" % val - def _findModel(self, (val, args)): + def _findModel(self, args): + (val, args) = args ids = [] val = val.lower() 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']) return "n.mid in %s" % ids2str(ids) - def _findDeck(self, (val, args)): + def _findDeck(self, args): # if searching for all decks, skip + (val, args) = args if val == "*": return "skip" # 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) 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? + (val, args) = args try: num = int(val) - 1 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(""" select id, mid, flds from notes where mid in %s and flds like ? escape '\\'""" % ( - ids2str(mods.keys())), + ids2str(list(mods.keys()))), "%"+val+"%"): flds = splitFields(flds) ord = mods[str(mid)][1] @@ -441,8 +452,9 @@ where mid in %s and flds like ? escape '\\'""" % ( return "0" return "n.id in %s" % ids2str(nids) - def _findDupes(self, (val, args)): + def _findDupes(self, args): # caller must call stripHTMLMedia on passed val + (val, args) = args try: mid, val = val.split(",", 1) except OSError: diff --git a/anki/importing/anki2.py b/anki/importing/anki2.py index 3f2923c34..abd19eb0f 100644 --- a/anki/importing/anki2.py +++ b/anki/importing/anki2.py @@ -404,7 +404,7 @@ insert or ignore into revlog values (?,?,?,?,?,?,?,?,?)""", revlog) ###################################################################### def _postImport(self): - for did in self._decks.values(): + for did in list(self._decks.values()): self.col.sched.maybeRandomizeDeck(did) # make sure new position is correct self.dst.conf['nextPos'] = self.dst.db.scalar( diff --git a/anki/importing/apkg.py b/anki/importing/apkg.py index afdadff94..aa44cb1fd 100644 --- a/anki/importing/apkg.py +++ b/anki/importing/apkg.py @@ -19,12 +19,12 @@ class AnkiPackageImporter(Anki2Importer): # we need the media dict in advance, and we'll need a map of fname -> # number to use during the import 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 # run anki2 importer Anki2Importer.run(self) # 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-"): continue path = os.path.join(self.col.media.dir(), diff --git a/anki/importing/csvfile.py b/anki/importing/csvfile.py index 612cdd27a..70204e010 100644 --- a/anki/importing/csvfile.py +++ b/anki/importing/csvfile.py @@ -35,13 +35,12 @@ class TextImporter(NoteImporter): reader = csv.reader(self.data, self.dialect, doublequote=True) try: for row in reader: - row = [unicode(x, "utf-8") for x in row] if len(row) != self.numFields: if row: log.append(_( "'%(row)s' had %(num1)d fields, " "expected %(num2)d") % { - "row": u" ".join(row), + "row": " ".join(row), "num1": len(row), "num2": self.numFields, }) @@ -49,7 +48,7 @@ class TextImporter(NoteImporter): continue note = self.noteFromFields(row) notes.append(note) - except (csv.Error), e: + except (csv.Error) as e: log.append(_("Aborted: %s") % str(e)) self.log = log self.ignored = ignored @@ -68,16 +67,14 @@ class TextImporter(NoteImporter): def openFile(self): self.dialect = None - self.fileobj = open(self.file, "rbU") + self.fileobj = open(self.file, "r", encoding='utf-8-sig') self.data = self.fileobj.read() - if self.data.startswith(codecs.BOM_UTF8): - self.data = self.data[len(codecs.BOM_UTF8):] def sub(s): return re.sub("^\#.*$", "__comment", s) self.data = [sub(x)+"\n" for x in self.data.split("\n") if sub(x) != "__comment"] if self.data: 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(" ") del self.data[0] self.updateDelimiter() @@ -117,7 +114,7 @@ class TextImporter(NoteImporter): reader = csv.reader(self.data, delimiter=self.delimiter, doublequote=True) try: while True: - row = reader.next() + row = next(reader) if row: self.numFields = len(row) break diff --git a/anki/importing/mnemo.py b/anki/importing/mnemo.py index 1d3284679..4ebe88eaa 100644 --- a/anki/importing/mnemo.py +++ b/anki/importing/mnemo.py @@ -158,7 +158,7 @@ acq_reps+ret_reps, lapses, card_type_id from cards"""): def _addCloze(self, notes): data = [] - notes = notes.values() + notes = list(notes.values()) for orig in notes: # create a foreign note object n = ForeignNote() diff --git a/anki/importing/noteimp.py b/anki/importing/noteimp.py index 2b30558e3..490395c4b 100644 --- a/anki/importing/noteimp.py +++ b/anki/importing/noteimp.py @@ -222,7 +222,7 @@ content in the text file to the correct fields.""")) if not self.processFields(n): return # 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.col.tags.register(n.tags) return [id, guid64(), self.model['id'], diff --git a/anki/importing/supermemo_xml.py b/anki/importing/supermemo_xml.py index ee1b3da7f..85fe73dbb 100644 --- a/anki/importing/supermemo_xml.py +++ b/anki/importing/supermemo_xml.py @@ -10,7 +10,6 @@ from anki.lang import _ from anki.lang import ngettext from xml.dom import minidom -from types import DictType, InstanceType from string import capwords import re, unicodedata, time @@ -27,9 +26,9 @@ class SmartDict(dict): def __init__(self, *a, **kw): if a: - if type(a[0]) is DictType: + if isinstance(type(a[0]), dict): kw.update(a[0]) - elif type(a[0]) is InstanceType: + elif isinstance(type(a[0]), object): kw.update(a[0].__dict__) elif hasattr(a[0], '__class__') and a[0].__class__.__name__=='SmartDict': kw.update(a[0].__dict__) @@ -121,26 +120,26 @@ class SupermemoXmlImporter(NoteImporter): def _fudgeText(self, text): "Replace sm syntax to Anki syntax" - text = text.replace("\n\r", u"
") - text = text.replace("\n", u"
") + text = text.replace("\n\r", "
") + text = text.replace("\n", "
") return text def _unicode2ascii(self,str): "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): """Unescape HTML 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. - s = re.sub(u'&',u'&',s) + s = re.sub('&','&',s) #unescaped solitary chars < or > that were ok for minidom confuse btfl soup #s = re.sub(u'>',u'>',s) #s = re.sub(u'<',u'<',s) - return unicode(btflsoup(s, selfClosingTags=['br','hr','img','wbr'], convertEntities=btflsoup.HTML_ENTITIES)) + return str(btflsoup(s, "html.parser")) def _afactor2efactor(self, af): # Adapted from @@ -173,9 +172,9 @@ class SupermemoXmlImporter(NoteImporter): # Migrating content / time consuming part # addItemToCards is called for each sm element - self.logger(u'Parsing started.') + self.logger('Parsing started.') self.parse() - self.logger(u'Parsing done.') + self.logger('Parsing done.') # Return imported cards self.total = len(self.notes) @@ -201,7 +200,7 @@ class SupermemoXmlImporter(NoteImporter): # pre-process scheduling data # convert learning data if (not self.META.resetLearningData - and item.Interval >= 1 + and int(item.Interval) >= 1 and getattr(item, "LastRepetition", None)): # migration of LearningData algorithm 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 tTaggTitle = False 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 break 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( '^[0-9 ]+$','',i) 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 - if self.META.tagMemorizedItems and item.Interval >0: + if self.META.tagMemorizedItems and int(item.Interval) >0: 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) def logger(self,text,level=1): "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: #self.deck.updateProgress(_(text)) 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 @@ -266,9 +265,9 @@ class SupermemoXmlImporter(NoteImporter): return sys.stdin # try to open with urllib (if source is http, ftp, or file URL) - import urllib + import urllib.request, urllib.parse, urllib.error try: - return urllib.urlopen(source) + return urllib.request.urlopen(source) except (IOError, OSError): pass @@ -279,24 +278,24 @@ class SupermemoXmlImporter(NoteImporter): pass # treat source as string - import StringIO - return StringIO.StringIO(str(source)) + import io + return io.StringIO(str(source)) def loadSource(self, source): """Load source file and parse with xml.dom.minidom""" self.source = source - self.logger(u'Load started...') + self.logger('Load started...') sock = open(self.source) self.xmldoc = minidom.parse(sock).documentElement sock.close() - self.logger(u'Load done.') + self.logger('Load done.') # PARSE def parse(self, node=None): "Parse method - parses document elements" - if node==None and self.xmldoc<>None: + if node==None and self.xmldoc!=None: node = self.xmldoc _method = "parse_%s" % node.__class__.__name__ @@ -304,7 +303,7 @@ class SupermemoXmlImporter(NoteImporter): parseMethod = getattr(self, _method) parseMethod(node) 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): "Parse XML document" @@ -319,7 +318,7 @@ class SupermemoXmlImporter(NoteImporter): handlerMethod = getattr(self, _method) handlerMethod(node) 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() def parse_Text(self, node): @@ -353,7 +352,7 @@ class SupermemoXmlImporter(NoteImporter): for child in node.childNodes: self.parse(child) #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'): self.cntElm[-1][key]=self.cntElm[-1][key].strip() @@ -367,18 +366,18 @@ class SupermemoXmlImporter(NoteImporter): # migrate only memorized otherway skip/continue 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: #import sm element data to Anki self.addItemToCards(smel) - self.logger(u"Import element \t- " + smel['Question'], level=3) + self.logger("Import element \t- " + smel['Question'], level=3) #print element 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 ) 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: @@ -389,7 +388,7 @@ class SupermemoXmlImporter(NoteImporter): if smel.Title != None: # remove topic from title list 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): "Process SM element Content" @@ -422,7 +421,7 @@ class SupermemoXmlImporter(NoteImporter): self.cntElm[-1][node.tagName] = t self.cntMeta['title'].append(t) 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): diff --git a/anki/lang.py b/anki/lang.py index dd7d98b2b..45a6d034a 100644 --- a/anki/lang.py +++ b/anki/lang.py @@ -7,48 +7,48 @@ import gettext import threading langs = [ - (u"Afrikaans", "af"), - (u"Bahasa Melayu", "ms"), - (u"Dansk", "da"), - (u"Deutsch", "de"), - (u"Eesti", "et"), - (u"English", "en"), - (u"Español", "es"), - (u"Esperanto", "eo"), - (u"Français", "fr"), - (u"Galego", "gl"), - (u"Italiano", "it"), - (u"Lenga d'òc", "oc"), - (u"Magyar", "hu"), - (u"Nederlands","nl"), - (u"Norsk","nb"), - (u"Occitan","oc"), - (u"Plattdüütsch", "nds"), - (u"Polski", "pl"), - (u"Português Brasileiro", "pt_BR"), - (u"Português", "pt"), - (u"Româneşte", "ro"), - (u"Slovenščina", "sl"), - (u"Suomi", "fi"), - (u"Svenska", "sv"), - (u"Tiếng Việt", "vi"), - (u"Türkçe", "tr"), - (u"Čeština", "cs"), - (u"Ελληνικά", "el"), - (u"босански", "bs"), - (u"Български", "bg"), - (u"Монгол хэл","mn"), - (u"русский язык", "ru"), - (u"Српски", "sr"), - (u"українська мова", "uk"), - (u"עִבְרִית", "he"), - (u"العربية", "ar"), - (u"فارسی", "fa"), - (u"ภาษาไทย", "th"), - (u"日本語", "ja"), - (u"简体中文", "zh_CN"), - (u"繁體中文", "zh_TW"), - (u"한국어", "ko"), + ("Afrikaans", "af"), + ("Bahasa Melayu", "ms"), + ("Dansk", "da"), + ("Deutsch", "de"), + ("Eesti", "et"), + ("English", "en"), + ("Español", "es"), + ("Esperanto", "eo"), + ("Français", "fr"), + ("Galego", "gl"), + ("Italiano", "it"), + ("Lenga d'òc", "oc"), + ("Magyar", "hu"), + ("Nederlands","nl"), + ("Norsk","nb"), + ("Occitan","oc"), + ("Plattdüütsch", "nds"), + ("Polski", "pl"), + ("Português Brasileiro", "pt_BR"), + ("Português", "pt"), + ("Româneşte", "ro"), + ("Slovenščina", "sl"), + ("Suomi", "fi"), + ("Svenska", "sv"), + ("Tiếng Việt", "vi"), + ("Türkçe", "tr"), + ("Čeština", "cs"), + ("Ελληνικά", "el"), + ("босански", "bs"), + ("Български", "bg"), + ("Монгол хэл","mn"), + ("русский язык", "ru"), + ("Српски", "sr"), + ("українська мова", "uk"), + ("עִבְרִית", "he"), + ("العربية", "ar"), + ("فارسی", "fa"), + ("ภาษาไทย", "th"), + ("日本語", "ja"), + ("简体中文", "zh_CN"), + ("繁體中文", "zh_TW"), + ("한국어", "ko"), ] threadLocal = threading.local() @@ -65,10 +65,10 @@ def localTranslation(): return currentTranslation def _(str): - return localTranslation().ugettext(str) + return localTranslation().gettext(str) def ngettext(single, plural, n): - return localTranslation().ungettext(single, plural, n) + return localTranslation().ngettext(single, plural, n) def langDir(): dir = os.path.join(os.path.dirname( diff --git a/anki/latex.py b/anki/latex.py index 387507e74..77cba41f1 100644 --- a/anki/latex.py +++ b/anki/latex.py @@ -56,7 +56,7 @@ def _imgLink(col, latex, model): if os.path.exists(fname): return link elif not build: - return u"[latex]%s[/latex]" % latex + return "[latex]%s[/latex]" % latex else: err = _buildImg(col, txt, fname, model) if err: @@ -71,11 +71,10 @@ def _latexFromHtml(col, latex): return latex def _buildImg(col, latex, fname, model): - # add header/footer & convert to utf8 + # add header/footer latex = (model["latexPre"] + "\n" + latex + "\n" + model["latexPost"]) - latex = latex.encode("utf8") # it's only really secure if run in a jail, but these are the most common tmplatex = latex.replace("\\includegraphics", "") for bad in ("\\write18", "\\readline", "\\input", "\\include", @@ -91,7 +90,7 @@ package in the LaTeX header instead.""") % bad # write into a temp file log = open(namedtmp("latex_log.txt"), "w") texpath = namedtmp("tmp.tex") - texfile = file(texpath, "w") + texfile = open(texpath, "w") texfile.write(latex) texfile.close() mdir = col.media.dir() diff --git a/anki/media.py b/anki/media.py index 7f36312a8..c298d9cdc 100644 --- a/anki/media.py +++ b/anki/media.py @@ -4,11 +4,11 @@ import re import traceback -import urllib +import urllib.request, urllib.parse, urllib.error import unicodedata import sys import zipfile -from cStringIO import StringIO +from io import StringIO from anki.utils import checksum, isWin, isMac, json from anki.db import DB @@ -33,9 +33,6 @@ class MediaManager(object): return # media directory 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): os.makedirs(self._dir) 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 """) self.db.commit() - except Exception, e: + except Exception as e: # if we couldn't import the old db for some reason, just start # anew 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): if unescape: - fn = urllib.unquote + fn = urllib.parse.unquote else: - fn = urllib.quote + fn = urllib.parse.quote def repl(match): tag = match.group(0) fname = match.group("fname") if re.match("(https?|ftp)://", fname): return tag - return tag.replace( - fname, unicode(fn(fname.encode("utf-8")), "utf8")) + return tag.replace(fname, fn(fname)) for reg in self.imgRegexps: string = re.sub(reg, repl, string) return string @@ -271,9 +267,6 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0); if file.startswith("_"): # leading _ says to ignore file continue - if not isinstance(file, unicode): - invalid.append(unicode(file, sys.getfilesystemencoding(), "replace")) - continue nfcFile = unicodedata.normalize("NFC", file) # we enforce NFC fs encoding on non-macs; on macs we'll have gotten # 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) 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) # Tracking changes @@ -413,7 +403,7 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0); # mark as used self.cache[f][2] = True # 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]: removed.append(k) return added, removed @@ -510,8 +500,6 @@ create table meta (dirMod int, lastUsn int); insert into meta values (0, 0); data = z.read(i) csum = checksum(data) name = meta[i.filename] - if not isinstance(name, unicode): - name = unicode(name, "utf8") # normalize name for platform if isMac: name = unicodedata.normalize("NFD", name) diff --git a/anki/models.py b/anki/models.py index 6f11bc05f..bda082a2a 100644 --- a/anki/models.py +++ b/anki/models.py @@ -108,7 +108,7 @@ class ModelManager(object): m = self.get(self.col.decks.current().get('mid')) if not forDeck or not m: 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): self.col.conf['curModel'] = m['id'] @@ -122,14 +122,14 @@ class ModelManager(object): def all(self): "Get all models." - return self.models.values() + return list(self.models.values()) def allNames(self): return [m['name'] for m in self.all()] def byName(self, name): "Get model with NAME." - for m in self.models.values(): + for m in list(self.models.values()): if m['name'] == name: return m @@ -158,7 +158,7 @@ select id from cards where nid in (select id from notes where mid = ?)""", self.save() # GUI should ensure last model is not deleted if current: - self.setCurrent(self.models.values()[0]) + self.setCurrent(list(self.models.values())[0]) def add(self, 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 def ids(self): - return self.models.keys() + return list(self.models.keys()) # Tools ################################################## @@ -429,7 +429,7 @@ select id from notes where mid = ?)""" % " ".join(map), "select id, flds from notes where id in "+ids2str(nids)): newflds = {} flds = splitFields(flds) - for old, new in map.items(): + for old, new in list(map.items()): newflds[new] = flds[old] flds = [] for c in range(nfields): diff --git a/anki/notes.py b/anki/notes.py index 16be73e0a..211df543a 100644 --- a/anki/notes.py +++ b/anki/notes.py @@ -79,7 +79,7 @@ insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""", ################################################## def keys(self): - return self._fmap.keys() + return list(self._fmap.keys()) def values(self): return self.fields @@ -101,7 +101,7 @@ insert or replace into notes values (?,?,?,?,?,?,?,?,?,?,?)""", self.fields[self._fieldOrd(key)] = value def __contains__(self, key): - return key in self._fmap.keys() + return key in list(self._fmap.keys()) # Tags ################################################## diff --git a/anki/sched.py b/anki/sched.py index 61c26256a..b378bc022 100644 --- a/anki/sched.py +++ b/anki/sched.py @@ -2,7 +2,6 @@ # Copyright: Damien Elmes # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from __future__ import division import time import random import itertools diff --git a/anki/sound.py b/anki/sound.py index f68ab9dd7..57c7c2c08 100644 --- a/anki/sound.py +++ b/anki/sound.py @@ -24,8 +24,8 @@ def hasSound(text): ########################################################################## -processingSrc = u"rec.wav" -processingDst = u"rec.mp3" +processingSrc = "rec.wav" +processingDst = "rec.mp3" processingChain = [] recFiles = [] @@ -229,7 +229,7 @@ class _Recorder(object): if ret: raise Exception(_( "Error running %s") % - u" ".join(c)) + " ".join(c)) class PyAudioThreadedRecorder(threading.Thread): @@ -258,7 +258,7 @@ class PyAudioThreadedRecorder(threading.Thread): while not self.finish: try: data = stream.read(chunk) - except IOError, e: + except IOError as e: if e[1] == pyaudio.paInputOverflowed: data = None else: @@ -295,7 +295,7 @@ class PyAudioRecorder(_Recorder): def file(self): if self.encode: - tgt = u"rec%d.mp3" % time.time() + tgt = "rec%d.mp3" % time.time() os.rename(processingDst, tgt) return tgt else: diff --git a/anki/stats.py b/anki/stats.py index 7ead5755f..01c814a35 100644 --- a/anki/stats.py +++ b/anki/stats.py @@ -2,7 +2,6 @@ # Copyright: Damien Elmes # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from __future__ import division import time import datetime import json @@ -112,7 +111,7 @@ class CollectionStats(object): def report(self, type=0): # 0=days, 1=weeks, 2=months self.type = type - from statsbg import bg + from .statsbg import bg txt = self.css % bg txt += self.todayStats() txt += self.dueGraph() @@ -160,7 +159,7 @@ from revlog where id > ? """+lim, (self.col.sched.dayCutoff-86400)*1000) filt = filt or 0 # studied def bold(s): - return ""+unicode(s)+"" + return ""+str(s)+"" msgp1 = ngettext("%d card", "%d cards", cards) % cards b += _("Studied %(a)s in %(b)s today.") % dict( a=bold(msgp1), b=bold(fmtTimeSpan(thetime, unit=1))) diff --git a/anki/sync.py b/anki/sync.py index 711f1bd3c..376548ec4 100644 --- a/anki/sync.py +++ b/anki/sync.py @@ -2,19 +2,19 @@ # Copyright: Damien Elmes # 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 gzip import random -from cStringIO import StringIO +from io import StringIO import httplib2 from anki.db import DB from anki.utils import ids2str, intTime, json, isWin, isMac, platDesc, checksum from anki.consts import * -from hooks import runHook +from .hooks import runHook import anki -from lang import ngettext +from .lang import ngettext # syncing vars HTTP_TIMEOUT = 90 @@ -64,17 +64,19 @@ def _setupProxy(): # platform-specific fetch url = None if isWin: - r = urllib.getproxies_registry() - if 'https' in r: - url = r['https'] - elif 'http' in r: - url = r['http'] + print("fixme: win proxy support") + # r = urllib.getproxies_registry() + # if 'https' in r: + # url = r['https'] + # elif 'http' in r: + # url = r['http'] elif isMac: - r = urllib.getproxies_macosx_sysconf() - if 'https' in r: - url = r['https'] - elif 'http' in r: - url = r['http'] + print("fixme: mac proxy support") + # r = urllib.getproxies_macosx_sysconf() + # if 'https' in r: + # url = r['https'] + # elif 'http' in r: + # url = r['http'] if url: p = _proxy_info_from_url(url, _proxyMethod(url)) if p: @@ -556,7 +558,7 @@ class HttpSyncer(object): buf = StringIO() # post vars 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( 'Content-Disposition: form-data; name="%s"\r\n\r\n%s\r\n' % diff --git a/anki/tags.py b/anki/tags.py index b87b0ddeb..3bfaac76e 100644 --- a/anki/tags.py +++ b/anki/tags.py @@ -47,7 +47,7 @@ class TagManager(object): runHook("newTag") def all(self): - return self.tags.keys() + return list(self.tags.keys()) def registerNotes(self, nids=None): "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))))) def allItems(self): - return self.tags.items() + return list(self.tags.items()) def save(self): self.changed = True @@ -122,13 +122,13 @@ class TagManager(object): def split(self, 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): "Join tags into a single string, with leading and trailing spaces." if not tags: - return u"" - return u" %s " % u" ".join(tags) + return "" + return " %s " % " ".join(tags) def addToStr(self, addtags, tags): "Add tags if they don't exist, and canonify." @@ -174,6 +174,6 @@ class TagManager(object): ########################################################################## def beforeUpload(self): - for k in self.tags.keys(): + for k in list(self.tags.keys()): self.tags[k] = 0 self.save() diff --git a/anki/template/README.rst b/anki/template/README.rst deleted file mode 100644 index c972a62cd..000000000 --- a/anki/template/README.rst +++ /dev/null @@ -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 \ No newline at end of file diff --git a/anki/template/template.py b/anki/template/template.py index 89b64b52a..a457a287f 100644 --- a/anki/template/template.py +++ b/anki/template/template.py @@ -99,7 +99,7 @@ class Template(object): replacer = '' # if it and isinstance(it, collections.Callable): # replacer = it(inner) - if isinstance(it, basestring): + if isinstance(it, str): it = stripHTMLMedia(it).strip() if it and not hasattr(it, '__iter__'): if section[2] != '^': @@ -133,7 +133,7 @@ class Template(object): replacement = func(self, tag_name, context) template = template.replace(tag, replacement) except (SyntaxError, KeyError): - return u"{{invalid template}}" + return "{{invalid template}}" return template diff --git a/anki/template/view.py b/anki/template/view.py index b3c43e3d7..372f39df3 100644 --- a/anki/template/view.py +++ b/anki/template/view.py @@ -53,7 +53,7 @@ class View(object): 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) return self._load_template() @@ -70,7 +70,7 @@ class View(object): try: template = f.read() if self.template_encoding: - template = unicode(template, self.template_encoding) + template = str(template, self.template_encoding) finally: f.close() return template diff --git a/anki/utils.py b/anki/utils.py index 5452d051b..7ef7d5efc 100644 --- a/anki/utils.py +++ b/anki/utils.py @@ -2,13 +2,12 @@ # Copyright: Damien Elmes # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from __future__ import division import re import os import random import time import math -import htmlentitydefs +import html.entities import subprocess import tempfile import shutil @@ -18,28 +17,10 @@ import locale from hashlib import sha1 import platform import traceback +import json 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 ############################################################################## @@ -182,15 +163,15 @@ def entsToTxt(html): # character reference try: if text[:3] == "&#x": - return unichr(int(text[3:-1], 16)) + return chr(int(text[3:-1], 16)) else: - return unichr(int(text[2:-1])) + return chr(int(text[2:-1])) except ValueError: pass else: # named entity try: - text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) + text = chr(html.entities.name2codepoint[text[1:-1]]) except KeyError: pass return text # leave as is @@ -222,8 +203,7 @@ def maxID(db): "Return the first safe ID to use." now = intTime(1000) for tbl in "cards", "notes": - now = max(now, db.scalar( - "select max(id) from %s" % tbl)) + now = max(now, db.scalar("select max(id) from %s" % tbl) or 0) return now + 1 # used in ankiweb @@ -271,7 +251,7 @@ def splitFields(string): ############################################################################## def checksum(data): - if isinstance(data, unicode): + if isinstance(data, str): data = data.encode("utf-8") return sha1(data).hexdigest() @@ -292,8 +272,7 @@ def tmpdir(): shutil.rmtree(_tmpdir) import atexit atexit.register(cleanup) - _tmpdir = unicode(os.path.join(tempfile.gettempdir(), "anki_temp"), \ - sys.getfilesystemencoding()) + _tmpdir = os.path.join(tempfile.gettempdir(), "anki_temp") if not os.path.exists(_tmpdir): os.mkdir(_tmpdir) return _tmpdir diff --git a/aqt/__init__.py b/aqt/__init__.py index c5de6ee2e..3eb5d881c 100644 --- a/aqt/__init__.py +++ b/aqt/__init__.py @@ -5,7 +5,7 @@ import os import sys import optparse import tempfile -import __builtin__ +import builtins import locale import gettext @@ -29,16 +29,16 @@ moduleDir = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0] try: import aqt.forms -except ImportError, e: +except ImportError as e: if "forms" in str(e): - print "If you're running from git, did you run build_ui.sh?" - print + print("If you're running from git, did you run build_ui.sh?") + print() raise from anki.utils import checksum # Dialog manager - manages modeless windows -########################################################################## +##########################################################################emacs class DialogManager(object): @@ -67,7 +67,7 @@ class DialogManager(object): def closeAll(self): "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 not instance.canClose(): return False @@ -98,8 +98,8 @@ def setupLang(pm, app, force=None): # gettext _gtrans = gettext.translation( 'anki', dir, languages=[lang], fallback=True) - __builtin__.__dict__['_'] = _gtrans.ugettext - __builtin__.__dict__['ngettext'] = _gtrans.ungettext + builtins.__dict__['_'] = _gtrans.gettext + builtins.__dict__['ngettext'] = _gtrans.ngettext anki.lang.setLang(lang, local=False) if lang in ("he","ar","fa"): app.setLayoutDirection(Qt.RightToLeft) @@ -133,7 +133,7 @@ class AnkiApp(QApplication): if args and args[0]: buf = os.path.abspath(args[0]) if self.sendMsg(buf): - print "Already running; reusing existing instance." + print("Already running; reusing existing instance.") return True else: # send failed, so we're the first instance or the @@ -163,7 +163,7 @@ class AnkiApp(QApplication): sys.stderr.write(sock.errorString()) return buf = sock.readAll() - buf = unicode(buf, sys.getfilesystemencoding(), "ignore") + buf = str(buf, sys.getfilesystemencoding(), "ignore") self.emit(SIGNAL("appMsg"), buf) sock.disconnectFromServer() @@ -192,7 +192,7 @@ def parseArgs(argv): def run(): try: _run() - except Exception, e: + except Exception as e: QMessageBox.critical(None, "Startup Error", "Please notify support of this error:\n\n"+ traceback.format_exc()) @@ -202,8 +202,8 @@ def _run(): # parse args opts, args = parseArgs(sys.argv) - opts.base = unicode(opts.base or "", sys.getfilesystemencoding()) - opts.profile = unicode(opts.profile or "", sys.getfilesystemencoding()) + opts.base = opts.base or "" + opts.profile = opts.profile or "" # on osx we'll need to add the qt plugins to the search path if isMac and getattr(sys, 'frozen', None): diff --git a/aqt/about.py b/aqt/about.py index f1ddb60a1..0f265fa54 100644 --- a/aqt/about.py +++ b/aqt/about.py @@ -27,7 +27,7 @@ system. It's free and open source.") abouttext += (_("Visit website") % aqt.appWebsite) + \ "" abouttext += '

' + _("Written by Damien Elmes, with patches, translation,\ - testing and design from:

%(cont)s") % {'cont': u"""Aaron Harsh, Ádám Szegi, + testing and design from:

%(cont)s") % {'cont': """Aaron Harsh, Ádám Szegi, Alex Fraser, Andreas Klauer, Andrew Wright, Bernhard Ibertsberger, C. van Rooyen, Charlene Barina, Christian Krause, Christian Rusche, David Smith, Dave Druelinger, Dotan Cohen, Emilio Wuerges, Emmanuel Jarri, Frank Harper, Gregor Skumavc, H. Mijail, diff --git a/aqt/addcards.py b/aqt/addcards.py index 40064b6da..306bcdd3b 100644 --- a/aqt/addcards.py +++ b/aqt/addcards.py @@ -69,7 +69,7 @@ class AddCards(QDialog): self.connect(self.helpButton, SIGNAL("clicked()"), self.helpRequested) # history b = bb.addButton( - _("History")+ u" "+downArrow(), ar) + _("History")+ " "+downArrow(), ar) if isMac: sc = "Ctrl+Shift+H" else: @@ -90,8 +90,8 @@ class AddCards(QDialog): oldNote = self.editor.note note = self.setupNewNote(set=False) if oldNote: - oldFields = oldNote.keys() - newFields = note.keys() + oldFields = list(oldNote.keys()) + newFields = list(note.keys()) for n, f in enumerate(note.model()['flds']): fieldName = f['name'] try: diff --git a/aqt/addons.py b/aqt/addons.py index 559d3e0c7..82183045b 100644 --- a/aqt/addons.py +++ b/aqt/addons.py @@ -3,7 +3,7 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import sys, os, traceback -from cStringIO import StringIO +from io import StringIO import zipfile from aqt.qt import * from aqt.utils import showInfo, openFolder, isWin, openLink, \ @@ -73,7 +73,7 @@ class AddonManager(object): frm = aqt.forms.editaddon.Ui_Dialog() frm.setupUi(d) 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()"), lambda: self.onAcceptEdit(path, frm)) d.exec_() diff --git a/aqt/browser.py b/aqt/browser.py index ca94c26ce..7c7e4e3f7 100644 --- a/aqt/browser.py +++ b/aqt/browser.py @@ -278,10 +278,10 @@ class DataModel(QAbstractTableModel): return a def formatQA(self, txt): - s = txt.replace("
", u" ") - s = s.replace("
", u" ") - s = s.replace("

", u" ") - s = s.replace("\n", u" ") + s = txt.replace("
", " ") + s = s.replace("
", " ") + s = s.replace("
", " ") + s = s.replace("\n", " ") s = re.sub("\[sound:[^]]+\]", "", s) s = re.sub("\[\[type:[^]]+\]\]", "", s) s = stripHTMLMedia(s) @@ -518,7 +518,7 @@ class Browser(QMainWindow): def onSearch(self, reset=True): "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 = _("") sh = self.mw.pm.profile['searchHistory'] # update search history @@ -788,12 +788,12 @@ by clicking on one on the left.""")) if self.mw.app.keyboardModifiers() & Qt.AltModifier: txt = "-"+txt 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 != \ _(""): txt = cur + " " + txt elif self.mw.app.keyboardModifiers() & Qt.ShiftModifier: - cur = unicode(self.form.searchEdit.lineEdit().text()) + cur = str(self.form.searchEdit.lineEdit().text()) if cur: txt = cur + " or " + txt self.form.searchEdit.lineEdit().setText(txt) @@ -1365,8 +1365,8 @@ update cards set usn=?, mod=?, did=? where id in """ + scids, self.model.beginReset() try: changed = self.col.findReplace(sf, - unicode(frm.find.text()), - unicode(frm.replace.text()), + str(frm.find.text()), + str(frm.replace.text()), frm.re.isChecked(), field, frm.ignoreCase.isChecked()) @@ -1679,7 +1679,7 @@ class ChangeModel(QDialog): # check maps fmap = self.getFieldMap() 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(_("""\ Any cards mapped to nothing will be deleted. \ If a note has no remaining cards, it will be lost. \ @@ -1786,7 +1786,7 @@ class FavouritesLineEdit(QLineEdit): self.mw = mw self.browser = browser # 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.button = QToolButton(self) self.button.setStyleSheet('border: 0px;') @@ -1818,8 +1818,8 @@ class FavouritesLineEdit(QLineEdit): def updateButton(self, reset=True): # If search text is a saved query, switch to the delete button. # Otherwise show save button. - txt = unicode(self.text()).strip() - for key, value in self.mw.col.conf['savedFilters'].items(): + txt = str(self.text()).strip() + for key, value in list(self.mw.col.conf['savedFilters'].items()): if txt == value: self.doSave = False self.name = key @@ -1835,7 +1835,7 @@ class FavouritesLineEdit(QLineEdit): self.deleteClicked() def saveClicked(self): - txt = unicode(self.text()).strip() + txt = str(self.text()).strip() dlg = QInputDialog(self) dlg.setInputMode(QInputDialog.TextInput) dlg.setLabelText(_("The current search terms will be added as a new " diff --git a/aqt/clayout.py b/aqt/clayout.py index 6fff4cb9e..19e427c53 100644 --- a/aqt/clayout.py +++ b/aqt/clayout.py @@ -32,7 +32,7 @@ class CardLayout(QDialog): if addMode: # save it to DB temporarily self.emptyFields = [] - for name, val in note.items(): + for name, val in list(note.items()): if val.strip(): continue self.emptyFields.append(name) @@ -90,10 +90,10 @@ class CardLayout(QDialog): # template area tform = aqt.forms.template.Ui_Form() tform.setupUi(left) - tform.label1.setText(u" →") - tform.label2.setText(u" →") - tform.labelc1.setText(u" ↗") - tform.labelc2.setText(u" ↘") + tform.label1.setText(" →") + tform.label2.setText(" →") + tform.labelc1.setText(" ↗") + tform.labelc2.setText(" ↘") if self.style().objectName() == "gtk+": # gtk+ requires margins in inner layout tform.tlayout1.setContentsMargins(0, 11, 0, 0) @@ -167,7 +167,7 @@ Please create a new card type first.""")) flip.setAutoDefault(False) l.addWidget(flip) c(flip, SIGNAL("clicked()"), self.onFlip) - more = QPushButton(_("More") + u" "+downArrow()) + more = QPushButton(_("More") + " "+downArrow()) more.setAutoDefault(False) l.addWidget(more) c(more, SIGNAL("clicked()"), lambda: self.onMore(more)) @@ -251,7 +251,7 @@ Please create a new card type first.""")) txt = txt.replace("
", "") hadHR = origLen != len(txt) def answerRepl(match): - res = self.mw.reviewer.correct(u"exomple", u"an example") + res = self.mw.reviewer.correct("exomple", "an example") if hadHR: res = "
" + res return res diff --git a/aqt/deckbrowser.py b/aqt/deckbrowser.py index acf8d24c3..e145928e6 100644 --- a/aqt/deckbrowser.py +++ b/aqt/deckbrowser.py @@ -63,7 +63,7 @@ class DeckBrowser(object): def _keyHandler(self, evt): # currently does nothing - key = unicode(evt.text()) + key = str(evt.text()) def _selDeck(self, did): self.scrollPos = self.web.page().mainFrame().scrollPosition() @@ -287,7 +287,7 @@ where id > ?""", (self.mw.col.sched.dayCutoff-86400)*1000) return try: self.mw.col.decks.rename(deck, newName) - except DeckRenameError, e: + except DeckRenameError as e: return showWarning(e.description) self.show() @@ -304,7 +304,7 @@ where id > ?""", (self.mw.col.sched.dayCutoff-86400)*1000) def _dragDeckOnto(self, draggedDeckDid, ontoDeckDid): try: self.mw.col.decks.renameForDragAndDrop(draggedDeckDid, ontoDeckDid) - except DeckRenameError, e: + except DeckRenameError as e: return showWarning(e.description) self.show() diff --git a/aqt/deckconf.py b/aqt/deckconf.py index 7cd38f8ea..b595c8b3e 100644 --- a/aqt/deckconf.py +++ b/aqt/deckconf.py @@ -42,7 +42,7 @@ class DeckConf(QDialog): def setupCombos(self): import anki.consts as cs f = self.form - f.newOrder.addItems(cs.newCardOrderLabels().values()) + f.newOrder.addItems(list(cs.newCardOrderLabels().values())) self.connect(f.newOrder, SIGNAL("currentIndexChanged(int)"), self.onNewOrderChanged) @@ -230,7 +230,7 @@ class DeckConf(QDialog): ################################################## def updateList(self, conf, key, w, minSize=1): - items = unicode(w.text()).split(" ") + items = str(w.text()).split(" ") ret = [] for i in items: if not i: diff --git a/aqt/downloader.py b/aqt/downloader.py index b797fcb1e..f09c1f6c4 100644 --- a/aqt/downloader.py +++ b/aqt/downloader.py @@ -65,12 +65,12 @@ class Downloader(QThread): try: resp, cont = con.request( aqt.appShared + "download/%d" % self.code) - except Exception, e: + except Exception as e: exc = traceback.format_exc() try: - self.error = unicode(e[0], "utf8", "ignore") + self.error = str(e[0]) except: - self.error = unicode(exc, "utf8", "ignore") + self.error = str(exc) return finally: remHook("httpRecv", recvEvent) diff --git a/aqt/dyndeckconf.py b/aqt/dyndeckconf.py index 1b782e44a..fce28821c 100644 --- a/aqt/dyndeckconf.py +++ b/aqt/dyndeckconf.py @@ -38,7 +38,7 @@ class DeckConf(QDialog): def setupOrder(self): import anki.consts as cs - self.form.order.addItems(cs.dynOrderLabels().values()) + self.form.order.addItems(list(cs.dynOrderLabels().values())) def loadConf(self): f = self.form @@ -94,7 +94,7 @@ it?""")): return " ".join([str(x) for x in l]) def userToList(self, w, minSize=1): - items = unicode(w.text()).split(" ") + items = str(w.text()).split(" ") ret = [] for i in items: if not i: diff --git a/aqt/editor.py b/aqt/editor.py index 8ad50f7ce..62bd2ae01 100644 --- a/aqt/editor.py +++ b/aqt/editor.py @@ -3,9 +3,9 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import re import os -import urllib2 +import urllib.request, urllib.error, urllib.parse import ctypes -import urllib +import urllib.request, urllib.parse, urllib.error from anki.lang import _ from aqt.qt import * @@ -18,7 +18,7 @@ from aqt.utils import shortcut, showInfo, showWarning, getBase, getFile, \ openHelp, tooltip, downArrow import aqt import anki.js -from BeautifulSoup import BeautifulSoup +from bs4 import BeautifulSoup pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp") audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv", "m4a", "3gp", "spx", "oga") @@ -407,7 +407,7 @@ class Editor(object): runHook("setupEditorButtons", self) def enableButtons(self, val=True): - for b in self._buttons.values(): + for b in list(self._buttons.values()): b.setEnabled(val) def disableButtons(self): @@ -491,7 +491,7 @@ class Editor(object): elif str.startswith("dupes"): self.showDupes() else: - print str + print(str) def mungeHTML(self, txt): if txt == "
": @@ -536,7 +536,7 @@ class Editor(object): # will be loaded when page is ready return 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))) self.web.eval("setFields(%s, %d);" % ( json.dumps(data), field)) @@ -614,7 +614,7 @@ class Editor(object): html = form.textEdit.toPlainText() # filter html through beautifulsoup so we can strip out things like a # leading
- html = unicode(BeautifulSoup(html)) + html = str(BeautifulSoup(html, "html.parser")) self.note.fields[self.currentField] = html self.loadNote() # focus field so it's saved @@ -702,7 +702,7 @@ to a cloze type first, via Edit>Change Note Type.""")) return # find the highest existing cloze highest = 0 - for name, val in self.note.items(): + for name, val in list(self.note.items()): m = re.findall("\{\{c(\d+)::", val) if m: 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): try: file = getAudio(self.widget) - except Exception, e: + except Exception as e: showWarning(_( "Couldn't record audio. Have you installed lame and sox?") + "\n\n" + repr(str(e))) @@ -804,7 +804,7 @@ to a cloze type first, via Edit>Change Note Type.""")) def fnameToLink(self, fname): ext = fname.split(".")[-1].lower() if ext in pics: - name = urllib.quote(fname.encode("utf8")) + name = urllib.parse.quote(fname.encode("utf8")) return '' % name else: anki.sound.play(fname) @@ -837,22 +837,22 @@ to a cloze type first, via Edit>Change Note Type.""")) self.mw.progress.start( immediate=True, parent=self.parentWindow) try: - req = urllib2.Request(url, None, { + req = urllib.request.Request(url, None, { 'User-Agent': 'Mozilla/5.0 (compatible; Anki)'}) - filecontents = urllib2.urlopen(req).read() - except urllib2.URLError, e: + filecontents = urllib.request.urlopen(req).read() + except urllib.error.URLError as e: showWarning(_("An error occurred while opening %s") % e) return finally: 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) # HTML filtering ###################################################################### def _filterHTML(self, html, localize=False): - doc = BeautifulSoup(html) + doc = BeautifulSoup(html, "html.parser") # remove implicit regular font style from outermost element if doc.span: try: @@ -919,7 +919,7 @@ to a cloze type first, via Edit>Change Note Type.""")) for elem in "html", "head", "body", "meta": for tag in doc(elem): tag.replaceWithChildren() - html = unicode(doc) + html = str(doc) return html # Advanced menu @@ -1136,7 +1136,7 @@ class EditorWebView(AnkiWebView): # be URL-encoded, and shouldn't be a file:// url unless they're browsing # locally, which we don't support def _processText(self, mime): - txt = unicode(mime.text()) + txt = str(mime.text()) html = None # if the user is pasting an image or sound link, convert it to local if self.editor.isURL(txt): diff --git a/aqt/errors.py b/aqt/errors.py index 1ce1421e6..d92099e07 100644 --- a/aqt/errors.py +++ b/aqt/errors.py @@ -21,11 +21,8 @@ class ErrorHandler(QObject): sys.stderr = self def write(self, data): - # make sure we have unicode - if not isinstance(data, unicode): - data = unicode(data, "utf8", "replace") # dump to stdout - sys.stdout.write(data.encode("utf-8")) + sys.stdout.write(data) # save in buffer self.pool += data # and update timer diff --git a/aqt/exporting.py b/aqt/exporting.py index 69e39e659..16caba3e8 100644 --- a/aqt/exporting.py +++ b/aqt/exporting.py @@ -88,7 +88,7 @@ class ExportDialog(QDialog): deck_name = self.decks[self.frm.deck.currentIndex()] deck_name = re.sub('[\\\\/?<>:*|"^]', '_', deck_name) 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: file = getSaveFile(self, _("Export"), "export", self.exporter.key, self.exporter.ext, @@ -104,8 +104,8 @@ class ExportDialog(QDialog): try: f = open(file, "wb") f.close() - except (OSError, IOError), e: - showWarning(_("Couldn't save file: %s") % unicode(e)) + except (OSError, IOError) as e: + showWarning(_("Couldn't save file: %s") % str(e)) else: os.unlink(file) exportedMedia = lambda cnt: self.mw.progress.update( diff --git a/aqt/importing.py b/aqt/importing.py index 05a286f55..b9de69451 100644 --- a/aqt/importing.py +++ b/aqt/importing.py @@ -138,7 +138,7 @@ you can enter it here. Use \\t to represent tab."""), elif d == ":": d = _("Colon") else: - d = `d` + d = repr(d) txt = _("Fields separated by: %s") % d self.frm.autoDetect.setText(txt) @@ -164,7 +164,7 @@ you can enter it here. Use \\t to represent tab."""), except UnicodeDecodeError: showUnicodeWarning() return - except Exception, e: + except Exception as e: msg = _("Import failed.\n") err = repr(str(e)) if "1-character string" in err: @@ -172,7 +172,7 @@ you can enter it here. Use \\t to represent tab."""), elif "invalidTempFolder" in err: msg += self.mw.errorHandler.tempFolderMsg() else: - msg += unicode(traceback.format_exc(), "ascii", "replace") + msg += str(traceback.format_exc(), "ascii", "replace") showText(msg) return finally: @@ -268,7 +268,7 @@ def onImport(mw): filter=filt) if not file: return - file = unicode(file) + file = str(file) importFile(mw, file) def importFile(mw, file): @@ -295,7 +295,7 @@ def importFile(mw, file): except UnicodeDecodeError: showUnicodeWarning() return - except Exception, e: + except Exception as e: msg = repr(str(e)) if msg == "'unknownFormat'": if file.endswith(".anki2"): @@ -306,7 +306,7 @@ backup, please see the 'Backups' section of the user manual.""")) showWarning(_("Unknown file format.")) else: msg = _("Import failed. Debugging info:\n") - msg += unicode(traceback.format_exc(), "ascii", "replace") + msg += str(traceback.format_exc()) showText(msg) return finally: @@ -329,7 +329,7 @@ backup, please see the 'Backups' section of the user manual.""")) importer.run() except zipfile.BadZipfile: showWarning(invalidZipMsg()) - except Exception, e: + except Exception as e: err = repr(str(e)) if "invalidFile" in err: msg = _("""\ @@ -342,7 +342,7 @@ Invalid file. Please restore from backup.""") Unable to import from a read-only file.""")) else: msg = _("Import failed.\n") - msg += unicode(traceback.format_exc(), "ascii", "replace") + msg += str(traceback.format_exc()) showText(msg) else: log = "\n".join(importer.log) diff --git a/aqt/main.py b/aqt/main.py index ec4ead54c..def4f8ec4 100644 --- a/aqt/main.py +++ b/aqt/main.py @@ -56,7 +56,7 @@ class AnkiQt(QMainWindow): "syncing and add-on loading.")) # were we given a file to import? 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 # close on profile load error. if isMac and qtmajor >= 5: @@ -272,7 +272,7 @@ see the manual for how to restore from an automatic backup. Debug info: """)+traceback.format_exc()) self.unloadProfile() - except Exception, e: + except Exception as e: # the custom exception handler won't catch this if we immediately # unload, so we have to manually handle it if "invalidTempFolder" in repr(str(e)): @@ -627,7 +627,7 @@ title="%s">%s''' % ( # run standard handler QMainWindow.keyPressEvent(self, evt) # check global keys - key = unicode(evt.text()) + key = str(evt.text()) if key == "d": self.moveToState("deckBrowser") elif key == "s": @@ -1060,7 +1060,7 @@ will be lost. Continue?""")) pp = pprint.pprint self._captureOutput(True) try: - exec text + exec(text) except: self._output += traceback.format_exc() self._captureOutput(False) @@ -1109,7 +1109,7 @@ will be lost. Continue?""")) return tgt = tgt or self for action in tgt.findChildren(QAction): - txt = unicode(action.text()) + txt = str(action.text()) m = re.match("^(.+)\(&.+\)(.+)?", txt) if m: 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": return # import - if not isinstance(buf, unicode): - buf = unicode(buf, "utf8", "ignore") - self.handleImport(buf) diff --git a/aqt/models.py b/aqt/models.py index a3c71afa9..329a39336 100644 --- a/aqt/models.py +++ b/aqt/models.py @@ -7,6 +7,7 @@ from aqt.utils import showInfo, askUser, getText, maybeHideClose, openHelp import aqt.modelchooser, aqt.clayout from anki import stdmodels from aqt.utils import saveGeom, restoreGeom +import collections class Models(QDialog): def __init__(self, mw, parent=None, fromMain=False): @@ -118,8 +119,8 @@ class Models(QDialog): restoreGeom(d, "modelopts") d.exec_() saveGeom(d, "modelopts") - self.model['latexPre'] = unicode(frm.latexHeader.toPlainText()) - self.model['latexPost'] = unicode(frm.latexFooter.toPlainText()) + self.model['latexPre'] = str(frm.latexHeader.toPlainText()) + self.model['latexPost'] = str(frm.latexFooter.toPlainText()) def saveModel(self): self.mm.save(self.model) @@ -127,7 +128,7 @@ class Models(QDialog): def _tmpNote(self): self.mm.setCurrent(self.model) n = self.col.newNote(forDeck=False) - for name in n.keys(): + for name in list(n.keys()): n[name] = "("+name+")" try: if "{{cloze:Text}}" in self.model['tmpls'][0]['qfmt']: @@ -171,7 +172,7 @@ class AddModel(QDialog): # standard models self.models = [] for (name, func) in stdmodels.models: - if callable(name): + if isinstance(name, collections.Callable): name = name() item = QListWidgetItem(_("Add: %s") % name) self.dialog.models.addItem(item) diff --git a/aqt/overview.py b/aqt/overview.py index 2da65b55b..294c3df15 100644 --- a/aqt/overview.py +++ b/aqt/overview.py @@ -38,7 +38,7 @@ class Overview(object): if self.mw.state == "overview": tooltip(_("No cards are due yet.")) elif url == "anki": - print "anki menu" + print("anki menu") elif url == "opts": self.mw.onDeckConf() elif url == "cram": @@ -64,7 +64,7 @@ class Overview(object): def _keyHandler(self, evt): cram = self.mw.col.decks.current()['dyn'] - key = unicode(evt.text()) + key = str(evt.text()) if key == "o": self.mw.onDeckConf() if key == "r" and cram: diff --git a/aqt/preferences.py b/aqt/preferences.py index 379b82937..87f6df017 100644 --- a/aqt/preferences.py +++ b/aqt/preferences.py @@ -80,7 +80,7 @@ class Preferences(QDialog): f.timeLimit.setValue(qc['timeLim']/60.0) f.showEstimates.setChecked(qc['estTimes']) f.showProgress.setChecked(qc['dueCounts']) - f.newSpread.addItems(c.newCardSchedulingLabels().values()) + f.newSpread.addItems(list(c.newCardSchedulingLabels().values())) f.newSpread.setCurrentIndex(qc['newSpread']) f.useCurrent.setCurrentIndex(int(not qc.get("addToCur", True))) diff --git a/aqt/profiles.py b/aqt/profiles.py index b49ef2661..900f86338 100644 --- a/aqt/profiles.py +++ b/aqt/profiles.py @@ -8,7 +8,7 @@ import os import random -import cPickle +import pickle import locale import re @@ -21,7 +21,6 @@ from aqt import appHelpSite import aqt.forms from send2trash import send2trash - metaConf = dict( ver=0, updates=True, @@ -103,12 +102,13 @@ a flash drive.""" % self.base) def profiles(self): return sorted(x for x in - self.db.list("select name from profiles") - if x != "_global") + self.db.list("select name from profiles") + if x != "_global") def load(self, name, passwd=None): 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): self.name = None return False @@ -119,14 +119,14 @@ a flash drive.""" % self.base) def save(self): sql = "update profiles set data = ? where name = ?" - self.db.execute(sql, buffer(cPickle.dumps(self.profile)), self.name) - self.db.execute(sql, buffer(cPickle.dumps(self.meta)), "_global") + self.db.execute(sql, pickle.dumps(self.profile), self.name) + self.db.execute(sql, pickle.dumps(self.meta), "_global") self.db.commit() def create(self, name): prof = profileConf.copy() self.db.execute("insert into profiles values (?, ?)", - name, buffer(cPickle.dumps(prof))) + name, pickle.dumps(prof)) self.db.commit() def remove(self, name): @@ -262,9 +262,9 @@ create table if not exists profiles if not new: # load previously created try: - data = self.db.scalar( - "select cast(data as blob) from profiles where name = '_global'") - self.meta = cPickle.loads(str(data)) + self.meta = pickle.loads( + self.db.scalar( + "select cast(data as blob) from profiles where name = '_global'")) return except: recover() @@ -272,7 +272,7 @@ create table if not exists profiles # create a default global profile self.meta = metaConf.copy() self.db.execute("insert or replace into profiles values ('_global', ?)", - buffer(cPickle.dumps(metaConf))) + pickle.dumps(metaConf)) self._setDefaultLang() return True @@ -281,16 +281,16 @@ create table if not exists profiles if self.firstRun: self.create(_("User 1")) 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, to make backups easy. To tell Anki to use a different location, please see: %s -""") % (appHelpSite + "#startupopts")).encode("utf8")) +""") % (appHelpSite + "#startupopts")) def _pwhash(self, passwd): - return checksum(unicode(self.meta['id'])+unicode(passwd)) + return checksum(str(self.meta['id'])+str(passwd)) # Default language ###################################################################### @@ -299,8 +299,8 @@ please see: def _setDefaultLang(self): # the dialog expects _ to be defined, but we're running before # setupLang() has been called. so we create a dummy op for now - import __builtin__ - __builtin__.__dict__['_'] = lambda x: x + import builtins + builtins.__dict__['_'] = lambda x: x # create dialog class NoCloseDiag(QDialog): def reject(self): @@ -350,6 +350,6 @@ please see: def setLang(self, code): self.meta['defaultLang'] = code 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() anki.lang.setLang(code, local=False) diff --git a/aqt/progress.py b/aqt/progress.py index b14b50aab..b50c2647a 100644 --- a/aqt/progress.py +++ b/aqt/progress.py @@ -32,8 +32,8 @@ class ProgressManager(object): try: db.set_progress_handler(self._dbProgress, 10000) except: - print """\ -Your pysqlite2 is too old. Anki will appear frozen during long operations.""" + print("""\ +Your pysqlite2 is too old. Anki will appear frozen during long operations.""") def _dbProgress(self): "Called from SQLite." diff --git a/aqt/reviewer.py b/aqt/reviewer.py index 9bc84a252..8eeec0bab 100644 --- a/aqt/reviewer.py +++ b/aqt/reviewer.py @@ -2,12 +2,11 @@ # Copyright: Damien Elmes # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from __future__ import division import difflib import re import cgi import unicodedata as ucd -import HTMLParser +import html.parser from anki.lang import _, ngettext 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');") def _keyHandler(self, evt): - key = unicode(evt.text()) + key = str(evt.text()) if key == "e": self.mw.onEditCurrent() elif (key == " " or evt.key() in (Qt.Key_Return, Qt.Key_Enter)): @@ -409,12 +408,12 @@ Please run Tools>Empty Cards""") buf = buf.replace("
", "") hadHR = len(buf) != origSize # munge correct value - parser = HTMLParser.HTMLParser() + parser = html.parser.HTMLParser() cor = stripHTML(self.mw.col.media.strip(self.typeCorrect)) # ensure we don't chomp multiple whitespace cor = cor.replace(" ", " ") cor = parser.unescape(cor) - cor = cor.replace(u"\xa0", " ") + cor = cor.replace("\xa0", " ") given = self.typedAnswer # compare with typed answer res = self.correct(given, cor, showBad=False) diff --git a/aqt/sync.py b/aqt/sync.py index 13696360d..691f113f3 100644 --- a/aqt/sync.py +++ b/aqt/sync.py @@ -1,7 +1,6 @@ # Copyright: Damien Elmes # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -from __future__ import division import socket import time import traceback @@ -324,8 +323,6 @@ class SyncThread(QThread): self._sync() except: err = traceback.format_exc() - if not isinstance(err, unicode): - err = unicode(err, "utf8", "replace") self.fireEvent("error", err) finally: # don't bump mod time unless we explicitly save @@ -348,7 +345,7 @@ class SyncThread(QThread): # run sync and check state try: ret = self.client.sync() - except Exception, e: + except Exception as e: log = traceback.format_exc() err = repr(str(e)) if ("Unable to find the server" in err or @@ -357,8 +354,6 @@ class SyncThread(QThread): else: if not err: err = log - if not isinstance(err, unicode): - err = unicode(err, "utf8", "replace") self.fireEvent("error", err) return if ret == "badAuth": @@ -429,22 +424,21 @@ class SyncThread(QThread): ###################################################################### CHUNK_SIZE = 65536 -import httplib, httplib2 -from cStringIO import StringIO +import http.client, httplib2 +from io import StringIO from anki.hooks import runHook # sending in httplib def _incrementalSend(self, data): + print("fixme: _incrementalSend needs updating for python3") """Send `data' to the server.""" if self.sock is None: if self.auto_open: self.connect() else: - raise httplib.NotConnected() + raise http.client.NotConnected() # if it's not a file object, make it one if not hasattr(data, 'read'): - if isinstance(data, unicode): - data = data.encode("utf8") data = StringIO(data) while 1: block = data.read(CHUNK_SIZE) @@ -453,7 +447,7 @@ def _incrementalSend(self, data): self.sock.sendall(block) runHook("httpSend", len(block)) -httplib.HTTPConnection.send = _incrementalSend +http.client.HTTPConnection.send = _incrementalSend # receiving in httplib2 # 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 # - retries only when keep-alive connection is closed def _conn_request(self, conn, request_uri, method, body, headers): + print("fixme: _conn_request updating for python3") for i in range(2): try: if conn.sock is None: @@ -475,20 +470,20 @@ def _conn_request(self, conn, request_uri, method, body, headers): except httplib2.ssl_SSLError: conn.close() raise - except socket.error, e: + except socket.error as e: conn.close() raise - except httplib.HTTPException: + except http.client.HTTPException: conn.close() raise try: response = conn.getresponse() - except httplib.BadStatusLine: - print "retry bad line" + except http.client.BadStatusLine: + print("retry bad line") conn.close() conn.connect() continue - except (socket.error, httplib.HTTPException): + except (socket.error, http.client.HTTPException): raise else: content = "" diff --git a/aqt/tagedit.py b/aqt/tagedit.py index c6987714f..836730543 100644 --- a/aqt/tagedit.py +++ b/aqt/tagedit.py @@ -68,10 +68,10 @@ class TagCompleter(QCompleter): self.cursor = None def splitPath(self, str): - str = unicode(str).strip() + str = str(str).strip() str = re.sub(" +", " ", str) self.tags = self.edit.col.tags.split(str) - self.tags.append(u"") + self.tags.append("") p = self.edit.cursorPosition() self.cursor = str.count(" ", 0, p) return [self.tags[self.cursor]] @@ -80,9 +80,9 @@ class TagCompleter(QCompleter): if self.cursor is None: return self.edit.text() ret = QCompleter.pathFromIndex(self, idx) - self.tags[self.cursor] = unicode(ret) + self.tags[self.cursor] = str(ret) try: - self.tags.remove(u"") + self.tags.remove("") except ValueError: pass return " ".join(self.tags) diff --git a/aqt/update.py b/aqt/update.py index b31fd452c..7a32f0989 100644 --- a/aqt/update.py +++ b/aqt/update.py @@ -1,8 +1,8 @@ # Copyright: Damien Elmes # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import urllib -import urllib2 +import urllib.request, urllib.parse, urllib.error +import urllib.request, urllib.error, urllib.parse import time from aqt.qt import * @@ -32,9 +32,9 @@ class LatestVersionFinder(QThread): return d = self._data() d['proto'] = 1 - d = urllib.urlencode(d) + d = urllib.parse.urlencode(d) try: - f = urllib2.urlopen(aqt.appUpdate, d) + f = urllib.request.urlopen(aqt.appUpdate, d) resp = f.read() if not resp: return diff --git a/aqt/utils.py b/aqt/utils.py index 0f73514fa..e193b6af0 100644 --- a/aqt/utils.py +++ b/aqt/utils.py @@ -3,7 +3,7 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from aqt.qt import * -import re, os, sys, urllib, subprocess +import re, os, sys, urllib.request, urllib.parse, urllib.error, subprocess import aqt from anki.sound import stripSounds from anki.utils import isWin, isMac, invalidFilename @@ -143,7 +143,7 @@ def askUserDialog(text, buttons, parent=None, help="", title="Anki"): 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): QDialog.__init__(self, parent) self.setWindowTitle(title) @@ -183,21 +183,21 @@ class GetTextDialog(QDialog): def helpRequested(self): 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: parent = aqt.mw.app.activeWindow() or aqt.mw d = GetTextDialog(parent, prompt, help=help, edit=edit, default=default, title=title) d.setWindowModality(Qt.WindowModal) ret = d.exec_() - return (unicode(d.l.text()), ret) + return (str(d.l.text()), ret) def getOnlyText(*args, **kwargs): (s, r) = getText(*args, **kwargs) if r: return s else: - return u"" + return "" # fixme: these utilities could be combined into a single base class def chooseList(prompt, choices, startrow=0, parent=None): @@ -251,7 +251,7 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None): def accept(): # work around an osx crash #aqt.mw.app.processEvents() - file = unicode(list(d.selectedFiles())[0]) + file = str(list(d.selectedFiles())[0]) if dirkey: dir = os.path.dirname(file) 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' base = aqt.mw.pm.profile.get(config_key, aqt.mw.pm.base) path = os.path.join(base, fname) - file = unicode(QFileDialog.getSaveFileName( - parent, title, path, u"{0} (*{1})".format(key, ext), + file = str(QFileDialog.getSaveFileName( + parent, title, path, "{0} (*{1})".format(key, ext), options=QFileDialog.DontConfirmOverwrite)) if file: # add extension @@ -350,18 +350,16 @@ def getBase(col): base = None mdir = col.media.dir() if isWin and not mdir.startswith("\\\\"): - prefix = u"file:///" + prefix = "file:///" else: - prefix = u"file://" + prefix = "file://" mdir = mdir.replace("\\", "/") - base = prefix + unicode( - urllib.quote(mdir.encode("utf-8")), - "utf-8") + "/" + base = prefix + urllib.parse.quote(mdir) + "/" return '' % base def openFolder(path): if isWin: - if isinstance(path, unicode): + if isinstance(path, str): path = path.encode(sys.getfilesystemencoding()) subprocess.Popen(["explorer", path]) else: @@ -387,9 +385,9 @@ def addCloseShortcut(widg): def downArrow(): if isWin: - return u"▼" + return "▼" # windows 10 is lacking the smaller arrow on English installs - return u"▾" + return "▾" # Tooltips ###################################################################### diff --git a/aqt/webview.py b/aqt/webview.py index 1cb7810d0..c9f40ff71 100644 --- a/aqt/webview.py +++ b/aqt/webview.py @@ -15,10 +15,10 @@ import anki.js class Bridge(QObject): @pyqtSlot(str, result=str) def run(self, str): - return unicode(self._bridge(unicode(str))) + return self._bridge(str) @pyqtSlot(str) def link(self, str): - self._linkHandler(unicode(str)) + self._linkHandler(str) def setBridge(self, func): self._bridge = func def setLinkHandler(self, func): @@ -146,7 +146,7 @@ button { def _jsErr(self, msg, line, srcID): sys.stdout.write( (_("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): self.linkHandler(url.toString()) diff --git a/tests/shared.py b/tests/shared.py index b081b0cbe..e9b269c98 100644 --- a/tests/shared.py +++ b/tests/shared.py @@ -37,6 +37,6 @@ def getUpgradeDeckPath(name="anki12.anki"): src = os.path.join(testDir, "support", name) (fd, dst) = tempfile.mkstemp(suffix=".anki2") shutil.copy(src, dst) - return unicode(dst, "utf8") + return dst testDir = os.path.dirname(__file__) diff --git a/tests/test_cards.py b/tests/test_cards.py index 16a47982a..5731228fd 100644 --- a/tests/test_cards.py +++ b/tests/test_cards.py @@ -5,8 +5,8 @@ from tests.shared import getEmptyCol def test_previewCards(): deck = getEmptyCol() f = deck.newNote() - f['Front'] = u'1' - f['Back'] = u'2' + f['Front'] = '1' + f['Back'] = '2' # non-empty and active cards = deck.previewCards(f, 0) assert len(cards) == 1 @@ -25,8 +25,8 @@ def test_previewCards(): def test_delete(): deck = getEmptyCol() f = deck.newNote() - f['Front'] = u'1' - f['Back'] = u'2' + f['Front'] = '1' + f['Back'] = '2' deck.addNote(f) cid = f.cards()[0].id deck.reset() @@ -41,8 +41,8 @@ def test_delete(): def test_misc(): d = getEmptyCol() f = d.newNote() - f['Front'] = u'1' - f['Back'] = u'2' + f['Front'] = '1' + f['Back'] = '2' d.addNote(f) c = f.cards()[0] id = d.models.current()['id'] @@ -51,8 +51,8 @@ def test_misc(): def test_genrem(): d = getEmptyCol() f = d.newNote() - f['Front'] = u'1' - f['Back'] = u'' + f['Front'] = '1' + f['Back'] = '' d.addNote(f) assert len(f.cards()) == 1 m = d.models.current() @@ -80,7 +80,7 @@ def test_gendeck(): cloze = d.models.byName("Cloze") d.models.setCurrent(cloze) f = d.newNote() - f['Text'] = u'{{c1::one}}' + f['Text'] = '{{c1::one}}' d.addNote(f) assert d.cardCount() == 1 assert f.cards()[0].did == 1 @@ -89,11 +89,11 @@ def test_gendeck(): cloze['did'] = newId d.models.save(cloze) # a newly generated card should share the first card's deck - f['Text'] += u'{{c2::two}}' + f['Text'] += '{{c2::two}}' f.flush() assert f.cards()[1].did == 1 # and same with multiple cards - f['Text'] += u'{{c3::three}}' + f['Text'] += '{{c3::three}}' f.flush() assert f.cards()[2].did == 1 # if one of the cards is in a different deck, it should revert to the @@ -101,7 +101,7 @@ def test_gendeck(): c = f.cards()[1] c.did = newId c.flush() - f['Text'] += u'{{c4::four}}' + f['Text'] += '{{c4::four}}' f.flush() assert f.cards()[3].did == newId diff --git a/tests/test_collection.py b/tests/test_collection.py index b1db640ef..2b6415dec 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -37,14 +37,14 @@ def test_openReadOnly(): os.chmod(newPath, 0) assertException(Exception, lambda: aopen(newPath)) - os.chmod(newPath, 0666) + os.chmod(newPath, 0o666) os.unlink(newPath) def test_noteAddDelete(): deck = getEmptyCol() # add a note f = deck.newNote() - f['Front'] = u"one"; f['Back'] = u"two" + f['Front'] = "one"; f['Back'] = "two" n = deck.addNote(f) assert n == 1 # test multiple cards - add another template @@ -62,7 +62,7 @@ def test_noteAddDelete(): assert deck.cardCount() == 2 # creating new notes should use both cards f = deck.newNote() - f['Front'] = u"three"; f['Back'] = u"four" + f['Front'] = "three"; f['Back'] = "four" n = deck.addNote(f) assert n == 2 assert deck.cardCount() == 4 @@ -73,7 +73,7 @@ def test_noteAddDelete(): assert not f.dupeOrEmpty() # now let's make a duplicate f2 = deck.newNote() - f2['Front'] = u"one"; f2['Back'] = u"" + f2['Front'] = "one"; f2['Back'] = "" assert f2.dupeOrEmpty() # empty first field should not be permitted either f2['Front'] = " " @@ -82,12 +82,12 @@ def test_noteAddDelete(): def test_fieldChecksum(): deck = getEmptyCol() f = deck.newNote() - f['Front'] = u"new"; f['Back'] = u"new2" + f['Front'] = "new"; f['Back'] = "new2" deck.addNote(f) assert deck.db.scalar( "select csum from notes") == int("c2a6b03f", 16) # changing the val should change the checksum - f['Front'] = u"newx" + f['Front'] = "newx" f.flush() assert deck.db.scalar( "select csum from notes") == int("302811ae", 16) @@ -95,10 +95,10 @@ def test_fieldChecksum(): def test_addDelTags(): deck = getEmptyCol() f = deck.newNote() - f['Front'] = u"1" + f['Front'] = "1" deck.addNote(f) f2 = deck.newNote() - f2['Front'] = u"2" + f2['Front'] = "2" deck.addNote(f2) # adding for a given id deck.tags.bulkAdd([f.id], "foo") diff --git a/tests/test_decks.py b/tests/test_decks.py index efe027e2f..f127688f4 100644 --- a/tests/test_decks.py +++ b/tests/test_decks.py @@ -46,7 +46,7 @@ def test_remove(): # create a new deck, and add a note/card to it g1 = deck.decks.id("g1") f = deck.newNote() - f['Front'] = u"1" + f['Front'] = "1" f.model()['did'] = g1 deck.addNote(f) c = f.cards()[0] @@ -92,7 +92,7 @@ def test_renameForDragAndDrop(): d = getEmptyCol() 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') chinese_did = d.decks.id('Chinese') diff --git a/tests/test_exporting.py b/tests/test_exporting.py index 63d950513..61c417356 100644 --- a/tests/test_exporting.py +++ b/tests/test_exporting.py @@ -4,7 +4,7 @@ import nose, os, tempfile from anki import Collection as aopen from anki.exporting import * from anki.importing import Anki2Importer -from shared import getEmptyCol +from .shared import getEmptyCol deck = None ds = None @@ -14,11 +14,11 @@ def setup1(): global deck deck = getEmptyCol() 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) # with a different deck 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") deck.addNote(f) @@ -37,7 +37,7 @@ def test_export_anki(): # export e = AnkiExporter(deck) fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2") - newname = unicode(newname) + newname = str(newname) os.close(fd) os.unlink(newname) e.exportInto(newname) @@ -57,7 +57,7 @@ def test_export_anki(): assert dobj['conf'] == 1 # try again, limited to a deck fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2") - newname = unicode(newname) + newname = str(newname) os.close(fd) os.unlink(newname) e.did = 1 @@ -68,13 +68,13 @@ def test_export_anki(): @nose.with_setup(setup1) def test_export_ankipkg(): # 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['Front'] = u'[sound:今日.mp3]' + n['Front'] = '[sound:今日.mp3]' deck.addNote(n) e = AnkiPackageExporter(deck) fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".apkg") - newname = unicode(newname) + newname = str(newname) os.close(fd) os.unlink(newname) e.exportInto(newname) @@ -83,7 +83,7 @@ def test_export_ankipkg(): def test_export_anki_due(): deck = getEmptyCol() f = deck.newNote() - f['Front'] = u"foo" + f['Front'] = "foo" deck.addNote(f) deck.crt -= 86400*10 deck.sched.reset() @@ -99,7 +99,7 @@ def test_export_anki_due(): e = AnkiExporter(deck) e.includeSched = True fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2") - newname = unicode(newname) + newname = str(newname) os.close(fd) os.unlink(newname) e.exportInto(newname) @@ -124,7 +124,7 @@ def test_export_anki_due(): def test_export_textnote(): e = TextNoteExporter(deck) fd, f = tempfile.mkstemp(prefix="ankitest") - f = unicode(f) + f = str(f) os.close(fd) os.unlink(f) e.exportInto(f) diff --git a/tests/test_find.py b/tests/test_find.py index 42058f3f1..8a1539bb0 100644 --- a/tests/test_find.py +++ b/tests/test_find.py @@ -22,21 +22,21 @@ def test_parse(): def test_findCards(): deck = getEmptyCol() f = deck.newNote() - f['Front'] = u'dog' - f['Back'] = u'cat' - f.tags.append(u"monkey") + f['Front'] = 'dog' + f['Back'] = 'cat' + f.tags.append("monkey") f1id = f.id deck.addNote(f) firstCardId = f.cards()[0].id f = deck.newNote() - f['Front'] = u'goats are fun' - f['Back'] = u'sheep' - f.tags.append(u"sheep goat horse") + f['Front'] = 'goats are fun' + f['Back'] = 'sheep' + f.tags.append("sheep goat horse") deck.addNote(f) f2id = f.id f = deck.newNote() - f['Front'] = u'cat' - f['Back'] = u'sheep' + f['Front'] = 'cat' + f['Back'] = 'sheep' deck.addNote(f) catCard = f.cards()[0] m = deck.models.current(); mm = deck.models @@ -46,8 +46,8 @@ def test_findCards(): mm.addTemplate(m, t) mm.save(m) f = deck.newNote() - f['Front'] = u'test' - f['Back'] = u'foo bar' + f['Front'] = 'test' + f['Back'] = 'foo bar' deck.addNote(f) latestCardIds = [c.id for c in f.cards()] # tag searches @@ -131,8 +131,8 @@ def test_findCards(): assert len(deck.findCards("deck:*cefault")) == 0 # full search f = deck.newNote() - f['Front'] = u'helloworld' - f['Back'] = u'abc' + f['Front'] = 'helloworld' + f['Back'] = 'abc' deck.addNote(f) # as it's the sort field, it matches assert len(deck.findCards("helloworld")) == 2 @@ -195,8 +195,8 @@ def test_findCards(): # empty field assert len(deck.findCards("front:")) == 0 f = deck.newNote() - f['Front'] = u'' - f['Back'] = u'abc2' + f['Front'] = '' + f['Back'] = 'abc2' assert deck.addNote(f) == 1 assert len(deck.findCards("front:")) == 1 # OR searches and nesting @@ -218,12 +218,12 @@ def test_findCards(): def test_findReplace(): deck = getEmptyCol() f = deck.newNote() - f['Front'] = u'foo' - f['Back'] = u'bar' + f['Front'] = 'foo' + f['Back'] = 'bar' deck.addNote(f) f2 = deck.newNote() - f2['Front'] = u'baz' - f2['Back'] = u'foo' + f2['Front'] = 'baz' + f2['Back'] = 'foo' deck.addNote(f2) nids = [f.id, f2.id] # should do nothing @@ -245,20 +245,20 @@ def test_findReplace(): def test_findDupes(): deck = getEmptyCol() f = deck.newNote() - f['Front'] = u'foo' - f['Back'] = u'bar' + f['Front'] = 'foo' + f['Back'] = 'bar' deck.addNote(f) f2 = deck.newNote() - f2['Front'] = u'baz' - f2['Back'] = u'bar' + f2['Front'] = 'baz' + f2['Back'] = 'bar' deck.addNote(f2) f3 = deck.newNote() - f3['Front'] = u'quux' - f3['Back'] = u'bar' + f3['Front'] = 'quux' + f3['Back'] = 'bar' deck.addNote(f3) f4 = deck.newNote() - f4['Front'] = u'quuux' - f4['Back'] = u'nope' + f4['Front'] = 'quuux' + f4['Back'] = 'nope' deck.addNote(f4) r = deck.findDupes("Back") assert r[0][0] == "bar" diff --git a/tests/test_importing.py b/tests/test_importing.py index 4f7bf77f1..15eb7d0fb 100644 --- a/tests/test_importing.py +++ b/tests/test_importing.py @@ -2,9 +2,8 @@ import os from tests.shared import getUpgradeDeckPath, getEmptyCol -from anki.upgrade import Upgrader from anki.utils import ids2str -from anki.importing import Anki1Importer, Anki2Importer, TextImporter, \ +from anki.importing import Anki2Importer, TextImporter, \ SupermemoXmlImporter, MnemosyneImporter, AnkiPackageImporter testDir = os.path.dirname(__file__) @@ -12,43 +11,6 @@ testDir = os.path.dirname(__file__) srcNotes=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(): tmp = getEmptyCol() # add a note that references a sound @@ -96,7 +58,7 @@ def test_anki2_mediadupes(): def test_apkg(): tmp = getEmptyCol() - apkg = unicode(os.path.join(testDir, "support/media.apkg")) + apkg = str(os.path.join(testDir, "support/media.apkg")) imp = AnkiPackageImporter(tmp, apkg) assert os.listdir(tmp.media.dir()) == [] imp.run() @@ -113,65 +75,6 @@ def test_apkg(): imp.run() 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(): # create a new empty deck dst = getEmptyCol() @@ -254,7 +157,7 @@ def test_anki2_updates(): def test_csv(): 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.initMapping() i.run() @@ -299,7 +202,7 @@ def test_csv2(): n['Three'] = "3" deck.addNote(n) # 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.initMapping() i.run() @@ -311,7 +214,7 @@ def test_csv2(): def test_supermemo_xml_01_unicode(): 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.META.logToStdOutput = True i.run() @@ -325,7 +228,7 @@ def test_supermemo_xml_01_unicode(): def test_mnemo(): 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.run() assert deck.cardCount() == 7 diff --git a/tests/test_latex.py b/tests/test_latex.py index cb723461f..e762f0fab 100644 --- a/tests/test_latex.py +++ b/tests/test_latex.py @@ -1,6 +1,9 @@ # coding: utf-8 import os + +import shutil + from tests.shared import getEmptyCol from anki.utils import stripHTML @@ -11,7 +14,7 @@ def test_latex(): anki.latex.latexCmds[0][0] = "nolatex" # add a note with latex f = d.newNote() - f['Front'] = u"[latex]hello[/latex]" + f['Front'] = "[latex]hello[/latex]" d.addNote(f) # but since latex couldn't run, there's nothing there assert len(os.listdir(d.media.dir())) == 0 @@ -20,11 +23,9 @@ def test_latex(): assert "executing nolatex" in msg assert "installed" in msg # check if we have latex installed, and abort test if we don't - for cmd in ("latex", "dvipng"): - if (not os.path.exists("/usr/bin/"+cmd) and - not os.path.exists("/usr/texbin/"+cmd)): - print "aborting test; %s is not installed" % cmd - return + if not shutil.which("latex") or not shutil.which("dvipng"): + print("aborting test; %s is not installed" % cmd) + return # fix path anki.latex.latexCmds[0][0] = "latex" # check media db should cause latex to be generated @@ -33,13 +34,13 @@ def test_latex(): assert ".png" in f.cards()[0].q() # adding new notes should cause generation on question display f = d.newNote() - f['Front'] = u"[latex]world[/latex]" + f['Front'] = "[latex]world[/latex]" d.addNote(f) f.cards()[0].q() assert len(os.listdir(d.media.dir())) == 2 # another note with the same media should reuse f = d.newNote() - f['Front'] = u" [latex]world[/latex]" + f['Front'] = " [latex]world[/latex]" d.addNote(f) assert len(os.listdir(d.media.dir())) == 2 oldcard = f.cards()[0] @@ -48,7 +49,7 @@ def test_latex(): # missing media will show the latex anki.latex.build = False f = d.newNote() - f['Front'] = u"[latex]foo[/latex]" + f['Front'] = "[latex]foo[/latex]" d.addNote(f) assert len(os.listdir(d.media.dir())) == 2 assert stripHTML(f.cards()[0].q()) == "[latex]foo[/latex]" @@ -107,7 +108,7 @@ def test_good_latex_command_works(): def _test_includes_bad_command(bad): d = getEmptyCol() f = d.newNote() - f['Front'] = u'[latex]%s[/latex]' % bad; + f['Front'] = '[latex]%s[/latex]' % bad; d.addNote(f) q = f.cards()[0].q() return ("'%s' is not allowed on cards" % bad in q, "Card content: %s" % q) \ No newline at end of file diff --git a/tests/test_media.py b/tests/test_media.py index b18969b5f..7a1c391c5 100644 --- a/tests/test_media.py +++ b/tests/test_media.py @@ -4,14 +4,14 @@ import tempfile import os import time -from shared import getEmptyCol, testDir +from .shared import getEmptyCol, testDir # copying files to media folder def test_add(): d = getEmptyCol() 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") # new file, should preserve name assert d.media.addFile(path) == "foo.jpg" @@ -24,7 +24,7 @@ def test_add(): def test_strings(): d = getEmptyCol() mf = d.media.filesInStr - mid = d.models.models.keys()[0] + mid = list(d.models.models.keys())[0] assert mf(mid, "aoeu") == [] assert mf(mid, "aoeuao") == ["foo.jpg"] assert mf(mid, "aoeuao") == ["foo.jpg"] @@ -50,18 +50,18 @@ def test_deckIntegration(): # create a media dir d.media.dir() # 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) # add a note which references it f = d.newNote() - f['Front'] = u"one"; f['Back'] = u"" + f['Front'] = "one"; f['Back'] = "" d.addNote(f) # and one which references a non-existent file f = d.newNote() - f['Front'] = u"one"; f['Back'] = u"" + f['Front'] = "one"; f['Back'] = "" d.addNote(f) # 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 ret = d.media.check() assert ret[0] == ["fake2.png"] @@ -78,7 +78,7 @@ def test_changes(): assert not list(removed()) # add a file 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") time.sleep(1) path = d.media.addFile(path) @@ -106,8 +106,8 @@ def test_changes(): def test_illegal(): d = getEmptyCol() - aString = u"a:b|cd\\e/f\0g*h" - good = u"abcdefgh" + aString = "a:b|cd\\e/f\0g*h" + good = "abcdefgh" assert d.media.stripIllegal(aString) == good for c in aString: bad = d.media.hasIllegal("somestring"+c+"morestring") diff --git a/tests/test_models.py b/tests/test_models.py index aa03b47b5..7eb43ccb3 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -6,8 +6,8 @@ from anki.utils import stripHTML, joinFields def test_modelDelete(): deck = getEmptyCol() f = deck.newNote() - f['Front'] = u'1' - f['Back'] = u'2' + f['Front'] = '1' + f['Back'] = '2' deck.addNote(f) assert deck.cardCount() == 1 deck.models.rem(deck.models.current()) @@ -29,8 +29,8 @@ def test_modelCopy(): def test_fields(): d = getEmptyCol() f = d.newNote() - f['Front'] = u'1' - f['Back'] = u'2' + f['Front'] = '1' + f['Back'] = '2' d.addNote(f) m = d.models.current() # make sure renaming a field updates the templates @@ -82,8 +82,8 @@ def test_templates(): mm.addTemplate(m, t) mm.save(m) f = d.newNote() - f['Front'] = u'1' - f['Back'] = u'2' + f['Front'] = '1' + f['Back'] = '2' d.addNote(f) assert d.cardCount() == 2 (c, c2) = f.cards() @@ -121,7 +121,7 @@ def test_cloze_ordinals(): d.models.remTemplate(m, m['tmpls'][0]) f = d.newNote() - f['Text'] = u'{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}' + f['Text'] = '{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}' d.addNote(f) assert d.cardCount() == 2 (c, c2) = f.cards() @@ -136,7 +136,7 @@ def test_text(): m['tmpls'][0]['qfmt'] = "{{text:Front}}" d.models.save(m) f = d.newNote() - f['Front'] = u'helloworld' + f['Front'] = 'helloworld' d.addNote(f) assert "helloworld" in f.cards()[0].q() @@ -146,7 +146,7 @@ def test_cloze(): f = d.newNote() assert f.model()['name'] == "Cloze" # a cloze model with no clozes is not empty - f['Text'] = u'nothing' + f['Text'] = 'nothing' assert d.addNote(f) # try with one cloze f = d.newNote() @@ -221,8 +221,8 @@ def test_modelChange(): mm.addTemplate(m, t) mm.save(m) f = deck.newNote() - f['Front'] = u'f' - f['Back'] = u'b123' + f['Front'] = 'f' + f['Back'] = 'b123' deck.addNote(f) # switch fields map = {0: 1, 1: 0} @@ -267,8 +267,8 @@ def test_modelChange(): assert f['Back'] == 'f' # another note to try model conversion f = deck.newNote() - f['Front'] = u'f2' - f['Back'] = u'b2' + f['Front'] = 'f2' + f['Back'] = 'b2' deck.addNote(f) assert deck.models.useCount(basic) == 2 assert deck.models.useCount(cloze) == 0 diff --git a/tests/test_sched.py b/tests/test_sched.py index f3b62240c..e1742c64a 100644 --- a/tests/test_sched.py +++ b/tests/test_sched.py @@ -28,7 +28,7 @@ def test_new(): assert d.sched.newCount == 0 # add a note f = d.newNote() - f['Front'] = u"one"; f['Back'] = u"two" + f['Front'] = "one"; f['Back'] = "two" d.addNote(f) d.reset() assert d.sched.newCount == 1 @@ -99,7 +99,7 @@ def test_newLimits(): def test_newBoxes(): d = getEmptyCol() f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) d.reset() c = d.sched.getCard() @@ -113,7 +113,7 @@ def test_learn(): d = getEmptyCol() # add a note f = d.newNote() - f['Front'] = u"one"; f['Back'] = u"two" + f['Front'] = "one"; f['Back'] = "two" f = d.addNote(f) # set as a learn card and rebuild queues d.db.execute("update cards set queue=0, type=0") @@ -126,7 +126,7 @@ def test_learn(): d.sched.answerCard(c, 1) # it should have three reps left to graduation assert c.left%1000 == 3 - assert c.left/1000 == 3 + assert c.left//1000 == 3 # it should by due in 30 seconds t = round(c.due - time.time()) assert t >= 25 and t <= 40 @@ -135,7 +135,7 @@ def test_learn(): # it should by due in 3 minutes assert round(c.due - time.time()) in (179, 180) assert c.left%1000 == 2 - assert c.left/1000 == 2 + assert c.left//1000 == 2 # check log is accurate log = d.db.first("select * from revlog order by id desc") assert log[3] == 2 @@ -146,7 +146,7 @@ def test_learn(): # it should by due in 10 minutes assert round(c.due - time.time()) in (599, 600) assert c.left%1000 == 1 - assert c.left/1000 == 1 + assert c.left//1000 == 1 # the next pass should graduate the card assert c.queue == 1 assert c.type == 1 @@ -187,10 +187,10 @@ def test_learn_collapsed(): d = getEmptyCol() # add 2 notes f = d.newNote() - f['Front'] = u"1" + f['Front'] = "1" f = d.addNote(f) f = d.newNote() - f['Front'] = u"2" + f['Front'] = "2" f = d.addNote(f) # set as a learn card and rebuild queues d.db.execute("update cards set queue=0, type=0") @@ -213,7 +213,7 @@ def test_learn_day(): d = getEmptyCol() # add a note f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" f = d.addNote(f) d.sched.reset() c = d.sched.getCard() @@ -222,7 +222,7 @@ def test_learn_day(): d.sched.answerCard(c, 2) # two reps to graduate, 1 more today assert c.left%1000 == 3 - assert c.left/1000 == 1 + assert c.left//1000 == 1 assert d.sched.counts() == (0, 1, 0) c = d.sched.getCard() ni = d.sched.nextIvl @@ -271,7 +271,7 @@ def test_reviews(): d = getEmptyCol() # add a note f = d.newNote() - f['Front'] = u"one"; f['Back'] = u"two" + f['Front'] = "one"; f['Back'] = "two" d.addNote(f) # set the card up as a review card, due 8 days ago c = f.cards()[0] @@ -362,7 +362,7 @@ def test_reviews(): def test_button_spacing(): d = getEmptyCol() f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) # 1 day ivl review card due now c = f.cards()[0] @@ -385,7 +385,7 @@ def test_overdue_lapse(): d = getEmptyCol() # add a note f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) # simulate a review that was lapsed and is now due for its normal review c = f.cards()[0] @@ -420,7 +420,7 @@ def test_finished(): assert "Congratulations" in d.sched.finishedMsg() assert "limit" not in d.sched.finishedMsg() f = d.newNote() - f['Front'] = u"one"; f['Back'] = u"two" + f['Front'] = "one"; f['Back'] = "two" d.addNote(f) # have a new card assert "new cards available" in d.sched.finishedMsg() @@ -436,7 +436,7 @@ def test_finished(): def test_nextIvl(): d = getEmptyCol() f = d.newNote() - f['Front'] = u"one"; f['Back'] = u"two" + f['Front'] = "one"; f['Back'] = "two" d.addNote(f) d.reset() conf = d.decks.confForDid(1) @@ -492,7 +492,7 @@ def test_nextIvl(): def test_misc(): d = getEmptyCol() f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) c = f.cards()[0] # burying @@ -506,7 +506,7 @@ def test_misc(): def test_suspend(): d = getEmptyCol() f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) c = f.cards()[0] # suspending @@ -549,7 +549,7 @@ def test_suspend(): def test_cram(): d = getEmptyCol() f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) c = f.cards()[0] c.ivl = 100 @@ -657,7 +657,7 @@ def test_cram(): def test_cram_rem(): d = getEmptyCol() f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) oldDue = f.cards()[0].due did = d.decks.newDyn("Cram") @@ -678,7 +678,7 @@ def test_cram_resched(): # add card d = getEmptyCol() f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) # cram deck did = d.decks.newDyn("Cram") @@ -806,7 +806,7 @@ def test_ordcycle(): def test_counts_idx(): d = getEmptyCol() f = d.newNote() - f['Front'] = u"one"; f['Back'] = u"two" + f['Front'] = "one"; f['Back'] = "two" d.addNote(f) d.reset() assert d.sched.counts() == (1, 0, 0) @@ -828,7 +828,7 @@ def test_counts_idx(): def test_repCounts(): d = getEmptyCol() f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) d.reset() # lrnReps should be accurate on pass/fail @@ -846,7 +846,7 @@ def test_repCounts(): d.sched.answerCard(d.sched.getCard(), 2) assert d.sched.counts() == (0, 0, 0) f = d.newNote() - f['Front'] = u"two" + f['Front'] = "two" d.addNote(f) d.reset() # initial pass should be correct too @@ -858,14 +858,14 @@ def test_repCounts(): assert d.sched.counts() == (0, 0, 0) # immediate graduate should work f = d.newNote() - f['Front'] = u"three" + f['Front'] = "three" d.addNote(f) d.reset() d.sched.answerCard(d.sched.getCard(), 3) assert d.sched.counts() == (0, 0, 0) # and failing a review should too f = d.newNote() - f['Front'] = u"three" + f['Front'] = "three" d.addNote(f) c = f.cards()[0] c.type = 2 @@ -907,7 +907,7 @@ def test_collapse(): d = getEmptyCol() # add a note f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) d.reset() # test collapsing @@ -921,11 +921,11 @@ def test_deckDue(): d = getEmptyCol() # add a note with default deck f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) # and one that's a child f = d.newNote() - f['Front'] = u"two" + f['Front'] = "two" default1 = f.model()['did'] = d.decks.id("Default::1") d.addNote(f) # make it a review card @@ -935,12 +935,12 @@ def test_deckDue(): c.flush() # add one more with a new deck f = d.newNote() - f['Front'] = u"two" + f['Front'] = "two" foobar = f.model()['did'] = d.decks.id("foo::bar") d.addNote(f) # and one that's a sibling f = d.newNote() - f['Front'] = u"three" + f['Front'] = "three" foobaz = f.model()['did'] = d.decks.id("foo::baz") d.addNote(f) d.reset() @@ -980,16 +980,16 @@ def test_deckFlow(): d = getEmptyCol() # add a note with default deck f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) # and one that's a child f = d.newNote() - f['Front'] = u"two" + f['Front'] = "two" default1 = f.model()['did'] = d.decks.id("Default::2") d.addNote(f) # and another that's higher up f = d.newNote() - f['Front'] = u"three" + f['Front'] = "three" default1 = f.model()['did'] = d.decks.id("Default::1") d.addNote(f) # should get top level one first, then ::1, then ::2 @@ -1004,10 +1004,10 @@ def test_reorder(): d = getEmptyCol() # add a note with default deck f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) f2 = d.newNote() - f2['Front'] = u"two" + f2['Front'] = "two" d.addNote(f2) assert f2.cards()[0].due == 2 found=False @@ -1022,10 +1022,10 @@ def test_reorder(): assert f.cards()[0].due == 1 # shifting f3 = d.newNote() - f3['Front'] = u"three" + f3['Front'] = "three" d.addNote(f3) f4 = d.newNote() - f4['Front'] = u"four" + f4['Front'] = "four" d.addNote(f4) assert f.cards()[0].due == 1 assert f2.cards()[0].due == 2 @@ -1041,7 +1041,7 @@ def test_reorder(): def test_forget(): d = getEmptyCol() f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) c = f.cards()[0] c.queue = 2; c.type = 2; c.ivl = 100; c.due = 0 @@ -1055,7 +1055,7 @@ def test_forget(): def test_resched(): d = getEmptyCol() f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) c = f.cards()[0] d.sched.reschedCards([c.id], 0, 0) @@ -1072,7 +1072,7 @@ def test_norelearn(): d = getEmptyCol() # add a note f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) c = f.cards()[0] c.type = 2 @@ -1092,7 +1092,7 @@ def test_norelearn(): def test_failmult(): d = getEmptyCol() f = d.newNote() - f['Front'] = u"one"; f['Back'] = u"two" + f['Front'] = "one"; f['Back'] = "two" d.addNote(f) c = f.cards()[0] c.type = 2 diff --git a/tests/test_sync.py b/tests/test_sync.py index 5e35a41e5..3ea85238c 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -21,14 +21,14 @@ def setup_basic(): deck1 = getEmptyCol() # add a note to deck 1 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) # answer it deck1.reset(); deck1.sched.answerCard(deck1.sched.getCard(), 4) # repeat for deck2 deck2 = getEmptyDeckWith(server=True) 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.reset(); deck2.sched.answerCard(deck2.sched.getCard(), 4) # start with same schema and sync time @@ -223,7 +223,7 @@ def test_threeway(): # client 1 adds a card at time 1 time.sleep(1) f = deck1.newNote() - f['Front'] = u"1"; + f['Front'] = "1"; deck1.addNote(f) deck1.save() # at time 2, client 2 syncs to server @@ -249,7 +249,7 @@ def test_threeway2(): # create collection 1 with a single note c1 = getEmptyCol() f = c1.newNote() - f['Front'] = u"startingpoint" + f['Front'] = "startingpoint" nid = f.id c1.addNote(f) cid = f.cards()[0].id @@ -329,9 +329,9 @@ def _test_speed(): deck1.scm = deck2.scm = 0 server = LocalServer(deck2) 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" - 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) def test_filtered_delete(): diff --git a/tests/test_undo.py b/tests/test_undo.py index c329c424e..a9b06b34c 100644 --- a/tests/test_undo.py +++ b/tests/test_undo.py @@ -27,7 +27,7 @@ def test_op(): # and a review will, too d.save("add") f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) d.reset() assert d.undoName() == "add" @@ -39,7 +39,7 @@ def test_review(): d = getEmptyCol() d.conf['counts'] = COUNT_REMAINING f = d.newNote() - f['Front'] = u"one" + f['Front'] = "one" d.addNote(f) d.reset() assert not d.undoName() @@ -62,7 +62,7 @@ def test_review(): assert not d.undoName() # we should be able to undo multiple answers too f = d.newNote() - f['Front'] = u"two" + f['Front'] = "two" d.addNote(f) d.reset() assert d.sched.counts() == (2, 0, 0) diff --git a/tools/tests.sh b/tools/tests.sh index 1e189760a..a73184f8e 100755 --- a/tools/tests.sh +++ b/tools/tests.sh @@ -22,5 +22,5 @@ else args="" echo "Call with coverage=1 to run coverage tests" fi -(cd $dir && nosetests -vs $lim $args --cover-package=anki) +(cd $dir && nosetests3 -vs $lim $args --cover-package=anki)