def test_01_xml(self): db = Database() acc1 = Account("Root") db += acc1 acc2 = Account("Foo") acc1 += acc2 acc3 = Account("Bar") acc1 += acc3 t = Transaction(datetime.date.today(), "bar") db += t i = Item("AAA", -2) t += i i += acc1 i = Item("BBB", 2) t += i i += acc2 t = Transaction(datetime.date.today() + datetime.timedelta(days=1), "foo") db += t i = Item("CCC", 2.99) t += i i += acc1 i = Item("DDD", -2.99) i += acc3 t += i t = Transaction(datetime.date.today() - datetime.timedelta(days=3), "Flix") db += t i = Item(u"EEE <>/\\!'\"§$%&/(){}", 4) t += i i += acc1 i = Item(u"FFF öäüÖÄÜß", -4) t += i i += acc3 # remove the date/time depending string datePatchRe = re.compile("^\s*<saved\s+datetime=\".+?\"\s*/>\s*$", re.MULTILINE) xml = etree.tostring(db.toXml(), encoding="utf-8", xml_declaration=True, pretty_print=True).decode("utf-8") print(xml) xml = datePatchRe.sub("", xml) print(xml) db2 = Database.parseFromXml( etree.parse(BytesIO(xml.encode("utf-8"))).getroot()) xml2 = etree.tostring(db2.toXml(), encoding="utf-8", xml_declaration=True, pretty_print=True).decode("utf-8") print(xml2) xml2 = datePatchRe.sub("", xml2) self.assertEqual(xml, xml2, "xml differs")
def menuNewDatabase(self): "Create a new and empty database." if not self.checkUnsavedChanges(): return LOGGER.info("Creating new database...") self._clearViews() self._db = Database() self._dbPath = "unnamed" self._accountTree.setDatabase(self._db) self.statusBar().showMessage("Created empty database", 2000)
def test_balanced(self): db = Database() asset = Account("Asset") db += asset debit = Account("Debit") db += asset t = Transaction(datetime.date.today()) db += t i = Item("Hello World", 2) t += i i += asset self.assertEqual(Decimal("2.00"), t.getBalance()) self.assertFalse(t.isBalanced()) i = Item("Foo Bar", -1) t += i i += debit self.assertEqual(Decimal("1.00"), t.getBalance()) self.assertFalse(t.isBalanced()) i.setValue(-2) self.assertEqual(Decimal("0"), t.getBalance()) self.assertTrue(t.isBalanced())
def test_01_root(self): db = Database() acc = Account("Root") db += acc self.assertIn("Root", db) self.assertIsNotNone(db["Root"]) self.assertNotIn("Foo", db) with self.assertRaises(KeyError): db["Foo"]
def test_transactions(self): today = datetime.date.today() db = Database() acc1 = Account("A") db += acc1 acc2 = Account("B") db += acc2 t0 = Transaction(today, "B") db += t0 i0 = Item("AA", 1) t0 += i0 i0 += acc1 i1 = Item("BB", -1) t0 += i1 i1 += acc2 t1 = Transaction(today + datetime.timedelta(days=1), "C") db += t1 i2 = Item("CC", -2) t1 += i2 i2 += acc1 i3 = Item("DD", -2) t1 += i3 i3 += acc2 t2 = Transaction(today - datetime.timedelta(days=5), "A") db += t2 i4 = Item("EE", -3) t2 += i4 i4 += acc1 i5 = Item("FF", -3) t2 += i5 i5 += acc2 expected_transactions = [t2, t0, t1] current_transactions = list(db.filterTransactions()) self.assertListEqual(expected_transactions, current_transactions) t1.setDate(today - datetime.timedelta(days=1)) expected_transactions = [t2, t1, t0] current_transactions = list(db.filterTransactions()) self.assertListEqual(expected_transactions, current_transactions)
def test_02_sub(self): db = Database() acc = Account("Root") db += acc sacc = Account("Sub") acc += sacc self.assertIn("Root/Sub", db) self.assertIsNotNone(db["Root/Sub"]) self.assertNotIn("Root/Foo", db) with self.assertRaises(KeyError): db["Root/Foo"]
def setUp(self): self._db = Database() accRoot = Account("Root") self._db += accRoot accFoo = Account("Foo") accRoot += accFoo accBar = Account("Bar") accRoot += accBar accTest = Account("Test") accRoot += accTest t = self._newTransaction(self.dates[0], "one") self._newItem("AAA", 2.5, t, accFoo) self._newItem("BBB", -2.5, t, accBar) t = self._newTransaction(self.dates[1], "two") self._newItem("CCC", -2, t, accRoot) self._newItem("DDD", 2, t, accTest) t = self._newTransaction(self.dates[2], "three") self._newItem(u"EEE", 4, t, accFoo) self._newItem(u"FFF", -4, t, accBar) t = self._newTransaction(self.dates[2], "four") self._newItem(u"GGG", 3, t, accFoo) self._newItem(u"HHH", -3, t, accBar) t = self._newTransaction(self.dates[3], "five") self._newItem(u"III", 5, t, accFoo) self._newItem(u"JJJ", -5, t, accTest) t = self._newTransaction(self.dates[4], "six") self._newItem(u"KKK", 5, t, accFoo) self._newItem(u"LLL", -5, t, accBar) t = self._newTransaction(self.dates[4], "seven") self._newItem(u"MMM", 7, t, accFoo) self._newItem(u"NNN", -7, t, accBar) t = self._newTransaction(self.dates[5], "eight") self._newItem(u"OOO", 1, t, accFoo) self._newItem(u"PPP", -1, t, accBar) t = self._newTransaction(self.dates[6], "nine") self._newItem(u"QQQ", 1, t, accTest) self._newItem(u"RRR", -1, t, accBar) t = self._newTransaction(self.dates[7], "ten") self._newItem(u"SSS", 1, t, accFoo) self._newItem(u"TTT", -1, t, accBar)
def test_item(self): db = Database() acc = Account("Root") db += acc t = Transaction(datetime.date.today()) db += t i = Item("Hello World", 2.99) t += i i += acc i = Item("Foo Bar", 1) t += i i += acc
def menuNewRandomDatabase(self): "Create a new and random filled database." LOGGER.info("Creating new random database...") self._clearViews() self._db = Database() self._dbPath = "unnamed" self._db._parsing = True for i in range(32): acc = Account("Account%d" % i) self._db += acc for ii in range(2): cacc = Account("Account%d" % ii) acc += cacc for iii in range(2): ccacc = Account("Account%d" % iii) cacc += ccacc accs = self._db.getChildAccounts(True) dt = datetime.datetime.now() - datetime.timedelta(seconds=60 * 60 * 500) for i in range(3000): t = Transaction(dt, "#" * random.randint(1, 32)) self._db += t v = float(random.randint(1, 1000)) / 100 i = Item("A" * random.randint(1, 32), v) t += i i += random.choice(accs) i = Item("B" * random.randint(1, 32), -v) t += i i += random.choice(accs) dt += datetime.timedelta(seconds=60 * 60) self._db._parsing = False self._accountTree.setDatabase(self._db) self._accountTree.expandAll() self.statusBar().showMessage("Created random database", 2000)
def menuOpenDatabase(self, filepath=""): "Open existing database from file." if not self.checkUnsavedChanges(): return if not filepath or not os.path.isfile(filepath): filepath, dummyFilter = QtWidgets.QFileDialog.getOpenFileName( self, self.tr("Open accounting database..."), "", self.tr("Accounting Database Files (*.accdb)")) if not filepath or not os.path.isfile(filepath): return self._clearViews() LOGGER.info("Loading database %s..." % filepath) self._db = Database.load(filepath) self._dbPath = filepath self._accountTree.setDatabase(self._db) self._accountTree.expandAll() self.statusBar().showMessage("Loaded database " + self._dbPath, 2000) self.handleModelDirty(False) LOGGER.info("Loaded database %s." % filepath)
def test_03_items(self): db = Database() acc1 = Account("A") db += acc1 acc2 = Account("B") db += acc2 t0 = Transaction(datetime.date.today()) db += t0 i0 = Item("AA", 1) t0 += i0 i0 += acc1 i1 = Item("BB", -1) t0 += i1 i1 += acc2 t1 = Transaction(datetime.date.today() + datetime.timedelta(days=1)) db += t1 i2 = Item("CC", -2) t1 += i2 i2 += acc1 i3 = Item("DD", -2) t1 += i3 i3 += acc2 expected_acc_items = [i0, i2] current_acc_items = list(acc1.filterItems()) self.assertListEqual(expected_acc_items, current_acc_items) expected_acc_items = [i1, i3] current_acc_items = list(acc2.filterItems()) self.assertListEqual(expected_acc_items, current_acc_items) t1.setDate(datetime.date.today() - datetime.timedelta(days=1)) expected_acc_items = [i2, i0] current_acc_items = list(acc1.filterItems()) self.assertListEqual(expected_acc_items, current_acc_items) expected_acc_items = [i3, i1] current_acc_items = list(acc2.filterItems()) self.assertListEqual(expected_acc_items, current_acc_items)
class MainWindow(QtWidgets.QMainWindow): def __init__(self, dbPath=None): "Construct main window and load selected database." super().__init__() self._initLogger() self._queuedActions = [] self._splash = None self._splashEnabled = True # disable splash for debugging LOGGER.info("Starting...") self._dbPath = dbPath self._db = None self._recentReportDir = "" self._discardTabChange = False self.initUI() self._handleQueuedAction(True) def _initLogger(self): "Initialize logging" self._logDlg = LogDialog() level = logging.DEBUG rootLogger = logging.getLogger() rootLogger.setLevel(level) self._outLogHdl = logging.StreamHandler(sys.stdout) self._outLogHdl.setLevel(level) rootLogger.addHandler(self._outLogHdl) # f = os.path.expanduser("~/tmp/accounting.log") # self._fileLogHdl = logging.StreamHandler( codecs.getwriter(consoleEncoding)(open(f,"wb+"),"replace") ) # self._fileLogHdl.setLevel( level ) # rootLogger.addHandler( self._fileLogHdl ) self._uiLogHdl = UILoggingHandler(self._logDlg.log) self._uiLogHdl.setLevel(level) rootLogger.addHandler(self._uiLogHdl) self._splashLogHdl = UILoggingHandler(self._splashLog) self._splashLogHdl.setLevel(level) rootLogger.addHandler(self._splashLogHdl) rootLogger.setLevel(level) def restoreSettings(self): "Restore previous application settings" screenDim = QtWidgets.QDesktopWidget().screenGeometry() defaultSize = QtCore.QSize(screenDim.width() * 0.75, screenDim.height() * 0.75) defaultPos = QtCore.QPoint( (screenDim.width() / 2) - (defaultSize.width() / 2), (screenDim.height() / 2) - (defaultSize.height() / 2)) settings = QtCore.QSettings() settings.beginGroup("MainWindow") self.resize(settings.value("size", defaultSize)) self.move(settings.value("pos", defaultPos)) self._recentReportDir = settings.value("mostRecentReportDir", "") loadDbPath = self._dbPath if not loadDbPath: loadDbPath = settings.value("mostRecentDb", "") if loadDbPath: if os.path.isfile(loadDbPath): self._queuedActions += [(self.menuOpenDatabase, (loadDbPath, )) ] else: LOGGER.warning( "Skipped loading non existing database at {}".format( loadDbPath)) self._dbPath = "" try: currTabName = "" for idx in range(settings.beginReadArray("recentTabs")): settings.setArrayIndex(idx) tab = settings.value("tab", "") tabType, tabCurr, tabName = tab.split(":") if tabType == "AccTrn": self._queuedActions += [(self._openAccTrnView, (tabName, )) ] if tabCurr: currTabName = tabCurr self._queuedActions += [(self._openAccTrnView, (currTabName, ))] except: LOGGER.exception("Failed to restore recent tabs") finally: settings.endArray() settings.endGroup() def saveSettings(self): "Save current application settings" settings = QtCore.QSettings() settings.beginGroup("MainWindow") settings.setValue("size", self.size()) settings.setValue("pos", self.pos()) settings.setValue("mostRecentDb", self._dbPath) recentTabIdx = 0 settings.beginWriteArray("recentTabs") for tabIdx in range(self._tabs.count()): widget = self._tabs.widget(tabIdx) currWidget = self._tabs.currentWidget() == widget if isinstance(widget, AccountTransactionView): settings.setArrayIndex(recentTabIdx) recentTabIdx += 1 settings.setValue( "tab", "AccTrn:%s:%s" % ("*" if currWidget else "", widget.getAccount().fullname)) settings.endArray() settings.endGroup() def closeEvent(self, closeEvt): "Handle event to close main window" if self.checkUnsavedChanges(): self.saveSettings() closeEvt.accept() else: closeEvt.ignore() def _splashMsg(self, msg): "Change splash screen message" if not self._splash and self._splashEnabled: self._splash = QtWidgets.QSplashScreen( QtGui.QPixmap(":/images/splash.jpg"), flags=QtCore.Qt.WindowStaysOnTopHint) self._splash.show() elif msg is None and self._splash: LOGGER.removeHandler(self._splashLogHdl) QtCore.QTimer.singleShot(200, lambda: self._splash.finish(self)) if self._splash: if msg: self._splash.showMessage(msg, color=QtGui.QColor("black")) QtCore.QCoreApplication.processEvents() def _splashLog(self, level, msg): "Add given logging message to splash" for l in msg.split("\n"): self._splashMsg(l) def initUI(self): "Initialize UI." centralWidget = QtWidgets.QWidget() self.setCentralWidget(centralWidget) vbox = QtWidgets.QVBoxLayout() vbox.setContentsMargins(0, 0, 0, 0) self._splitter = QtWidgets.QSplitter() self._splitter.setChildrenCollapsible(False) self._tabs = QtWidgets.QTabWidget() self._tabs.setTabsClosable(True) self._tabs.currentChanged.connect(self.handleUpdateAccTab) self._tabs.tabCloseRequested.connect(self.handleCloseAccTab) self._accountTree = AccountTree() self._accountTree.showAccount.connect(self._openAccTrnView) self._accountTree.createReport.connect(self.handleAccTreeReport) self._accountTree.importData.connect(self.handleAccTreeImport) self._accountTree.dirty.connect(self.handleModelDirty) self._accountTree.editAccount.connect(self.handleEditAccount) self._splitter.addWidget(self._accountTree) self._splitter.addWidget(self._tabs) vbox.addWidget(self._splitter) hbox = QtWidgets.QHBoxLayout() hbox.setSpacing(4) hbox.setContentsMargins(2, 2, 2, 2) hbox.addLayout(vbox, 0) centralWidget.setLayout(hbox) self.initMenuBar() self.initStatusBar() self.restoreSettings() if not self._db: self.menuNewDatabase() self.show() self.raise_() def initStatusBar(self): self.statusBar().showMessage("Ready.", 4000) def initMenuBar(self): menuBar = self.menuBar() fileMenu = menuBar.addMenu(self.tr("File")) fileMenu.addAction(self.tr("New database..."), self.menuNewDatabase) fileMenu.addAction(self.tr("New random database..."), self.menuNewRandomDatabase) fileMenu.addSeparator() fileMenu.addAction(self.tr("Open database..."), self.menuOpenDatabase) fileMenu.addSeparator() self._actionSave = fileMenu.addAction(self.tr("Save database..."), self.menuSaveDatabase) self._actionSave.setDisabled(True) self._actionSaveAs = fileMenu.addAction(self.tr("Save database as..."), self.menuSaveAsDatabase) fileMenu.addSeparator() fileMenu.addAction(self.tr("Quit"), self.close) helpMenu = menuBar.addMenu(self.tr("Help")) helpMenu.addAction(self.tr("About..."), self.menuAbout) helpMenu.addAction(self.tr("About Qt..."), self.menuAboutQt) helpMenu.addSeparator() helpMenu.addAction(self.tr("Log..."), self._logDlg.show) def _handleQueuedAction(self, triggerTimer=False): if not self._queuedActions: LOGGER.info("Ready...") self._splashMsg(None) return if triggerTimer: QtCore.QTimer.singleShot(100, self._handleQueuedAction) return func, args = self._queuedActions.pop(0) func(*args) self._handleQueuedAction(True) def checkUnsavedChanges(self): """Returns true when unsaved changes have been saved or user wants to discard unsaved changes. Returns False if there are unsaved changes and user wants to abort current action""" if self.isModelDirty(): res = QtWidgets.QMessageBox.question( self, self.tr("Unsaved changes"), self. tr("There are unsaved changes in current database. Do you want to save them now ?" ), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel) if res == QtWidgets.QMessageBox.Yes: self.menuSaveDatabase() elif res == QtWidgets.QMessageBox.Cancel or res == QtWidgets.QMessageBox.Escape: return False return True def _clearViews(self): while self._tabs.count(): self._discardTabChange = True self._tabs.removeTab(0) self._accountTree.setDatabase(self._db) def menuAbout(self): "Show about dialog." QtWidgets.QMessageBox.about( self, self.tr("About Accounting"), self. tr("A tool to manage transactions of accounts and generate reports of transactions.\n\nVersion " + __version__ + "\nWritten by Manuel Koch.")) def menuAboutQt(self): "Show about dialog." QtWidgets.QMessageBox.aboutQt(self, self.tr("About Qt")) def menuNewDatabase(self): "Create a new and empty database." if not self.checkUnsavedChanges(): return LOGGER.info("Creating new database...") self._clearViews() self._db = Database() self._dbPath = "unnamed" self._accountTree.setDatabase(self._db) self.statusBar().showMessage("Created empty database", 2000) def menuNewRandomDatabase(self): "Create a new and random filled database." LOGGER.info("Creating new random database...") self._clearViews() self._db = Database() self._dbPath = "unnamed" self._db._parsing = True for i in range(32): acc = Account("Account%d" % i) self._db += acc for ii in range(2): cacc = Account("Account%d" % ii) acc += cacc for iii in range(2): ccacc = Account("Account%d" % iii) cacc += ccacc accs = self._db.getChildAccounts(True) dt = datetime.datetime.now() - datetime.timedelta(seconds=60 * 60 * 500) for i in range(3000): t = Transaction(dt, "#" * random.randint(1, 32)) self._db += t v = float(random.randint(1, 1000)) / 100 i = Item("A" * random.randint(1, 32), v) t += i i += random.choice(accs) i = Item("B" * random.randint(1, 32), -v) t += i i += random.choice(accs) dt += datetime.timedelta(seconds=60 * 60) self._db._parsing = False self._accountTree.setDatabase(self._db) self._accountTree.expandAll() self.statusBar().showMessage("Created random database", 2000) def menuOpenDatabase(self, filepath=""): "Open existing database from file." if not self.checkUnsavedChanges(): return if not filepath or not os.path.isfile(filepath): filepath, dummyFilter = QtWidgets.QFileDialog.getOpenFileName( self, self.tr("Open accounting database..."), "", self.tr("Accounting Database Files (*.accdb)")) if not filepath or not os.path.isfile(filepath): return self._clearViews() LOGGER.info("Loading database %s..." % filepath) self._db = Database.load(filepath) self._dbPath = filepath self._accountTree.setDatabase(self._db) self._accountTree.expandAll() self.statusBar().showMessage("Loaded database " + self._dbPath, 2000) self.handleModelDirty(False) LOGGER.info("Loaded database %s." % filepath) def _resetDirty(self): "Tell all models to be clean." self._accountTree.setDirty(False) for i in range(self._tabs.count()): widget = self._tabs.widget(i) if isinstance(widget, AccountTransactionView): widget.setDirty(False) def menuSaveDatabase(self): "Save current database to file." LOGGER.info("Saving database %s..." % self._dbPath) self._db.save(self._dbPath) self._resetDirty() self.statusBar().showMessage("Saved database " + self._dbPath, 2000) LOGGER.info("Saved database %s." % self._dbPath) def menuSaveAsDatabase(self): "Save current database to file." fileName, dummyFilter = QtWidgets.QFileDialog.getSaveFileName( self, self.tr("Save accounting database..."), "", self.tr("Accounting Database Files (*.accdb)")) if not fileName: return LOGGER.info("Saving database as %s..." % fileName) self._db.save(fileName) self._dbPath = fileName self._resetDirty() self.statusBar().showMessage("Saved database as " + self._dbPath, 2000) LOGGER.info("Saved database as %s..." % self._dbPath) def isModelDirty(self): "Return whether our model is dirty and needs to be saved." if self._accountTree.isDirty(): return True for i in range(self._tabs.count()): widget = self._tabs.widget(i) if isinstance(widget, AccountTransactionView): if widget.isDirty(): return True return False def handleModelDirty(self, isDirty): "Handle altered model." if not isDirty or os.path.isfile(self._dbPath): self._actionSave.setEnabled(isDirty) if isDirty: self.setWindowTitle("Accounting - " + self._dbPath + " (modified)") else: self.setWindowTitle("Accounting - " + self._dbPath) def handleEditAccount(self, acc): "Handle editing account" dlg = EditAccountDialog(acc, self) if dlg.exec_() == QDialog.Accepted: pass def handleAccTreeRename(self, acc): "Handle renamed account to update tab header" for i in range(self._tabs.count()): widget = self._tabs.widget(i) if isinstance(widget, AccountTransactionView): if widget.getAccount() == acc: self._tabs.setTabText(i, acc.fullname) return def _openAccTrnView(self, acc): "Open or activate tab with transaction table view for given Account instance and return view instance." if isinstance(acc, str): if not acc in self._db: return acc = self._db[acc] LOGGER.info("Opening account transaction view %s" % acc.fullname) for i in range(self._tabs.count()): widget = self._tabs.widget(i) if isinstance(widget, AccountTransactionView): if widget.getAccount() == acc: self.statusBar().showMessage( "Selecting account transactions view for %s..." % acc.fullname, 1000) self._tabs.setCurrentIndex(i) return widget self.statusBar().showMessage( "Creating account transactions view for %s..." % acc.fullname, 1000) accView = AccountTransactionView() accView.dirty.connect(self.handleModelDirty) accView.setAccount(acc) self._discardTabChange = True self._tabs.addTab(accView, acc.fullname) self._tabs.setCurrentWidget(accView) acc.nameChanged.connect(lambda n: self.handleAccTreeRename(acc)) return accView def handleAccTreeReport(self): indexes = self._accountTree.selectedIndexes() if not indexes: return self.statusBar().showMessage( "Creating report for %d accounts..." % len(indexes), 1000) accounts = [ self._accountTree.model().data(idx, AccountModel.AccountRole) for idx in indexes ] accReport = AccountReport(accounts) self._tabs.addTab(accReport, "Report") self._tabs.setCurrentWidget(accReport) def handleAccTreeImport(self): indexes = self._accountTree.selectedIndexes() if not indexes: return idx = indexes[0] if not idx.isValid(): return acc = self._accountTree.model().data(idx, AccountModel.AccountRole) accView = self._openAccTrnView(acc) accView.startImport() def handleUpdateAccTab(self, idx): "Handle activation of another tab to force updating it's content." if idx >= 0 and not self._discardTabChange: # force updating all the table cells from model widget = self._tabs.currentWidget() if isinstance(widget, AccountTransactionView): widget.refreshFromModel() self._discardTabChange = False def handleCloseAccTab(self, idx): "Handle request to closing tab." self._tabs.removeTab(idx)
def test_transaction(self): db = Database() acc = Account("Root") db += acc t = Transaction(datetime.date.today()) db += t
def test_01_save(self): db = Database() acc1 = Account("Root") db += acc1 acc2 = Account("Foo") acc1 += acc2 self.assertEqual(2, db.nofAccounts()) account_names = set( [a.fullname for a in db.getChildAccounts(recurse=False)]) self.assertSetEqual({"Root"}, account_names) account_names = set( [a.fullname for a in db.getChildAccounts(recurse=True)]) self.assertSetEqual({"Root", "Root/Foo"}, account_names) db.save(self.temp_path) time.sleep(1) acc3 = Account("Bar") acc1 += acc3 self.assertEqual(3, db.nofAccounts()) account_names = set( [a.fullname for a in db.getChildAccounts(recurse=False)]) self.assertSetEqual({"Root"}, account_names) account_names = set( [a.fullname for a in db.getChildAccounts(recurse=True)]) self.assertSetEqual({"Root", "Root/Foo", "Root/Bar"}, account_names) db.save(self.temp_path) time.sleep(1) acc4 = Account("Hello World") acc3 += acc4 self.assertEqual(4, db.nofAccounts()) account_names = set( [a.fullname for a in db.getChildAccounts(recurse=False)]) self.assertSetEqual({"Root"}, account_names) account_names = set( [a.fullname for a in db.getChildAccounts(recurse=True)]) self.assertSetEqual( {"Root", "Root/Foo", "Root/Bar", "Root/Bar/Hello World"}, account_names) db.save(self.temp_path) db2 = Database.load(self.temp_path) self.assertEqual(4, db2.nofAccounts()) account_names = set( [a.fullname for a in db2.getChildAccounts(recurse=False)]) self.assertSetEqual({"Root"}, account_names) account_names = set( [a.fullname for a in db2.getChildAccounts(recurse=True)]) self.assertSetEqual( {"Root", "Root/Foo", "Root/Bar", "Root/Bar/Hello World"}, account_names)