44 >>> class Movie(Persistent):
55 ... title: str
66 ... year: int
7- ... boxmega : float
7+ ... megabucks : float
88
99Implemented behavior::
1010
1111 >>> Movie._connect() # doctest: +ELLIPSIS
1212 <sqlite3.Connection object at 0x...>
13- >>> movie = Movie('The Godfather', 1972, 137)
13+ >>> movie = Movie(title= 'The Godfather', year= 1972, megabucks= 137)
1414 >>> movie.title
1515 'The Godfather'
16- >>> movie.boxmega
16+ >>> movie.megabucks
1717 137.0
1818
19- Instances always have a ``.pk `` attribute, but it is ``None`` until the
19+ Instances always have a ``._pk `` attribute, but it is ``None`` until the
2020object is saved::
2121
22- >>> movie.pk is None
22+ >>> movie._pk is None
2323 True
24- >>> movie._persist()
25- >>> movie.pk
24+ >>> movie._save()
25+ 1
26+ >>> movie._pk
2627 1
2728
2829Delete the in-memory ``movie``, and fetch the record from the database,
3132 >>> del movie
3233 >>> film = Movie[1]
3334 >>> film
34- Movie('The Godfather', 1972, 137.0, pk =1)
35+ Movie(title= 'The Godfather', year= 1972, megabucks= 137.0, _pk =1)
3536
3637By default, the table name is the class name lowercased, with an appended
3738"s" for plural::
@@ -51,69 +52,89 @@ class declaration::
5152
5253"""
5354
54- from typing import get_type_hints
55+ from typing import Any , ClassVar , get_type_hints
5556
5657import dblib as db
5758
5859
5960class Field :
60- def __init__ (self , name , py_type ) :
61+ def __init__ (self , name : str , py_type : type ) -> None :
6162 self .name = name
6263 self .type = py_type
6364
64- def __set__ (self , instance , value ) :
65+ def __set__ (self , instance : 'Persistent' , value : Any ) -> None :
6566 try :
6667 value = self .type (value )
67- except TypeError as e :
68- msg = f'{ value !r} is not compatible with { self .name } :{ self .type } .'
68+ except (TypeError , ValueError ) as e :
69+ type_name = self .type .__name__
70+ msg = f'{ value !r} is not compatible with { self .name } :{ type_name } .'
6971 raise TypeError (msg ) from e
7072 instance .__dict__ [self .name ] = value
7173
7274
7375class Persistent :
74- def __init_subclass__ (
75- cls , * , db_path = db .DEFAULT_DB_PATH , table = '' , ** kwargs
76- ):
77- super ().__init_subclass__ (** kwargs )
76+ _TABLE_NAME : ClassVar [str ]
77+ _TABLE_READY : ClassVar [bool ] = False
78+
79+ @classmethod
80+ def _fields (cls ) -> dict [str , type ]:
81+ return {
82+ name : py_type
83+ for name , py_type in get_type_hints (cls ).items ()
84+ if not name .startswith ('_' )
85+ }
86+
87+ def __init_subclass__ (cls , * , table : str = '' , ** kwargs : dict ):
88+ super ().__init_subclass__ (** kwargs ) # type:ignore
7889 cls ._TABLE_NAME = table if table else cls .__name__ .lower () + 's'
79- cls ._TABLE_READY = False
80- for name , py_type in get_type_hints (cls ).items ():
90+ for name , py_type in cls ._fields ().items ():
8191 setattr (cls , name , Field (name , py_type ))
8292
8393 @staticmethod
84- def _connect (db_path = db .DEFAULT_DB_PATH ):
94+ def _connect (db_path : str = db .DEFAULT_DB_PATH ):
8595 return db .connect (db_path )
8696
8797 @classmethod
88- def _ensure_table (cls ):
98+ def _ensure_table (cls ) -> str :
8999 if not cls ._TABLE_READY :
90- db .ensure_table (cls ._TABLE_NAME , get_type_hints ( cls ))
100+ db .ensure_table (cls ._TABLE_NAME , cls . _fields ( ))
91101 cls ._TABLE_READY = True
92102 return cls ._TABLE_NAME
93103
94- def _fields (self ):
104+ def __class_getitem__ (cls , pk : int ) -> 'Persistent' :
105+ field_names = ['_pk' ] + list (cls ._fields ())
106+ values = db .fetch_record (cls ._TABLE_NAME , pk )
107+ return cls (** dict (zip (field_names , values )))
108+
109+ def _asdict (self ) -> dict [str , Any ]:
95110 return {
96111 name : getattr (self , name )
97112 for name , attr in self .__class__ .__dict__ .items ()
98113 if isinstance (attr , Field )
99114 }
100115
101- def __init__ (self , * args , pk = None ):
102- for name , arg in zip (self ._fields (), args ):
116+ def __init__ (self , * , _pk = None , ** kwargs ):
117+ field_names = self ._asdict ().keys ()
118+ for name , arg in kwargs .items ():
119+ if name not in field_names :
120+ msg = f'{ self .__class__ .__name__ !r} has no attribute { name !r} '
121+ raise AttributeError (msg )
103122 setattr (self , name , arg )
104- self .pk = pk
105-
106- def __class_getitem__ (cls , pk ):
107- return cls (* db .fetch_record (cls ._TABLE_NAME , pk )[1 :], pk = pk )
108-
109- def __repr__ (self ):
110- args = ', ' .join (repr (value ) for value in self ._fields ().values ())
111- pk = '' if self .pk is None else f', pk={ self .pk } '
112- return f'{ self .__class__ .__name__ } ({ args } { pk } )'
113-
114- def _persist (self ):
123+ self ._pk = _pk
124+
125+ def __repr__ (self ) -> str :
126+ kwargs = ', ' .join (
127+ f'{ key } ={ value !r} ' for key , value in self ._asdict ().items ()
128+ )
129+ cls_name = self .__class__ .__name__
130+ if self ._pk is None :
131+ return f'{ cls_name } ({ kwargs } )'
132+ return f'{ cls_name } ({ kwargs } , _pk={ self ._pk } )'
133+
134+ def _save (self ) -> int :
115135 table = self .__class__ ._ensure_table ()
116- if self .pk is None :
117- self .pk = db .insert_record (table , self ._fields ())
136+ if self ._pk is None :
137+ self ._pk = db .insert_record (table , self ._asdict ())
118138 else :
119- db .update_record (table , self .pk , self ._fields ())
139+ db .update_record (table , self ._pk , self ._asdict ())
140+ return self ._pk
0 commit comments