1
+ from __future__ import annotations
2
+
1
3
import dataclasses
2
4
import enum as builtin_enum
3
5
import json
22
24
from typing import (
23
25
TYPE_CHECKING ,
24
26
Any ,
25
- BinaryIO ,
26
27
Callable ,
28
+ ClassVar ,
27
29
Dict ,
28
30
Generator ,
29
31
Iterable ,
37
39
)
38
40
39
41
from dateutil .parser import isoparse
42
+ from typing_extensions import Self
40
43
41
44
from ._types import T
42
45
from ._version import __version__
47
50
)
48
51
from .enum import Enum as Enum
49
52
from .grpc .grpclib_client import ServiceStub as ServiceStub
53
+ from .utils import (
54
+ classproperty ,
55
+ hybridmethod ,
56
+ )
50
57
51
58
52
59
if TYPE_CHECKING :
@@ -729,6 +736,7 @@ class Message(ABC):
729
736
_serialized_on_wire : bool
730
737
_unknown_fields : bytes
731
738
_group_current : Dict [str , str ]
739
+ _betterproto_meta : ClassVar [ProtoClassMetadata ]
732
740
733
741
def __post_init__ (self ) -> None :
734
742
# Keep track of whether every field was default
@@ -882,18 +890,18 @@ def __copy__(self: T, _: Any = {}) -> T:
882
890
kwargs [name ] = value
883
891
return self .__class__ (** kwargs ) # type: ignore
884
892
885
- @property
886
- def _betterproto (self ) -> ProtoClassMetadata :
893
+ @classproperty
894
+ def _betterproto (cls : type [ Self ] ) -> ProtoClassMetadata : # type: ignore
887
895
"""
888
896
Lazy initialize metadata for each protobuf class.
889
897
It may be initialized multiple times in a multi-threaded environment,
890
898
but that won't affect the correctness.
891
899
"""
892
- meta = getattr ( self . __class__ , "_betterproto_meta" , None )
893
- if not meta :
894
- meta = ProtoClassMetadata ( self . __class__ )
895
- self . __class__ . _betterproto_meta = meta # type: ignore
896
- return meta
900
+ try :
901
+ return cls . _betterproto_meta
902
+ except AttributeError :
903
+ cls . _betterproto_meta = meta = ProtoClassMetadata ( cls )
904
+ return meta
897
905
898
906
def dump (self , stream : "SupportsWrite[bytes]" , delimit : bool = False ) -> None :
899
907
"""
@@ -1512,10 +1520,74 @@ def to_dict(
1512
1520
output [cased_name ] = value
1513
1521
return output
1514
1522
1515
- def from_dict (self : T , value : Mapping [str , Any ]) -> T :
1523
+ @classmethod
1524
+ def _from_dict_init (cls , mapping : Mapping [str , Any ]) -> Mapping [str , Any ]:
1525
+ init_kwargs : Dict [str , Any ] = {}
1526
+ for key , value in mapping .items ():
1527
+ field_name = safe_snake_case (key )
1528
+ try :
1529
+ meta = cls ._betterproto .meta_by_field_name [field_name ]
1530
+ except KeyError :
1531
+ continue
1532
+ if value is None :
1533
+ continue
1534
+
1535
+ if meta .proto_type == TYPE_MESSAGE :
1536
+ sub_cls = cls ._betterproto .cls_by_field [field_name ]
1537
+ if sub_cls == datetime :
1538
+ value = (
1539
+ [isoparse (item ) for item in value ]
1540
+ if isinstance (value , list )
1541
+ else isoparse (value )
1542
+ )
1543
+ elif sub_cls == timedelta :
1544
+ value = (
1545
+ [timedelta (seconds = float (item [:- 1 ])) for item in value ]
1546
+ if isinstance (value , list )
1547
+ else timedelta (seconds = float (value [:- 1 ]))
1548
+ )
1549
+ elif not meta .wraps :
1550
+ value = (
1551
+ [sub_cls .from_dict (item ) for item in value ]
1552
+ if isinstance (value , list )
1553
+ else sub_cls .from_dict (value )
1554
+ )
1555
+ elif meta .map_types and meta .map_types [1 ] == TYPE_MESSAGE :
1556
+ sub_cls = cls ._betterproto .cls_by_field [f"{ field_name } .value" ]
1557
+ value = {k : sub_cls .from_dict (v ) for k , v in value .items ()}
1558
+ else :
1559
+ if meta .proto_type in INT_64_TYPES :
1560
+ value = (
1561
+ [int (n ) for n in value ]
1562
+ if isinstance (value , list )
1563
+ else int (value )
1564
+ )
1565
+ elif meta .proto_type == TYPE_BYTES :
1566
+ value = (
1567
+ [b64decode (n ) for n in value ]
1568
+ if isinstance (value , list )
1569
+ else b64decode (value )
1570
+ )
1571
+ elif meta .proto_type == TYPE_ENUM :
1572
+ enum_cls = cls ._betterproto .cls_by_field [field_name ]
1573
+ if isinstance (value , list ):
1574
+ value = [enum_cls .from_string (e ) for e in value ]
1575
+ elif isinstance (value , str ):
1576
+ value = enum_cls .from_string (value )
1577
+ elif meta .proto_type in (TYPE_FLOAT , TYPE_DOUBLE ):
1578
+ value = (
1579
+ [_parse_float (n ) for n in value ]
1580
+ if isinstance (value , list )
1581
+ else _parse_float (value )
1582
+ )
1583
+
1584
+ init_kwargs [field_name ] = value
1585
+ return init_kwargs
1586
+
1587
+ @hybridmethod
1588
+ def from_dict (cls : type [Self ], value : Mapping [str , Any ]) -> Self : # type: ignore
1516
1589
"""
1517
- Parse the key/value pairs into the current message instance. This returns the
1518
- instance itself and is therefore assignable and chainable.
1590
+ Parse the key/value pairs into the a new message instance.
1519
1591
1520
1592
Parameters
1521
1593
-----------
@@ -1527,72 +1599,29 @@ def from_dict(self: T, value: Mapping[str, Any]) -> T:
1527
1599
:class:`Message`
1528
1600
The initialized message.
1529
1601
"""
1602
+ self = cls (** cls ._from_dict_init (value ))
1530
1603
self ._serialized_on_wire = True
1531
- for key in value :
1532
- field_name = safe_snake_case (key )
1533
- meta = self ._betterproto .meta_by_field_name .get (field_name )
1534
- if not meta :
1535
- continue
1604
+ return self
1536
1605
1537
- if value [key ] is not None :
1538
- if meta .proto_type == TYPE_MESSAGE :
1539
- v = self ._get_field_default (field_name )
1540
- cls = self ._betterproto .cls_by_field [field_name ]
1541
- if isinstance (v , list ):
1542
- if cls == datetime :
1543
- v = [isoparse (item ) for item in value [key ]]
1544
- elif cls == timedelta :
1545
- v = [
1546
- timedelta (seconds = float (item [:- 1 ]))
1547
- for item in value [key ]
1548
- ]
1549
- else :
1550
- v = [cls ().from_dict (item ) for item in value [key ]]
1551
- elif cls == datetime :
1552
- v = isoparse (value [key ])
1553
- setattr (self , field_name , v )
1554
- elif cls == timedelta :
1555
- v = timedelta (seconds = float (value [key ][:- 1 ]))
1556
- setattr (self , field_name , v )
1557
- elif meta .wraps :
1558
- setattr (self , field_name , value [key ])
1559
- elif v is None :
1560
- setattr (self , field_name , cls ().from_dict (value [key ]))
1561
- else :
1562
- # NOTE: `from_dict` mutates the underlying message, so no
1563
- # assignment here is necessary.
1564
- v .from_dict (value [key ])
1565
- elif meta .map_types and meta .map_types [1 ] == TYPE_MESSAGE :
1566
- v = getattr (self , field_name )
1567
- cls = self ._betterproto .cls_by_field [f"{ field_name } .value" ]
1568
- for k in value [key ]:
1569
- v [k ] = cls ().from_dict (value [key ][k ])
1570
- else :
1571
- v = value [key ]
1572
- if meta .proto_type in INT_64_TYPES :
1573
- if isinstance (value [key ], list ):
1574
- v = [int (n ) for n in value [key ]]
1575
- else :
1576
- v = int (value [key ])
1577
- elif meta .proto_type == TYPE_BYTES :
1578
- if isinstance (value [key ], list ):
1579
- v = [b64decode (n ) for n in value [key ]]
1580
- else :
1581
- v = b64decode (value [key ])
1582
- elif meta .proto_type == TYPE_ENUM :
1583
- enum_cls = self ._betterproto .cls_by_field [field_name ]
1584
- if isinstance (v , list ):
1585
- v = [enum_cls .from_string (e ) for e in v ]
1586
- elif isinstance (v , str ):
1587
- v = enum_cls .from_string (v )
1588
- elif meta .proto_type in (TYPE_FLOAT , TYPE_DOUBLE ):
1589
- if isinstance (value [key ], list ):
1590
- v = [_parse_float (n ) for n in value [key ]]
1591
- else :
1592
- v = _parse_float (value [key ])
1606
+ @from_dict .instancemethod
1607
+ def from_dict (self , value : Mapping [str , Any ]) -> Self :
1608
+ """
1609
+ Parse the key/value pairs into the current message instance. This returns the
1610
+ instance itself and is therefore assignable and chainable.
1593
1611
1594
- if v is not None :
1595
- setattr (self , field_name , v )
1612
+ Parameters
1613
+ -----------
1614
+ value: Dict[:class:`str`, Any]
1615
+ The dictionary to parse from.
1616
+
1617
+ Returns
1618
+ --------
1619
+ :class:`Message`
1620
+ The initialized message.
1621
+ """
1622
+ self ._serialized_on_wire = True
1623
+ for field , value in self ._from_dict_init (value ).items ():
1624
+ setattr (self , field , value )
1596
1625
return self
1597
1626
1598
1627
def to_json (
@@ -1809,8 +1838,8 @@ def is_set(self, name: str) -> bool:
1809
1838
1810
1839
@classmethod
1811
1840
def _validate_field_groups (cls , values ):
1812
- group_to_one_ofs = cls ._betterproto_meta .oneof_field_by_group # type: ignore
1813
- field_name_to_meta = cls ._betterproto_meta .meta_by_field_name # type: ignore
1841
+ group_to_one_ofs = cls ._betterproto .oneof_field_by_group
1842
+ field_name_to_meta = cls ._betterproto .meta_by_field_name
1814
1843
1815
1844
for group , field_set in group_to_one_ofs .items ():
1816
1845
if len (field_set ) == 1 :
@@ -1837,6 +1866,9 @@ def _validate_field_groups(cls, values):
1837
1866
return values
1838
1867
1839
1868
1869
+ Message .__annotations__ = {} # HACK to avoid typing.get_type_hints breaking :)
1870
+
1871
+
1840
1872
def serialized_on_wire (message : Message ) -> bool :
1841
1873
"""
1842
1874
If this message was or should be serialized on the wire. This can be used to detect
0 commit comments