Skip to content

Commit 8f4242b

Browse files
committed
update lib to support pydantic=2.x.x
1 parent 83ec9e9 commit 8f4242b

File tree

31 files changed

+408
-18
lines changed

31 files changed

+408
-18
lines changed

pydantic2ts/cli/script.py

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,28 @@
77
import shutil
88
import sys
99
from importlib.util import module_from_spec, spec_from_file_location
10+
from pathlib import Path
1011
from tempfile import mkdtemp
1112
from types import ModuleType
1213
from typing import Any, Dict, List, Tuple, Type
1314
from uuid import uuid4
1415

15-
from pydantic import BaseModel, Extra, create_model
16+
from pydantic import VERSION, BaseModel, Extra, create_model
1617

17-
try:
18-
from pydantic.generics import GenericModel
19-
except ImportError:
20-
GenericModel = None
18+
V2 = True if VERSION.startswith("2") else False
19+
20+
if not V2:
21+
try:
22+
from pydantic.generics import GenericModel
23+
except ImportError:
24+
GenericModel = None
2125

2226
logger = logging.getLogger("pydantic2ts")
2327

2428

29+
DEBUG = os.environ.get("DEBUG", False)
30+
31+
2532
def import_module(path: str) -> ModuleType:
2633
"""
2734
Helper which allows modules to be specified by either dotted path notation or by filepath.
@@ -61,12 +68,15 @@ def is_concrete_pydantic_model(obj) -> bool:
6168
Return true if an object is a concrete subclass of pydantic's BaseModel.
6269
'concrete' meaning that it's not a GenericModel.
6370
"""
71+
generic_metadata = getattr(obj, "__pydantic_generic_metadata__", None)
6472
if not inspect.isclass(obj):
6573
return False
6674
elif obj is BaseModel:
6775
return False
68-
elif GenericModel and issubclass(obj, GenericModel):
76+
elif not V2 and GenericModel and issubclass(obj, GenericModel):
6977
return bool(obj.__concrete__)
78+
elif V2 and generic_metadata:
79+
return not bool(generic_metadata["parameters"])
7080
else:
7181
return issubclass(obj, BaseModel)
7282

@@ -141,7 +151,7 @@ def clean_schema(schema: Dict[str, Any]) -> None:
141151
del schema["description"]
142152

143153

144-
def generate_json_schema(models: List[Type[BaseModel]]) -> str:
154+
def generate_json_schema_v1(models: List[Type[BaseModel]]) -> str:
145155
"""
146156
Create a top-level '_Master_' model with references to each of the actual models.
147157
Generate the schema for this model, which will include the schemas for all the
@@ -178,6 +188,43 @@ def generate_json_schema(models: List[Type[BaseModel]]) -> str:
178188
m.Config.extra = x
179189

180190

191+
def generate_json_schema_v2(models: List[Type[BaseModel]]) -> str:
192+
"""
193+
Create a top-level '_Master_' model with references to each of the actual models.
194+
Generate the schema for this model, which will include the schemas for all the
195+
nested models. Then clean up the schema.
196+
197+
One weird thing we do is we temporarily override the 'extra' setting in models,
198+
changing it to 'forbid' UNLESS it was explicitly set to 'allow'. This prevents
199+
'[k: string]: any' from being added to every interface. This change is reverted
200+
once the schema has been generated.
201+
"""
202+
model_extras = [m.model_config.get("extra") for m in models]
203+
204+
try:
205+
for m in models:
206+
if m.model_config.get("extra") != "allow":
207+
m.model_config["extra"] = "forbid"
208+
209+
master_model: BaseModel = create_model(
210+
"_Master_", **{m.__name__: (m, ...) for m in models}
211+
)
212+
master_model.model_config["extra"] = "forbid"
213+
master_model.model_config["json_schema_extra"] = staticmethod(clean_schema)
214+
215+
schema: dict = master_model.model_json_schema(mode="serialization")
216+
217+
for d in schema.get("$defs", {}).values():
218+
clean_schema(d)
219+
220+
return json.dumps(schema, indent=2)
221+
222+
finally:
223+
for m, x in zip(models, model_extras):
224+
if x is not None:
225+
m.model_config["extra"] = x
226+
227+
181228
def generate_typescript_defs(
182229
module: str, output: str, exclude: Tuple[str] = (), json2ts_cmd: str = "json2ts"
183230
) -> None:
@@ -205,13 +252,20 @@ def generate_typescript_defs(
205252

206253
logger.info("Generating JSON schema from pydantic models...")
207254

208-
schema = generate_json_schema(models)
255+
schema = generate_json_schema_v2(models) if V2 else generate_json_schema_v1(models)
256+
209257
schema_dir = mkdtemp()
210258
schema_file_path = os.path.join(schema_dir, "schema.json")
211259

212260
with open(schema_file_path, "w") as f:
213261
f.write(schema)
214262

263+
if DEBUG:
264+
debug_schema_file_path = Path(module).parent / "schema_debug.json"
265+
# raise ValueError(module)
266+
with open(debug_schema_file_path, "w") as f:
267+
f.write(schema)
268+
215269
logger.info("Converting JSON schema to typescript definitions...")
216270

217271
json2ts_exit_code = os.system(

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from setuptools import setup, find_packages
1+
from setuptools import find_packages, setup
22

33

44
def readme():
@@ -25,7 +25,7 @@ def readme():
2525

2626
setup(
2727
name="pydantic-to-typescript",
28-
version="1.0.10",
28+
version="1.1.11",
2929
description="Convert pydantic models to typescript interfaces",
3030
license="MIT",
3131
long_description=readme(),
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# https://docs.pydantic.dev/latest/usage/computed_fields/
2+
3+
from pydantic import BaseModel, computed_field
4+
5+
6+
class Rectangle(BaseModel):
7+
width: int
8+
length: int
9+
10+
@computed_field
11+
@property
12+
def area(self) -> int:
13+
return self.width * self.length
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/* tslint:disable */
2+
/* eslint-disable */
3+
/**
4+
/* This file was automatically generated from pydantic models by running pydantic2ts.
5+
/* Do not modify it by hand - just update the pydantic models and then re-run the script
6+
*/
7+
8+
export interface Rectangle {
9+
width: number;
10+
length: number;
11+
area: number;
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/* tslint:disable */
2+
/* eslint-disable */
3+
/**
4+
/* This file was automatically generated from pydantic models by running pydantic2ts.
5+
/* Do not modify it by hand - just update the pydantic models and then re-run the script
6+
*/
7+
8+
export interface Profile {
9+
username: string;
10+
age: number | null;
11+
hobbies: string[];
12+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from pydantic import BaseModel, Extra
2+
3+
4+
class ModelAllow(BaseModel, extra=Extra.allow):
5+
a: str
6+
7+
class ModelDefault(BaseModel):
8+
a: str
9+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/* tslint:disable */
2+
/* eslint-disable */
3+
/**
4+
/* This file was automatically generated from pydantic models by running pydantic2ts.
5+
/* Do not modify it by hand - just update the pydantic models and then re-run the script
6+
*/
7+
8+
export interface ModelAllow {
9+
a: string;
10+
[k: string]: unknown;
11+
}
12+
export interface ModelDefault {
13+
a: string;
14+
}

0 commit comments

Comments
 (0)