Skip to content

Commit abff3c8

Browse files
committed
migrated to python 3.10, pydantic 2
1 parent a030f16 commit abff3c8

File tree

5 files changed

+81
-63
lines changed

5 files changed

+81
-63
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ testcaches = .hypothesis .pytest_cache .pytype coverage.xml htmlcov .coverage
2323

2424
all: version test build
2525

26-
develop: devversion package
26+
develop: devversion package test
2727
python3 setup.py develop --uninstall
2828
python3 setup.py develop
2929

clams/appmetadata/__init__.py

Lines changed: 77 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ def get_clams_pyver():
3131
import clams
3232
return clams.__version__
3333
except ImportError:
34-
version_fname = os.path.join(os.path.dirname(__file__), '..', '..', 'VERSION')
35-
if os.path.exists(version_fname):
34+
version_fname = Path(__file__).joinpath('../../VERSION')
35+
if version_fname.exists():
3636
with open(version_fname) as version_f:
3737
return version_f.read().strip()
3838
else:
@@ -59,13 +59,21 @@ def get_mmif_specver():
5959
return mmif.__specver__
6060

6161

62+
def pop_titles(js):
63+
for prop in js.get('properties', {}).values():
64+
prop.pop('title', None)
65+
66+
67+
def jsonschema_versioning(js):
68+
js['$schema'] = pydantic.json_schema.GenerateJsonSchema.schema_dialect
69+
js['$comment'] = f"clams-python SDK {get_clams_pyver()} was used to generate this schema"
70+
71+
6272
class _BaseModel(pydantic.BaseModel):
6373

64-
class Config:
65-
@staticmethod
66-
def json_schema_extra(schema, model) -> None:
67-
for prop in schema.get('properties', {}).values():
68-
prop.pop('title', None)
74+
model_config = {
75+
"json_schema_extra": pop_titles
76+
}
6977

7078

7179
class Output(_BaseModel):
@@ -97,17 +105,27 @@ class Output(_BaseModel):
97105
{},
98106
description="(optional) Specification for type properties, if any. ``\"*\"`` indicates any value."
99107
)
108+
109+
def __init__(self, **kwargs):
110+
super().__init__(**kwargs)
111+
112+
@pydantic.field_validator('at_type', mode='after') # because pydantic v2 doesn't auto-convert url to string
113+
@classmethod
114+
def stringify(cls, val):
115+
return str(val)
100116

101-
@pydantic.validator('at_type', pre=True)
117+
@pydantic.field_validator('at_type', mode='before')
118+
@classmethod
102119
def at_type_must_be_str(cls, v):
103120
if not isinstance(v, str):
104121
return str(v)
105122
return v
106123

107-
class Config:
108-
title = 'CLAMS Output Specification'
109-
extra = 'forbid'
110-
allow_population_by_field_name = True
124+
model_config = {
125+
'title': 'CLAMS Output Specification',
126+
'extra': 'forbid',
127+
'validate_by_name': True,
128+
}
111129

