Skip to content

Commit 1332a42

Browse files
Merge pull request #230 from phenobarbital/bugfix-optional-list-str
Bugfix optional list str
2 parents 3d3d0a9 + a85e935 commit 1332a42

File tree

8 files changed

+365
-37
lines changed

8 files changed

+365
-37
lines changed

datamodel/abstract.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def _initialize_fields(attrs, annotations, strict):
210210
if _is_prim:
211211
_type_category = 'primitive'
212212
elif origin == type:
213-
_type_category = 'typing'
213+
_type_category = 'type'
214214
elif _is_dc:
215215
_type_category = 'dataclass'
216216
elif _is_typing: # noqa

datamodel/converters.pyx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,12 @@ cdef object _parse_typing_type(
586586
"""
587587
cdef tuple type_args = getattr(T, '__args__', ())
588588

589+
# print('FIELD > ', field)
590+
# print('T > ', T)
591+
# print('NAME > ', name)
592+
# print('DATA > ', data)
593+
# print('TYPE > ', type_args)
594+
589595
if field.origin in {dict, Mapping} or name in {'Dict', 'Mapping'}:
590596
if isinstance(data, dict):
591597
if type_args:
@@ -1034,6 +1040,8 @@ cpdef dict process_attributes(object obj, list columns):
10341040
if isinstance(value, int) and _type == int:
10351041
continue # short-circuit
10361042
value = parse_basic(_type, value, _encoder)
1043+
elif field_category == 'type':
1044+
pass
10371045
elif field_category == 'typing':
10381046
value = parse_typing(
10391047
f,

datamodel/validation.pyx

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,20 @@ cpdef bool_t is_optional_type(object annotated_type):
4444
return False
4545

4646
cpdef list _validation(object F, str name, object value, object annotated_type, object val_type, str field_type):
47+
cdef bint _valid = False
48+
4749
if not annotated_type:
4850
annotated_type = F.type
4951
elif isinstance(annotated_type, Field):
5052
annotated_type = annotated_type.type
5153
errors = []
5254

5355
# first: calling (if exists) custom validator:
56+
# print('VALIDATION F ', F)
57+
# print('VALIDATION NAME ', name)
58+
# print('VALIDATION VALUE ', value)
59+
# print('VALIDATION ANNOTATED TYPE ', annotated_type)
60+
5461
fn = F.metadata.get('validator', None)
5562
if fn is not None and callable(fn):
5663
try:
@@ -104,16 +111,21 @@ cpdef list _validation(object F, str name, object value, object annotated_type,
104111
errors.append(
105112
f"Field '{name}': expected an awaitable, but got {type(value)}."
106113
)
114+
elif field_type == 'type':
115+
if not isinstance(value, type):
116+
errors.append(
117+
_create_error(name, value, f'Invalid type for {annotated_type}.{name}, expected a type', val_type, annotated_type)
118+
)
119+
inner_types = get_args(F.args[0])
120+
for allowed in inner_types:
121+
if value is allowed:
122+
break
123+
else:
124+
expected = ', '.join([str(t) for t in F.args])
125+
errors.append(
126+
_create_error(name, value, f'Invalid type for {annotated_type}.{name}, expected a type of {expected}', val_type, annotated_type)
127+
)
107128
elif field_type == 'typing' or hasattr(annotated_type, '__module__') and annotated_type.__module__ == 'typing':
108-
if F.origin is type:
109-
for allowed in F.args:
110-
if isinstance(value, allowed):
111-
break
112-
else:
113-
expected = ', '.join([str(t) for t in F.args])
114-
errors.append(
115-
_create_error(name, value, f'Invalid type for {annotated_type}.{name}, expected a subclass of {expected}', val_type, annotated_type)
116-
)
117129
if F.origin is tuple:
118130
# Check if we are in the homogeneous case: Tuple[T, ...]
119131
if len(F.args) == 2 and F.args[1] is Ellipsis:
@@ -148,15 +160,19 @@ cpdef list _validation(object F, str name, object value, object annotated_type,
148160
# Otherwise check that value is an instance of at least one inner type:
149161
for t in inner_types:
150162
base_type = get_origin(t) or t
151-
if not isinstance(value, base_type):
152-
errors.append(
153-
_create_error(
154-
name,
155-
value,
156-
f"Invalid type for Optional field; expected one of {inner_types}",
157-
val_type, annotated_type
158-
)
163+
if isinstance(value, base_type):
164+
_valid = True
165+
break
166+
if not _valid:
167+
errors.append(
168+
_create_error(
169+
name,
170+
value,
171+
f'Invalid type for {annotated_type}.{name}, expected a type of {inner_types!s}',
172+
val_type,
173+
annotated_type
159174
)
175+
)
160176
return errors
161177
# elif type(annotated_type) is ModelMeta:
162178
elif type(annotated_type).__name__ == "ModelMeta":

datamodel/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
'simple library based on python +3.8 to use Dataclass-syntax'
77
'for interacting with Data'
88
)
9-
__version__ = '0.8.13'
9+
__version__ = '0.8.14'
1010
__copyright__ = 'Copyright (c) 2020-2024 Jesus Lara'
1111
__author__ = 'Jesus Lara'
1212
__author_email__ = '[email protected]'

examples/test_driver.py

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,125 @@
11
from typing import Union, Optional
2-
from dataclasses import InitVar
2+
from dataclasses import asdict, InitVar
33
from pathlib import Path
44
from datamodel import BaseModel, Field
5+
from datamodel.exceptions import ValidationError
6+
7+
8+
def default_properties() -> tuple:
9+
return ('host', 'port', 'user', 'username', 'password')
10+
11+
class BaseDriver(BaseModel):
12+
"""BaseDriver.
13+
14+
Description: Base class for all required datasources.
15+
"""
16+
driver: str = Field(required=True, primary_key=True)
17+
driver_type: str = Field(
18+
required=True,
19+
default='asyncdb',
20+
comment="type of driver, can be asyncdb, qs or REST"
21+
)
22+
name: str = Field(required=False, comment='Datasource name, default to driver.')
23+
description: str = Field(comment='Datasource Description', repr=False)
24+
icon: str = Field(required=False, comment='Icon Path for Datasource.', repr=False)
25+
dsn: str = Field(default=None)
26+
dsn_format: str = Field(required=False, default=None, repr=False)
27+
user: InitVar = Field(default='')
28+
username: str = Field(default='')
29+
password: str = Field(required=False, default=None, repr=False, is_secret=True)
30+
auth: dict = Field(required=False, default_factory=dict)
31+
required_properties: Optional[tuple] = Field(
32+
repr=False,
33+
default=default_properties(),
34+
default_factory=tuple
35+
)
36+
37+
def __post_init__(self, user, **kwargs) -> None: # pylint: disable=W0613,W0221
38+
if not self.name:
39+
self.name = self.driver
40+
if user:
41+
self.username = user
42+
self.auth = {
43+
"username": self.username,
44+
"password": self.password
45+
}
46+
# set DSN (if needed)
47+
if self.dsn_format is not None and self.dsn is None:
48+
self.create_dsn()
49+
super(BaseDriver, self).__post_init__()
50+
51+
def create_dsn(self) -> str:
52+
"""create_dsn.
53+
54+
Description: creates DSN from DSN Format.
55+
Returns:
56+
str: DSN.
57+
"""
58+
params = asdict(self)
59+
try:
60+
self.dsn = self.dsn_format.format(**params)
61+
return self.dsn
62+
except (AttributeError, ValueError):
63+
return None
64+
65+
def get_credentials(self) -> dict:
66+
"""get_credentials.
67+
68+
Description: Returns credentials for Datasource.
69+
Returns:
70+
dict: credentials.
71+
"""
72+
return self.params()
73+
74+
def get_parameters(self) -> dict:
75+
return {}
76+
77+
@classmethod
78+
def properties(cls) -> dict:
79+
"""properties.
80+
81+
Description: Returns fields related to Drivers Supported.
82+
Returns:
83+
dict: all required fields for Supported Drivers.
84+
"""
85+
86+
fields = {}
87+
for field in cls.required_properties:
88+
# because tuple is ordered:
89+
try:
90+
f = cls.column(cls, field)
91+
except KeyError:
92+
continue # Field Missing on Driver:
93+
secret = False
94+
if 'is_secret' in f.metadata:
95+
secret = f.metadata["is_secret"]
96+
title = field
97+
if 'title' in f.metadata:
98+
title = f.metadata['title']
99+
required = False
100+
if 'required' in f.metadata:
101+
required = f.metadata['required']
102+
f = {
103+
"name": field,
104+
"title": title,
105+
"required": required,
106+
"is_secret": secret
107+
}
108+
value = getattr(cls, field)
109+
default = hasattr(f, 'default')
110+
if not value and default:
111+
value = f.default
112+
if value:
113+
f["value"] = value
114+
fields[field] = f
115+
return {
116+
"driver": cls.driver,
117+
"name": cls.name,
118+
"icon": cls.icon,
119+
"dsn_format": cls.dsn_format,
120+
"fields": fields
121+
}
122+
5123

6124
def jdbc_properties() -> tuple:
7125
return ('host', 'port', 'user', 'password', 'database', 'dsn', 'jar', 'classpath')
@@ -20,7 +138,7 @@ class jdbcDriver(BaseModel):
20138
jar: Union[list, str] = Field(Required=True)
21139
classpath: Path = Field(Required=False)
22140
required_properties: Optional[Union[list, tuple]] = Field(
23-
repr=False, default=jdbc_properties()
141+
repr=False, default=jdbc_properties(), default_factory=tuple
24142
)
25143

26144
def __post_init__(self, username, *args, **kwargs):
@@ -53,9 +171,21 @@ def params(self) -> dict:
53171
jar='/Users/jlara/.m2/repository/com/oracle/ojdbc/ojdbc8/',
54172
classpath='/Users/jlara/.m2/repository/com/oracle/ojdbc/ojdbc8/ojdbc8-'
55173
)
174+
print('JDBC > ', jdbc_default)
56175
except ValueError:
57176
jdbc_default = None
58177
except Exception as e:
59178
print('ERROR > ', e, type(e))
60179
print('PAYLOAD > ', e.payload)
61180
jdbc_default = None
181+
182+
try:
183+
base_driver = BaseDriver(
184+
driver='asyncdb',
185+
user='admin',
186+
password='admin',
187+
name='asyncdb_test'
188+
)
189+
print('BASE DRIVER > ', base_driver)
190+
except ValidationError as e:
191+
print(e.payload)

examples/test_type_user.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,14 @@ class Employee(BaseModel):
2323

2424

2525
try:
26-
# This should raise a ValidationError because the type hint
27-
# specifies that user_class must be a type (class), not an instance.
28-
user = BasicUser(username="user", email="email")
29-
employee = Employee(user_class=user)
26+
employee = Employee(user_class=BasicUser)
3027
print(employee)
3128
except ValidationError as e:
3229
print(e.payload)
3330

3431
try:
3532
# This should raise a ValidationError because the type hint
36-
# specifies that user_class must be a type (class), not an instance.
37-
user = User(username="user", email="email")
38-
employee = Employee(user_class=user)
33+
employee = Employee(user_class=User)
3934
print(employee)
4035
except ValidationError as e:
4136
print(e.payload)

0 commit comments

Comments
 (0)