def test_m2m_add(): db = SqliteDb(":memory:") mgr = Harbinger(db, groups=Groups, peeps=Peeps) mgr.groups = Groups(mgr) mgr.peeps = mgr[Peeps] grp1 = mgr.groups.new(id=1, data="g1") peep1 = mgr.peeps.new(id=2, data="p1") mix1 = grp1.peeps.add(peep1.id, role="role") assert mix1.role == "role" assert db.select_one("group_peeps", groupid=1).peepid == 2 with pytest.raises(OmenKeyError): grp1.peeps.add(99, role="role") grp1.peeps.remove(peep1.id) # gone assert db.select_one("group_peeps", groupid=1) is None # ok to remove more than once, silently ignored grp1.peeps.remove(peep1.id) grp1.peeps.remove(peep1.id) assert grp1.peeps.get(peep1.id) is None # add/remove obj grp1.peeps.add(peep1) assert grp1.peeps.get(peep1.id) assert peep1.id in grp1.peeps grp1.peeps.remove(peep1) assert grp1.peeps.get(peep1.id) is None
def __init_subclass__(cls, **_kws): cls.table_types = {} if not cls.model: db = SqliteDb(":memory:") cls.__multi_query(db, cls.schema(cls.version)) cls.model: DbModel = db.model()
def test_multi_pk_issues(): # noinspection PyAbstractClass class Harbinger(Omen): @classmethod def schema(cls, version): return ( "create table basic (id1 integer, id2 integer, primary key (id1, id2));" "create table subs (id1 integer, id2 integer, sub text)") class Basic(ObjBase): _pk = ("id1", "id2") def __init__(self, *, id1=None, id2=None, **kws): self.id1 = id1 self.id2 = id2 self.subs = SubsRel(self, where={ "id1": id1, "id2": id2 }, cascade=True) super().__init__(**kws) class Sub(ObjBase): _pk = ("id1", "id2") def __init__(self, *, id1=None, id2=None, sub, **kws): self.id1 = id1 self.id2 = id2 self.sub = sub super().__init__(**kws) db = SqliteDb(":memory:") class Basics(Table): table_name = "basic" row_type = Basic class Subs(Table): table_name = "subs" row_type = Sub class SubsRel(Relation[Sub]): table_type = Subs mgr = Harbinger(db) mgr.set_table(Basics(mgr)) mgr.set_table(Subs(mgr)) basics = mgr[Basics] with pytest.raises(OmenNoPkError): basics.new(id1=4) b = basics.new(id1=4, id2=5) b.subs.add(Sub(sub="what")) assert db.select_one("subs", id1=4, id2=5).sub == "what"
def test_sync_on_getattr(): db = SqliteDb(":memory:") mgr = MyOmen(db) mgr.cars = Cars(mgr) car = Car(id=44, gas_level=0, color="green") mgr.cars.add(car) car._sync_on_getattr = True assert car.color == "green" db.update("cars", id=car.id, color="blue") assert car.color == "blue"
def test_nested_with(): db = SqliteDb(":memory:") mgr = MyOmen(db, cars=Cars) mgr.cars = mgr[Cars] car = mgr.cars.add(Car(id=4, gas_level=2)) with car: car.gas_level = 3 with pytest.raises(OmenLockingError): with car: car.color = "blx" assert db.select_one("cars", id=4).gas_level == 3
def test_underlying_delete(): db = SqliteDb(":memory:") mgr = MyOmen(db) mgr.cars = Cars(mgr) car = Car(id=44, gas_level=0, color="green") mgr.cars.add(car) assert db.select_one("cars", id=car.id) db.delete("cars", id=car.id) car2 = Car(id=44, gas_level=0, color="green") mgr.cars.add(car2) assert car2 is not car assert mgr.cars.get(id=44) is car2
def test_m2m_multi_inherit(): db = SqliteDb(":memory:") mgr = Harbinger(db, groups=Groups, peeps=Peeps) mgr.groups = Groups(mgr) mgr.peeps = mgr[Peeps] grp1 = mgr.groups.new(id=1, data="g1") grp2 = mgr.groups.new(id=2, data="g2") peep1 = mgr.peeps.new(id=2, data="p1") peep2 = mgr.peeps.new(id=3, data="p2") res1 = grp1.peeps.add(peep1, role="role") res2 = grp2.peeps.add(peep2, role="role2") assert len(grp1.peeps) == 1 assert len(grp2.peeps) == 1 assert res1.role == "role" assert res1.data == "p1" assert res2.role == "role2" assert res2.data == "p2" peep1 = grp1.peeps.get(role="role") peep_by_id = grp1.peeps.get(id=2) assert peep_by_id == peep1 assert not grp2.peeps.get(role="role") assert grp2.peeps.get(role="role2") for gr in peep1.groups: assert gr.role == "role" with peep1.groups(1) as gr: gr.role = "new_role" gr.data = "new_data" assert mgr.db.select_one("groups", id=1).data == "new_data" assert mgr.db.select_one("group_peeps", groupid=1).role == "new_role" grp1.peeps.remove(peep1) assert mgr.db.select_one("groups", id=1).data == "new_data" assert not mgr.db.select_one("group_peeps", groupid=1) # you can add by id too grp1.peeps.add(peep1.id, role="role3") assert db.select_one("group_peeps", groupid=1).role == "role3" # you can get by id too assert grp1.peeps(peep1.id).role == "role3" assert grp1.peeps(id=peep1.id).role == "role3" assert grp1.peeps(peepid=peep1.id).role == "role3" assert grp1.peeps.get(peep1.id).role == "role3"
def test_unbound_add(): db = SqliteDb(":memory:") mgr = MyOmen(db) mgr.cars = Cars(mgr) car = Car(id=44, gas_level=0, color="green") door = gen_objs.doors_row(type="a") car.doors.add(door) assert door.carid == car.id assert door.carid assert door in car.doors assert car.doors.select_one(type="a") mgr.cars.add(car) assert db.select_one("doors", carid=car.id, type="a")
def test_update_only(): db = SqliteDb(":memory:") mgr = MyOmen(db, cars=Cars) mgr.cars = mgr[Cars] car = mgr.cars.add(Car(id=4, gas_level=2)) with car: car.gas_level = 3 # hack in the color.... car.__dict__["color"] = "blue" assert "gas_level" in car._changes assert "color" not in car._changes # color doesn't change in db, because we only update "normally-changed" attributes assert db.select_one("cars", id=4).gas_level == 3 assert db.select_one("cars", id=4).color == "black"
def test_reload_from_disk(): db = SqliteDb(":memory:") mgr = MyOmen(db) mgr.cars = Cars(mgr) car = mgr.cars.add(Car(gas_level=0, color="green")) assert db.select_one("cars", id=1).color == "green" db.update("cars", id=1, color="blue") # doesn't notice db change because we have a weakref-cache assert car.color == "green" car2 = mgr.cars.select_one(id=1) # weakref cache assert car2 is car # db fills to cache on select assert car.color == "blue"
def test_threaded_reads(): db = SqliteDb(":memory:") mgr = MyOmen(db) mgr.cars = Cars(mgr) car = mgr.cars.add(Car(gas_level=0, color=str(0))) pool = ThreadPool(50) # to reproduce the problem with this, you need to catch python while switching contexts # in between setattr calls in a non-atomic "apply" function # this is basically impossible without sticking a time sleep in there # even with 100 attributes and 5000 threads it never failed # so this test case only tests if the atomic apply is totally/deeply broken def update_stuff(_i): time.sleep(0.00001) assert car.color == str(car.gas_level) with car: car.gas_level += 1 time.sleep(0.00001) car.color = str(car.gas_level) assert car.color == str(car.gas_level) time.sleep(0.00001) assert car.color == str(car.gas_level) # lots of threads can update stuff num_t = 10 pool.map(update_stuff, range(num_t)) assert car.gas_level == num_t
def test_other_attrs(): # noinspection PyAbstractClass class Harbinger(Omen): @classmethod def schema(cls, version): return "create table basic (id integer primary key, data text)" db = SqliteDb(":memory:") class Basic(InlineBasic): def __init__(self, *, id, data, **kws): self.custom_thing = 44 super().__init__(id=id, data=data, **kws) # tests bootstrapping class Basics(Table[Basic]): pass mgr = Harbinger(db, basic=Basics) mgr.basic = Basics(mgr) mgr.basic.new(id=1, data="someval") bas = mgr.basic.select_one(id=1) assert bas.custom_thing == 44 with pytest.raises(OmenUseWithError): bas.custom_thing = 3 assert mgr.basic.select_one(custom_thing=44) assert not mgr.basic.select_one(custom_thing=43)
def test_cascade_relations(): db = SqliteDb(":memory:") mgr = MyOmen(db, cars=Cars) mgr.cars = mgr[Cars] car = mgr.cars.add(Car(id=1, gas_level=2, color="green")) assert not car._is_new with car: car.doors.add(gen_objs.doors_row(type="z")) car.doors.add(gen_objs.doors_row(type="x")) assert len(car.doors) == 2 with car: car.id = 3 assert len(car.doors) == 2 assert mgr.cars.select_one(id=3) assert not mgr.cars.select_one(id=1) # cascaded remove assert len(list(mgr.db.select("doors"))) == 2 car._remove() assert len(list(mgr.db.select("doors"))) == 0
def test_disable_allow_auto(): db = SqliteDb(":memory:") with patch.object(Cars, "allow_auto", False): mgr = MyOmen(db) mgr.cars = Cars(mgr) with pytest.raises(OmenNoPkError): mgr.cars.add(Car(gas_level=0, color="green"))
def test_type_checking(): db = SqliteDb(":memory:") mgr = MyOmen(db) Car._type_check = True mgr.cars = Cars(mgr) car = mgr.cars.add(Car(gas_level=0, color="green")) with pytest.raises(TypeError): with car: car.color = None with pytest.raises(TypeError): with car: car.color = 4 with pytest.raises(TypeError): with car: car.gas_level = "hello" with pytest.raises(TypeError): with car: car.color = b"ggh" car._type_check = False # notanorm integrity error because color is a required field with pytest.raises(notanorm.errors.IntegrityError): with car: car.color = None # sqlite allows this, so we do too, since type checking is off with car: car.color = b"ggh"
def test_shortcut_syntax(): db = SqliteDb(":memory:") mgr = MyOmen(db, cars=Cars) car = mgr[Cars].new(gas_level=2) assert car assert db.select_one("cars") assert mgr[Cars].get(car.id) assert not mgr[Cars].get("not a car id") assert car.id in mgr[Cars] assert car in mgr[Cars] # todo: why does __call__ not type hint properly? # noinspection PyTypeChecker assert mgr[Cars](car.id) with pytest.raises(OmenKeyError): # noinspection PyTypeChecker assert mgr[Cars]("not a car id")
def test_m2m_subsorts(): db = SqliteDb(":memory:") mgr = Harbinger(db, groups=Groups, peeps=Peeps, group_peeps=GroupPeeps) mgr.groups = Groups(mgr) mgr.peeps = mgr[Peeps] grp1 = mgr.groups.new(id=1, data="g1") peep1 = mgr.peeps.new(id=2, data="p1") peep2 = mgr.peeps.new(id=3, data="p2") peep3 = mgr.peeps.new(id=4, data="p3") peep4 = mgr.peeps.new(id=5, data="p0") # should sort by role, and then by peep mix4 = grp1.peeps.add(peep4, role="role3") mix2 = grp1.peeps.add(peep1, role="role2") mix3 = grp1.peeps.add(peep2, role="role2") mix1 = grp1.peeps.add(peep3, role="role1") # check that m2m table properties properly override other props assert peep1.groups(1).same_method() == "group_peep" assert mix1.same_method() == "group_peep" # add function sort all = [mix1, mix2, mix3, mix4] srt = sorted(all) expect = [mix1, mix2, mix3, mix4] assert srt == expect # select function sort srt = sorted(grp1.peeps) assert srt == expect assert len(grp1.peeps) == 4 assert grp1.peeps.count() == 4
def test_thread_locked_writer_only(): db = SqliteDb(":memory:") mgr = MyOmen(db) mgr.cars = Cars(mgr) car = mgr.cars.add(Car(gas_level=0, color=str(0))) num_t = 15 num_w = 3 pool = ThreadPool(num_t) # to reproduce the problem with this, you need to catch python while switching contexts # in between setattr calls in a non-atomic "apply" function # this is basically impossible without sticking a time sleep in there # even with 100 attributes and 5000 threads it never failed # so this test case only tests if the atomic apply is totally/deeply broken def update_stuff(i): if i < num_w: with car: car.gas_level += 1 car.color = str(car.gas_level) time.sleep(0.1) else: with pytest.raises(OmenUseWithError): car.gas_level += 1 # lots of threads can update stuff pool.map(update_stuff, range(num_t)) assert car.gas_level == num_w
def test_deadlock(): db = SqliteDb(":memory:") mgr = MyOmen(db, cars=Cars) mgr.cars = mgr[Cars] car = mgr.cars.add(Car(gas_level=2)) def insert(i): try: with car: car.gas_level = i if i == 0: time.sleep(1) return True except OmenLockingError: return False num = 3 pool = ThreadPool(num) ret = pool.map(insert, range(num)) assert sorted(ret) == [False, False, True] pool.terminate() pool.join()
def test_type_custom(): class Harbinger(Omen): @classmethod def schema(cls, version): return "create table basic (id integer primary key, data integer)" class Basic(InlineBasic): _type_check = True other: Any flt: float wack: List[str] opt: Optional[str] un: Union[str, int] def __init__(self, id, data, other, flt, wack=None, opt=None, un: Union[str, int] = 0): self.other = other self.flt = flt self.wack = wack or [] self.opt = opt self.un = un super().__init__(id=id, data=data) class Basics(Table): table_name = "basic" row_type = Basic db = SqliteDb(":memory:") mgr = Harbinger(db) mgr.basics = Basics(mgr) Basic(4, 5, 6, 7) # list of integers is allowed, because we don't check complex types, this is mostly for sql checking! Basic(4, 5, 6, 7, [9]) Basic(4, 5, 6, 7.1, [9]) with pytest.raises(TypeError): Basic(4, 5, 6, "not") with pytest.raises(TypeError): Basic(4.1, 5, 6, 7) with pytest.raises(TypeError): Basic(4, 5, 6, 7, opt=5) Basic(4, 5, 6, 7, opt=None) with pytest.raises(TypeError): # noinspection PyTypeChecker Basic(4, 5, 6, 7, un=None) Basic(4, 5, 6, 7, un=4) Basic(4, 5, 6, 7, un="whatever")
def test_need_with(): db = SqliteDb(":memory:") mgr = MyOmen(db, cars=Cars) mgr.cars = mgr[Cars] car = mgr.cars.add(Car(gas_level=2)) with pytest.raises(OmenUseWithError): car.doors = "green" assert car._manager is mgr assert mgr.cars.manager is mgr
def test_readme(tmp_path): fname = str(tmp_path / "test.txt") db = SqliteDb(fname) mgr = MyOmen(db) # cars pk is autoincrement assert Cars.allow_auto mgr.cars = Cars(mgr) # by default, you can always iterate on tables assert mgr.cars.count() == 0 car = Car() # creates a default car (black, full tank) car.color = "red" car.gas_level = 0.5 car.doors.add(gen_objs.doors_row(type="a")) car.doors.add(gen_objs.doors_row(type="b")) car.doors.add(gen_objs.doors_row(type="c")) car.doors.add(gen_objs.doors_row(type="d")) assert not car.id mgr.cars.add(car) # cars have ids, generated by the db assert car.id with pytest.raises(AttributeError, match=r".*gas.*"): mgr.cars.add(Car(color="red", gas=0.3)) mgr.cars.add( Car( color="red", gas_level=0.3, doors=[gen_objs.doors_row(type=str(i)) for i in range(4)], )) assert sum(1 for _ in mgr.cars.select(color="red")) == 2 # 2 log.info("cars: %s", list(mgr.cars.select(color="red"))) car = mgr.cars.select_one(color="red", gas_level=0.3) assert not car._is_new with car: with pytest.raises(AttributeError, match=r".*gas.*"): car.gas = 0.9 car.gas_level = 0.9 types = set() for door in car.doors: types.add(int(door.type)) assert types == set(range(4)) # doors are inserted assert len(car.doors) == 4
def test_cache_sharing_threaded(): db = SqliteDb(":memory:") mgr = MyOmen(db) mgr.cars = Cars(mgr) cache = ObjCache(mgr.cars) db.insert("cars", id=12, gas_level=0, color="green") assert mgr.cars.select_one(id=12) assert cache.select_one(id=12) # all threads update gas_level of cached car, only the first thread reloads the cache def update_stuff(_i): if _i == 0: cache.reload() # if the cache was cleared in in another thread, this returns None (behavior we want to avoid) c = cache.select_one(id=12) assert c with c: c.gas_level += 1 num_t = 10 pool = ThreadPool(num_t) pool.map(update_stuff, range(num_t)) assert cache.select_one(id=12).gas_level == num_t
def test_m2m_id_change(): db = SqliteDb(":memory:") mgr = Harbinger(db, groups=Groups, peeps=Peeps) mgr.groups = Groups(mgr) mgr.peeps = mgr[Peeps] grp1 = mgr.groups.new(id=1, data="g1") peep1 = mgr.peeps.new(id=2, data="p1") grp1.peeps.add(peep1.id, role="role") with grp1: grp1.id = 4 assert grp1.peeps.get(peep1.id) assert mgr.db.select_one("groups").data == "g1" assert mgr.db.select_one("group_peeps", groupid=4, peepid=2) assert mgr.db.select_one("group_peeps") assert mgr.db.select_one("peeps") assert mgr.groups.select_one(id=4).peeps.select_one(id=2)
def test_remove_driver(): db = SqliteDb(":memory:") mgr = MyOmen(db, cars=Cars) mgr.cars = mgr[Cars] mgr.car_drivers = CarDrivers(mgr) Driver = gen_objs.drivers mgr.drivers = mgr[Driver] car1 = mgr.cars.add(Car(id=1, gas_level=2, color="green")) car2 = mgr.cars.add(Car(id=2, gas_level=2, color="green")) driver1 = mgr.drivers.new(name="bob") driver2 = mgr.drivers.new(name="joe") car1.car_drivers.add(CarDriver(carid=car1.id, driverid=driver1.id)) car2.car_drivers.add(CarDriver(carid=car2.id, driverid=driver1.id)) assert len(car1.car_drivers) == 1 assert len(car2.car_drivers) == 1 for cd in car1.car_drivers: car1.car_drivers.remove(cd) assert len(car1.car_drivers) == 0 assert len(car2.car_drivers) == 1 assert len(list(mgr.db.select("drivers"))) == 2 assert len(list(mgr.db.select("car_drivers"))) == 1 car1.car_drivers.add(CarDriver(carid=car1.id, driverid=driver1.id)) assert len(car1.car_drivers) == 1 mgr.car_drivers.remove(carid=car1.id, driverid=driver1.id) # ok to double-remove mgr.car_drivers.remove(carid=car1.id, driverid=driver1.id) # removing None is a no-op (ie: remove.(select_one(criteria....))) mgr.car_drivers.remove(None) assert len(car1.car_drivers) == 0 assert len(car2.car_drivers) == 1 car2.car_drivers.add(CarDriver(carid=car2.id, driverid=driver2.id)) assert len(list(mgr.db.select("car_drivers"))) == 2 with pytest.raises(OmenMoreThanOneError): # kwargs remove is not a generic method for removing all matching things # make your own loop if that's what you want mgr.car_drivers.remove(carid=car2.id)
def test_custom_data_type(): # noinspection PyAbstractClass class Harbinger(Omen): @classmethod def schema(cls, version): return "create table basic (id integer primary key, data text)" db = SqliteDb(":memory:") class Custom(CustomType): # by deriving from CustomType, we track-changes properly def __init__(self, a, b): self.a = a self.b = b def _to_db(self): return self.a + "," + self.b class Basic(InlineBasic): @classmethod def _from_db(cls, dct): dct["data"] = Custom(*dct["data"].split(",")) return Basic(**dct) # tests bootstrapping class Basics(Table[Basic]): pass mgr = Harbinger(db, basic=Basics) mgr.basic = Basics(mgr) mgr.basic.new(id=1, data=Custom("a", "b")) bas = mgr.basic.select_one(id=1) assert bas.data.a == "a" assert bas.data.b == "b" with bas: bas.data = Custom("z", "b") bas = mgr.basic.select_one(id=1) assert bas.data.a == "z" # if you don't derive from CustomType, then this will not work with bas: # partial change to object is tracked as change to main data type bas.data.a = "x" bas = mgr.basic.select_one(id=1) assert bas.data.a == "x"
def test_threaded(): db = SqliteDb(":memory:") mgr = MyOmen(db) mgr.cars = Cars(mgr) car = mgr.cars.add(Car(gas_level=0)) pool = ThreadPool(10) def update_stuff(_i): with car: car.gas_level += 1 # lots of threads can update stuff num_t = 10 pool.map(update_stuff, range(num_t)) assert car.gas_level == num_t # written to db assert mgr.db.select_one("cars", id=car.id).gas_level == num_t
def test_rollback(): db = SqliteDb(":memory:") mgr = MyOmen(db, cars=Cars) mgr.cars = mgr[Cars] car = mgr.cars.add(Car(gas_level=2)) with suppress(ValueError): with car: car.gas_level = 3 raise ValueError assert car.gas_level == 2 with car: car.gas_level = 3 raise OmenRollbackError assert car.gas_level == 2
def test_race_sync(tmp_path): fname = str(tmp_path / "test.txt") db = SqliteDb(fname) mgr = MyOmen(db, cars=Cars) mgr.cars = mgr[Cars] ids = [] def insert(i): h = mgr.cars.row_type(gas_level=i) mgr.cars.add(h) ids.append(h.id) num = 10 pool = ThreadPool(10) pool.map(insert, range(num)) assert mgr.cars.count() == num
def test_other_types(): blob = gen_objs.blobs_row db = SqliteDb(":memory:") mgr = MyOmen(db) mgr.blobs = mgr[gen_objs.blobs] mgr.blobs.add(blob(oid=b"1234", data=b"1234", num=2.4, boo=True)) # cache works blob = mgr.blobs.select_one(oid=b"1234") assert blob.data == b"1234" assert blob.num == 2.4 assert blob.boo with blob: blob.data = b"2345" blob.boo = False blob = mgr.blobs.select_one(oid=b"1234") assert blob.data == b"2345" assert not blob.boo