112130
def add_description(self, description: str):
113131
"""
@@ -127,20 +145,21 @@ class Input(Output):
127145
128146
Developers should take diligent care to include all input types and their properties in the app metadata.
129147
"""
130-
required: bool = pydantic.Field(
148+
required: Optional[bool] = pydantic.Field(
131149
None,
132150
description="(optional, True by default) Indicating whether this input type is mandatory or optional."
133151
)
134152

135-
def __init__(self, *args, **kwargs):
136-
super().__init__(*args, **kwargs)
153+
def __init__(self, **kwargs):
154+
super().__init__(**kwargs)
137155
if self.required is None:
138156
self.required = True
139157

140-
class Config:
141-
title = 'CLAMS Input Specification'
142-
extra = 'forbid'
143-
allow_population_by_field_name = True
158+
model_config = {
159+
'title': 'CLAMS Input Specification',
160+
'extra': 'forbid',
161+
'validate_by_name': True,
162+
}
144163

145164

146165
class RuntimeParameter(_BaseModel):
@@ -178,12 +197,13 @@ class RuntimeParameter(_BaseModel):
178197
"desired dictionary is ``{'key1': 'value1', 'key2': 'value2'}``, the default value (used when "
179198
"initializing a parameter) should be ``['key1:value1','key2:value2']``\n."
180199
)
181-
choices: List[real_valued_primitives] = pydantic.Field(
200+
choices: Optional[List[real_valued_primitives]] = pydantic.Field(
182201
None,
183202
description="(optional) List of string values that can be accepted."
184203
)
185-
default: Union[real_valued_primitives, List[real_valued_primitives]] = pydantic.Field(
204+
default: Optional[Union[real_valued_primitives, List[real_valued_primitives]]] = pydantic.Field(
186205
None,
206+
union_mode='left_to_right',
187207
description="(optional) Default value for the parameter.\n\n"
188208
"Notes for developers: \n\n"
189209
"Setting a default value makes a parameter `optional`. \n\n"
@@ -208,9 +228,10 @@ def __init__(self, **kwargs):
208228
if self.multivalued and self.default is not None and not isinstance(self.default, list):
209229
self.default = [self.default]
210230

211-
class Config:
212-
title = 'CLAMS App Runtime Parameter'
213-
extra = 'forbid'
231+
model_config = {
232+
'title': 'CLAMS App Runtime Parameter',
233+
'extra': 'forbid',
234+
}
214235

215236

216237
class AppMetadata(pydantic.BaseModel):
@@ -236,26 +257,27 @@ class AppMetadata(pydantic.BaseModel):
236257
description="A longer description of the app (what it does, how to use, etc.)."
237258
)
238259
app_version: str = pydantic.Field(
239-
default_factory=generate_app_version,
260+
'', # instead of using default_factory, I will use model_validator to set the default value
261+
# this will work around the limitation of exclude_defaults=True condition when serializing
240262
description="(AUTO-GENERATED, DO NOT SET MANUALLY)\n\n"
241263
"Version of the app.\n\n"
242264
"When the metadata is generated using clams-python SDK, this field is automatically filled in"
243265
)
244266
mmif_version: str = pydantic.Field(
245-
default_factory=get_mmif_specver,
267+
'', # same as above
246268
description="(AUTO-GENERATED, DO NOT SET MANUALLY)\n\n"
247269
"Version of MMIF specification the app.\n\n"
248270
"When the metadata is generated using clams-python SDK, this field is automatically filled in."
249271
)
250-
analyzer_version: str = pydantic.Field(
272+
analyzer_version: Optional[str] = pydantic.Field(
251273
None,
252274
description="(optional) Version of an analyzer software, if the app is working as a wrapper for one. "
253275
)
254276
app_license: str = pydantic.Field(
255277
...,
256278
description="License information of the app."
257279
)
258-
analyzer_license: str = pydantic.Field(
280+
analyzer_license: Optional[str] = pydantic.Field(
259281
None,
260282
description="(optional) License information of an analyzer software, if the app works as a wrapper for one. "
261283
)
@@ -298,7 +320,7 @@ class AppMetadata(pydantic.BaseModel):
298320
[],
299321
description="List of runtime parameters. Can be empty."
300322
)
301-
dependencies: List[str] = pydantic.Field(
323+
dependencies: Optional[List[str]] = pydantic.Field(
302324
None,
303325
description="(optional) List of software dependencies of the app. \n\n"
304326
"This list is completely optional, as in most cases such dependencies are specified in a separate "
@@ -307,36 +329,38 @@ class AppMetadata(pydantic.BaseModel):
307329
"List items must be strings, not any kind of structured data. Thus, it is recommended to include "
308330
"a package name and its version in the string value at the minimum (e.g., ``clams-python==1.2.3``)."
309331
)
310-
more: Dict[str, str] = pydantic.Field(
332+
more: Optional[Dict[str, str]] = pydantic.Field(
311333
None,
312334
description="(optional) A string-to-string map that can be used to store any additional metadata of the app."
313335
)
314336

315-
class Config:
316-
title = "CLAMS AppMetadata"
317-
extra = 'forbid'
318-
allow_population_by_field_name = True
319-
320-
@staticmethod
321-
def json_schema_extra(schema, model) -> None:
322-
for prop in schema.get('properties', {}).values():
323-
prop.pop('title', None)
324-
schema['$schema'] = "http://json-schema.org/draft-07/schema#" # currently pydantic doesn't natively support the $schema field. See https://github.com/samuelcolvin/pydantic/issues/1478
325-
schema['$comment'] = f"clams-python SDK {get_clams_pyver()} was used to generate this schema" # this is only to hold version information
326-
327-
@pydantic.validator('identifier', pre=True)
337+
model_config = {
338+
'title': 'CLAMS AppMetadata',
339+
'extra': 'forbid',
340+
'validate_by_name': True,
341+
'json_schema_extra': lambda schema, model: [adjust(schema) for adjust in [pop_titles, jsonschema_versioning]],
342+
}
343+
344+
@pydantic.model_validator(mode='after')
345+
@classmethod
346+
def assign_versions(cls, data):
347+
if data.app_version == '':
348+
data.app_version = generate_app_version()
349+
if data.mmif_version == '':
350+
data.mmif_version = get_mmif_specver()
351+
return data
352+
353+
@pydantic.field_validator('identifier', mode='before')
354+
@classmethod
328355
def append_version(cls, val):
329356
prefix = f'{app_directory_baseurl if "/" not in val else""}'
330357
suffix = generate_app_version()
331358
return '/'.join(map(lambda x: x.strip('/'), filter(None, (prefix, val, suffix))))
332359

333-
@pydantic.validator('mmif_version', pre=True)
334-
def auto_mmif_version(cls, val):
335-
return get_mmif_specver()
336-
337-
@pydantic.validator('app_version', pre=True)
338-
def auto_app_version(cls, val):
339-
return generate_app_version()
360+
@pydantic.field_validator('url', 'identifier', mode='after') # because pydantic v2 doesn't auto-convert url to string
361+
@classmethod
362+
def stringify(cls, val):
363+
return str(val)
340364

341365
def _check_input_duplicate(self, a_input):
342366
for elem in self.input:
@@ -400,9 +424,7 @@ def add_output(self, at_type: Union[str, vocabulary.ThingTypesBase], **propertie
400424
:param properties: additional property specifications
401425
:return: the newly added Output object
402426
"""
403-
new = Output(at_type=at_type)
404-
if len(properties) > 0:
405-
new.properties = properties
427+
new = Output(at_type=at_type, properties=properties)
406428
if new not in self.output:
407429
self.output.append(new)
408430
else:
@@ -412,7 +434,7 @@ def add_output(self, at_type: Union[str, vocabulary.ThingTypesBase], **propertie
412434
def add_parameter(self, name: str, description: str, type: param_value_types,
413435
choices: Optional[List[real_valued_primitives]] = None,
414436
multivalued: bool = False,
415-
default: Union[real_valued_primitives, List[real_valued_primitives]] = None):
437+
default: Union[None, real_valued_primitives, List[real_valued_primitives]] = None):
416438
"""
417439
Helper method to add an element to the ``parameters`` list.
418440
"""
@@ -456,10 +478,7 @@ def add_more(self, key: str, value: str):
456478
raise ValueError("Key and value should not be empty!")
457479

458480
def jsonify(self, pretty=False):
459-
if pretty:
460-
return self.json(exclude_defaults=True, by_alias=True, indent=2)
461-
else:
462-
return self.json(exclude_defaults=True, by_alias=True)
481+
return self.model_dump_json(exclude_defaults=True, by_alias=True, indent=2 if pretty else None)
463482

464483

465484
if __name__ == '__main__':

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ Flask>=2
44
Flask-RESTful>=0.3.9
55
gunicorn>=20
66
lapps>=0.0.2
7-
pydantic>=1.8,<2
7+
pydantic>=2
88
jsonschema>=3

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def run(self):
6666
'clams': ['develop/templates/**/*', 'develop/templates/**/.*']
6767
},
6868
install_requires=requires,
69-
python_requires='>=3.8',
69+
python_requires='>=3.10',
7070
packages=setuptools.find_packages(),
7171
entry_points={
7272
'console_scripts': [

tests/metadata.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,5 @@ def appmetadata() -> AppMetadata:
1515
)
1616
metadata.add_input(DocumentTypes.TextDocument)
1717
metadata.add_input_oneof(DocumentTypes.AudioDocument, str(DocumentTypes.VideoDocument))
18-
metadata.add_parameter(name='raise_error', description='force raise a ValueError',
19-
type='boolean', default='false')
18+
metadata.add_parameter(name='raise_error', description='force raise a ValueError', type='boolean', default='false')
2019
return metadata

0 commit comments

Comments
 (0)