Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
What's new
7
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Open sidebar
Rolf van Kleef
serializer_utils
Commits
e2b97bcf
Verified
Commit
e2b97bcf
authored
Nov 08, 2018
by
Rolf van Kleef
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Documentation and style!
parent
00e99d9c
Pipeline
#1261
canceled with stages
Changes
2
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
100 additions
and
21 deletions
+100
-21
dict_deserializer/annotations.py
dict_deserializer/annotations.py
+3
-2
dict_deserializer/deserializer.py
dict_deserializer/deserializer.py
+97
-19
No files found.
dict_deserializer/annotations.py
View file @
e2b97bcf
...
...
@@ -15,7 +15,8 @@ class KeyValueDiscriminator:
def
__repr__
(
self
):
if
self
.
has_value
:
return
'KeyValueDiscriminator(key={}, value={})'
.
format
(
self
.
key
,
self
.
value
)
return
'KeyValueDiscriminator(key={}, value={})'
\
.
format
(
self
.
key
,
self
.
value
)
return
'KeyValueDiscriminator(key={})'
.
format
(
self
.
key
)
def
check
(
self
,
d
:
dict
):
...
...
@@ -56,4 +57,4 @@ def discriminate(key=None, value=sentinel, matcher=None):
def
abstract
(
cls
):
cls
.
_abstract
=
True
return
cls
\ No newline at end of file
return
cls
dict_deserializer/deserializer.py
View file @
e2b97bcf
from
typing
import
Optional
,
Union
,
List
from
typing
import
Optional
,
Union
,
List
,
Tuple
,
Dict
from
typeguard
import
check_type
class
Rule
:
"""
This class is primarily used as a container to store type information.
"""
@
staticmethod
def
to_rule
(
tpe
):
def
to_rule
(
tpe
)
->
'Rule'
:
"""
Ensures type is a rule. Otherwise, it will be converted into a rule.
:param tpe: The type/rule.
:return: a Rule
"""
if
isinstance
(
tpe
,
Rule
):
return
tpe
return
Rule
(
tpe
)
def
__init__
(
self
,
type
,
default
=
None
):
# noinspection PyShadowingBuiltins
def
__init__
(
self
:
'Rule'
,
type
,
default
=
None
):
self
.
type
=
type
self
.
default
=
default
def
__repr__
(
self
):
def
__repr__
(
self
:
'Rule'
):
return
"Rule(type={}, default={})"
.
format
(
self
.
type
,
self
.
default
)
def
validate
(
self
,
key
,
value
):
def
validate
(
self
:
'Rule'
,
key
:
str
,
value
):
"""
Returns the original value, a default value, or throws.
:param key: The key of this field.
:param value: Which value to validate.
:return: value, default.
"""
check_type
(
key
,
value
,
self
.
type
)
if
value
is
None
:
value
=
self
.
default
...
...
@@ -26,7 +43,13 @@ class Rule:
class
BaseMeta
(
type
):
def
__new__
(
mcs
,
name
,
bases
,
namespace
):
"""
Metaclass for all Deserializable
"""
def
__new__
(
mcs
:
'BaseMeta'
,
name
:
str
,
bases
:
Tuple
[
type
],
namespace
:
dict
)
\
->
type
:
namespace
[
'_discriminators'
]
=
[]
namespace
[
'_abstract'
]
=
False
...
...
@@ -44,7 +67,10 @@ class BaseMeta(type):
return
cls
def
rbase
(
cls
,
ls
=
None
):
def
rbase
(
cls
:
type
,
ls
:
List
[
type
]
=
None
)
->
List
[
type
]:
"""
Get all base classes for cls.
"""
if
ls
is
None
:
ls
=
[]
...
...
@@ -56,14 +82,31 @@ def rbase(cls, ls=None):
return
ls
def
_is_valid
(
key
:
str
,
value
):
return
not
key
.
startswith
(
'_'
)
and
not
callable
(
value
)
and
not
isinstance
(
value
,
classmethod
)
and
\
not
isinstance
(
value
,
staticmethod
)
and
not
isinstance
(
value
,
property
)
def
_is_valid
(
key
:
str
,
value
)
->
bool
:
"""
Value is not a method and key does not start with an underscore.
:param key: The name of the field
:param value: The value of the field
:return: Boolean.
"""
return
not
key
.
startswith
(
'_'
)
and
\
not
callable
(
value
)
and
\
not
isinstance
(
value
,
classmethod
)
and
\
not
isinstance
(
value
,
staticmethod
)
and
\
not
isinstance
(
value
,
property
)
class
Deserializable
(
metaclass
=
BaseMeta
):
"""
Base class for all automagically deserializing classes.
"""
@
classmethod
def
get_attrs
(
cls
):
def
get_attrs
(
cls
)
->
Dict
[
str
,
Rule
]:
"""
Returns a list of all type rules for the given class.
:return: a dict from property to type rule.
"""
fields
=
{}
defaults
=
{}
rl
=
list
(
reversed
(
rbase
(
cls
)))
...
...
@@ -72,7 +115,8 @@ class Deserializable(metaclass=BaseMeta):
for
k
in
c
.
__dict__
:
if
_is_valid
(
k
,
c
.
__dict__
[
k
]):
defaults
[
k
]
=
c
.
__dict__
[
k
]
fields
[
k
]
=
Rule
(
Optional
[
type
(
defaults
[
k
])],
default
=
defaults
[
k
])
fields
[
k
]
=
Rule
(
Optional
[
type
(
defaults
[
k
])],
default
=
defaults
[
k
])
for
k
in
cls
.
__annotations__
:
if
k
in
defaults
and
not
_is_valid
(
k
,
defaults
[
k
]):
...
...
@@ -87,9 +131,18 @@ class Deserializable(metaclass=BaseMeta):
def
get_deserialization_classes
(
t
,
d
,
try_all
=
True
)
->
List
[
type
]:
"""
Find all candidates that are a (sub)type of t, matching d.
:param t: The type to match from.
:param d: The dict to match onto.
:param try_all: Whether to support automatic discrimination.
:return:
"""
candidates
=
[]
for
sc
in
t
.
__subclasses__
():
if
hasattr
(
sc
,
'_discriminators'
):
# noinspection PyProtectedMember
for
discriminator
in
sc
.
_discriminators
:
if
not
discriminator
.
check
(
d
):
# Invalid
...
...
@@ -97,7 +150,8 @@ def get_deserialization_classes(t, d, try_all=True) -> List[type]:
else
:
# All were valid
try
:
candidates
.
extend
(
get_deserialization_classes
(
sc
,
t
,
try_all
))
candidates
.
extend
(
get_deserialization_classes
(
sc
,
t
,
try_all
))
except
TypeError
as
e
:
if
not
try_all
:
raise
e
...
...
@@ -107,7 +161,18 @@ def get_deserialization_classes(t, d, try_all=True) -> List[type]:
return
candidates
def
deserialize
(
rule
:
Rule
,
data
,
try_all
=
True
,
key
=
'$'
):
def
deserialize
(
rule
:
Rule
,
data
,
try_all
:
bool
=
True
,
key
:
str
=
'$'
):
"""
Converts the passed in data into a type that is compatible with rule.
:param rule:
:param data:
:param try_all: Whether to attempt other subtypes when a TypeError has
occurred. This is useful when automatically deriving discriminators.
:param key: Used for exceptions and error reporting. Preferrably the full
path to the current value.
:return: An instance matching Rule.
"""
# In case of primitive types, attempt to assign.
try
:
return
rule
.
validate
(
key
,
data
)
...
...
@@ -123,7 +188,8 @@ def deserialize(rule: Rule, data, try_all=True, key='$'):
return
v
except
TypeError
:
pass
raise
TypeError
(
'{} did not match any of {} for key {}.'
.
format
(
type
(
data
),
rule
.
type
.
__args__
,
key
))
raise
TypeError
(
'{} did not match any of {} for key {}.'
.
format
(
type
(
data
),
rule
.
type
.
__args__
,
key
))
if
type
(
rule
.
type
)
is
type
(
List
):
if
len
(
rule
.
type
.
__args__
)
!=
1
:
...
...
@@ -135,7 +201,12 @@ def deserialize(rule: Rule, data, try_all=True, key='$'):
t
=
rule
.
type
.
__args__
[
0
]
result
=
[]
for
i
,
v
in
enumerate
(
data
):
result
.
append
(
deserialize
(
Rule
(
t
),
v
,
try_all
,
'{}.{}'
.
format
(
key
,
i
)))
result
.
append
(
deserialize
(
Rule
(
t
),
v
,
try_all
,
'{}.{}'
.
format
(
key
,
i
)
))
return
result
if
issubclass
(
rule
.
type
,
Deserializable
):
...
...
@@ -148,7 +219,12 @@ def deserialize(rule: Rule, data, try_all=True, key='$'):
try
:
instance
=
cls
()
for
k
,
r
in
cls
.
get_attrs
().
items
():
v
=
deserialize
(
r
,
data
[
k
]
if
k
in
data
else
r
.
default
,
try_all
,
key
=
'{}.{}'
.
format
(
key
,
k
))
v
=
deserialize
(
r
,
data
[
k
]
if
k
in
data
else
r
.
default
,
try_all
,
key
=
'{}.{}'
.
format
(
key
,
k
)
)
setattr
(
instance
,
k
,
v
)
return
instance
...
...
@@ -156,6 +232,8 @@ def deserialize(rule: Rule, data, try_all=True, key='$'):
if
not
try_all
:
raise
e
raise
TypeError
(
'Unable to find matching non-abstract (sub)type of {} with key {}.'
.
format
(
rule
.
type
,
key
))
raise
TypeError
(
'Unable to find matching non-abstract (sub)type of '
'{} with key {}.'
.
format
(
rule
.
type
,
key
))
raise
TypeError
(
'Unable to find a deserialization candidate for {} with key {}.'
.
format
(
rule
,
key
))
\ No newline at end of file
raise
TypeError
(
'Unable to find a deserialization candidate for '
'{} with key {}.'
.
format
(
rule
,
key
))
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment