Coverage for dibbler / models / Base.py: 83%

16 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-01-25 14:26 +0000

1from sqlalchemy import MetaData 

2from sqlalchemy.orm import ( 

3 DeclarativeBase, 

4 declared_attr, 

5) 

6from sqlalchemy.orm.collections import ( 

7 InstrumentedDict, 

8 InstrumentedList, 

9 InstrumentedSet, 

10) 

11 

12 

13def _pascal_case_to_snake_case(name: str) -> str: 

14 return "".join(["_" + i.lower() if i.isupper() else i for i in name]).lstrip("_") 

15 

16 

17class Base(DeclarativeBase): 

18 metadata = MetaData( 

19 naming_convention={ 

20 "ix": "ix__%(table_name)s__%(column_0_label)s", 

21 "uq": "uq__%(table_name)s__%(column_0_name)s", 

22 "ck": "ck__%(table_name)s__%(constraint_name)s", 

23 "fk": "fk__%(table_name)s__%(column_0_name)s_%(referred_table_name)s", 

24 "pk": "pk__%(table_name)s", 

25 } 

26 ) 

27 

28 @declared_attr.directive 

29 def __tablename__(cls) -> str: 

30 if hasattr(cls, "__table_name__"): 30 ↛ 31line 30 didn't jump to line 31 because the condition on line 30 was never true

31 assert isinstance(cls.__table_name__, str) 

32 return cls.__table_name__ 

33 return _pascal_case_to_snake_case(cls.__name__) 

34 

35 # NOTE: This is the default implementation of __repr__ for all tables, 

36 # but it is preferable to override it for each table to get a nicer 

37 # looking representation. This trades a bit of messiness for a complete 

38 # output of all relevant fields. 

39 def __repr__(self) -> str: 

40 columns = ", ".join( 

41 f"{k}={repr(v)}" 

42 for k, v in self.__dict__.items() 

43 if not any( 

44 [ 

45 k.startswith("_"), 

46 # Ensure that we don't try to print out the entire list of 

47 # relationships, which could create an infinite loop 

48 isinstance(v, Base), 

49 isinstance(v, InstrumentedList), 

50 isinstance(v, InstrumentedSet), 

51 isinstance(v, InstrumentedDict), 

52 ] 

53 ) 

54 ) 

55 return f"<{self.__class__.__name__}({columns})>"