linkml 1.7.5__py3-none-any.whl → 1.7.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. linkml/generators/__init__.py +2 -0
  2. linkml/generators/docgen/class.md.jinja2 +15 -2
  3. linkml/generators/docgen/slot.md.jinja2 +14 -1
  4. linkml/generators/docgen.py +17 -3
  5. linkml/generators/jsonldcontextgen.py +9 -6
  6. linkml/generators/jsonldgen.py +3 -1
  7. linkml/generators/owlgen.py +14 -0
  8. linkml/generators/prefixmapgen.py +5 -4
  9. linkml/generators/projectgen.py +14 -2
  10. linkml/generators/pydanticgen/__init__.py +29 -0
  11. linkml/generators/{pydanticgen.py → pydanticgen/pydanticgen.py} +192 -299
  12. linkml/generators/pydanticgen/template.py +504 -0
  13. linkml/generators/pydanticgen/templates/attribute.py.jinja +10 -0
  14. linkml/generators/pydanticgen/templates/base_model.py.jinja +27 -0
  15. linkml/generators/pydanticgen/templates/class.py.jinja +21 -0
  16. linkml/generators/pydanticgen/templates/conditional_import.py.jinja +9 -0
  17. linkml/generators/pydanticgen/templates/enum.py.jinja +16 -0
  18. linkml/generators/pydanticgen/templates/footer.py.jinja +13 -0
  19. linkml/generators/pydanticgen/templates/imports.py.jinja +31 -0
  20. linkml/generators/pydanticgen/templates/module.py.jinja +27 -0
  21. linkml/generators/pydanticgen/templates/validator.py.jinja +15 -0
  22. linkml/generators/pythongen.py +13 -7
  23. linkml/generators/shacl/__init__.py +3 -0
  24. linkml/generators/shacl/ifabsent_processor.py +41 -0
  25. linkml/generators/shacl/shacl_data_type.py +40 -0
  26. linkml/generators/shaclgen.py +105 -82
  27. linkml/generators/shexgen.py +1 -1
  28. linkml/generators/sqlalchemygen.py +1 -1
  29. linkml/generators/sqltablegen.py +32 -22
  30. linkml/generators/terminusdbgen.py +7 -1
  31. linkml/linter/config/datamodel/config.py +8 -0
  32. linkml/linter/rules.py +11 -2
  33. linkml/utils/generator.py +7 -6
  34. linkml/utils/ifabsent_functions.py +7 -9
  35. linkml/utils/schemaloader.py +1 -9
  36. linkml/utils/sqlutils.py +39 -25
  37. {linkml-1.7.5.dist-info → linkml-1.7.6.dist-info}/METADATA +4 -1
  38. {linkml-1.7.5.dist-info → linkml-1.7.6.dist-info}/RECORD +41 -27
  39. {linkml-1.7.5.dist-info → linkml-1.7.6.dist-info}/LICENSE +0 -0
  40. {linkml-1.7.5.dist-info → linkml-1.7.6.dist-info}/WHEEL +0 -0
  41. {linkml-1.7.5.dist-info → linkml-1.7.6.dist-info}/entry_points.txt +0 -0
@@ -3,12 +3,13 @@ import logging
3
3
  import os
4
4
  from collections import defaultdict
5
5
  from copy import deepcopy
6
- from dataclasses import dataclass, field
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
7
8
  from types import ModuleType
8
- from typing import Dict, List, Optional, Set, Type, Union
9
+ from typing import Dict, List, Literal, Optional, Set, Type, Union
9
10
 
10
11
  import click
11
- from jinja2 import Template
12
+ from jinja2 import ChoiceLoader, Environment, FileSystemLoader
12
13
 
13
14
  # from linkml.generators import pydantic_GEN_VERSION
14
15
  from linkml_runtime.linkml_model.meta import (
@@ -29,255 +30,21 @@ from linkml.generators.common.type_designators import (
29
30
  get_type_designator_value,
30
31
  )
31
32
  from linkml.generators.oocodegen import OOCodeGenerator
33
+ from linkml.generators.pydanticgen.template import (
34
+ ConditionalImport,
35
+ Import,
36
+ Imports,
37
+ ObjectImport,
38
+ PydanticAttribute,
39
+ PydanticBaseModel,
40
+ PydanticClass,
41
+ PydanticModule,
42
+ TemplateModel,
43
+ )
32
44
  from linkml.utils.generator import shared_arguments
33
45
  from linkml.utils.ifabsent_functions import ifabsent_value_declaration
34
46
 
35
47
 
36
- def default_template(
37
- pydantic_ver: str = "1", extra_fields: str = "forbid", injected_classes: Optional[List[Union[Type, str]]] = None
38
- ) -> str:
39
- """Constructs a default template for pydantic classes based on the version of pydantic"""
40
- ### HEADER ###
41
- template = """
42
- {#-
43
-
44
- Jinja2 Template for a pydantic classes
45
- -#}
46
- from __future__ import annotations
47
- from datetime import datetime, date
48
- from enum import Enum
49
- {% if uses_numpy -%}
50
- import numpy as np
51
- {%- endif %}
52
- from decimal import Decimal
53
- from typing import List, Dict, Optional, Any, Union"""
54
- if pydantic_ver == "1":
55
- template += """
56
- from pydantic import BaseModel as BaseModel, Field, validator"""
57
- elif pydantic_ver == "2":
58
- template += """
59
- from pydantic import BaseModel as BaseModel, ConfigDict, Field, field_validator"""
60
- template += """
61
- import re
62
- import sys
63
- if sys.version_info >= (3, 8):
64
- from typing import Literal
65
- else:
66
- from typing_extensions import Literal
67
-
68
- {% if imports is not none %}
69
- {%- for import_module, import_classes in imports.items() -%}
70
- {% if import_classes is none -%}
71
- import {{ import_module }}
72
- {% elif import_classes is mapping -%}
73
- import {{ import_module }} as {{ import_classes['as'] }}
74
- {% else -%}
75
- from {{ import_module }} import (
76
- {% for imported_class in import_classes %}
77
- {%- if imported_class is string -%}
78
- {{ imported_class }}
79
- {%- else -%}
80
- {{ imported_class['name'] }} as {{ imported_class['as'] }}
81
- {%- endif -%}
82
- {%- if not loop.last %},{{ '\n ' }}{% else %}{{ '\n' }}{%- endif -%}
83
- {% endfor -%}
84
- )
85
- {% endif -%}
86
- {% endfor -%}
87
- {% endif %}
88
- metamodel_version = "{{metamodel_version}}"
89
- version = "{{version if version else None}}"
90
- """
91
- ### BASE MODEL ###
92
- if pydantic_ver == "1":
93
- template += f"""
94
- class WeakRefShimBaseModel(BaseModel):
95
- __slots__ = '__weakref__'
96
-
97
- class ConfiguredBaseModel(WeakRefShimBaseModel,
98
- validate_assignment = True,
99
- validate_all = True,
100
- underscore_attrs_are_private = True,
101
- extra = '{extra_fields}',
102
- arbitrary_types_allowed = True,
103
- use_enum_values = True):
104
- """
105
- else:
106
- template += f"""
107
- class ConfiguredBaseModel(BaseModel):
108
- model_config = ConfigDict(
109
- validate_assignment=True,
110
- validate_default=True,
111
- extra = '{extra_fields}',
112
- arbitrary_types_allowed=True,
113
- use_enum_values = True)
114
- """
115
-
116
- ### Fields injected into base model
117
- template += """{% if injected_fields is not none %}
118
- {% for field in injected_fields -%}
119
- {{ field }}
120
- {% endfor %}
121
- {% else %}
122
- pass
123
- {% endif %}
124
- """
125
-
126
- ### Extra classes
127
- if injected_classes is not None and len(injected_classes) > 0:
128
- template += """{{ '\n\n' }}"""
129
- for cls in injected_classes:
130
- if isinstance(cls, str):
131
- template += cls + "\n\n"
132
- else:
133
- template += inspect.getsource(cls) + "\n\n"
134
-
135
- ### ENUMS ###
136
- template += """
137
- {% for e in enums.values() %}
138
- class {{ e.name }}(str{% if e['values'] %}, Enum{% endif %}):
139
- {% if e.description -%}
140
- \"\"\"
141
- {{ e.description }}
142
- \"\"\"
143
- {%- endif %}
144
- {% for _, pv in e['values'].items() -%}
145
- {% if pv.description -%}
146
- # {{pv.description}}
147
- {%- endif %}
148
- {{pv.label}} = "{{pv.value}}"
149
- {% endfor %}
150
- {% if not e['values'] -%}
151
- dummy = "dummy"
152
- {% endif %}
153
- {% endfor %}
154
- """
155
- ### CLASSES ###
156
- if pydantic_ver == "1":
157
- template += """
158
- {%- for c in schema.classes.values() %}
159
- class {{ c.name }}
160
- {%- if class_isa_plus_mixins[c.name] -%}
161
- ({{class_isa_plus_mixins[c.name]|join(', ')}})
162
- {%- else -%}
163
- (ConfiguredBaseModel)
164
- {%- endif -%}
165
- :
166
- {% if c.description -%}
167
- \"\"\"
168
- {{ c.description }}
169
- \"\"\"
170
- {%- endif %}
171
- {% for attr in c.attributes.values() if c.attributes -%}
172
- {{attr.name}}: {{ attr.annotations['python_range'].value }} = Field(
173
- {%- if predefined_slot_values[c.name][attr.name] is not callable -%}
174
- {{ predefined_slot_values[c.name][attr.name] }}
175
- {%- elif (attr.required or attr.identifier or attr.key) -%}
176
- ...
177
- {%- else -%}
178
- None
179
- {%- endif -%}
180
- {%- if attr.title != None %}, title="{{attr.title}}"{% endif -%}
181
- {%- if attr.description %}, description=\"\"\"{{attr.description}}\"\"\"{% endif -%}
182
- {%- if attr.equals_number != None %}, le={{attr.equals_number}}, ge={{attr.equals_number}}
183
- {%- else -%}
184
- {%- if attr.minimum_value != None %}, ge={{attr.minimum_value}}{% endif -%}
185
- {%- if attr.maximum_value != None %}, le={{attr.maximum_value}}{% endif -%}
186
- {%- endif -%}
187
- )
188
- {% else -%}
189
- None
190
- {% endfor %}
191
- {% for attr in c.attributes.values() if c.attributes -%}
192
- {%- if attr.pattern %}
193
- @validator('{{attr.name}}', allow_reuse=True)
194
- def pattern_{{attr.name}}(cls, v):
195
- pattern=re.compile(r"{{attr.pattern}}")
196
- if isinstance(v,list):
197
- for element in v:
198
- if not pattern.match(element):
199
- raise ValueError(f"Invalid {{attr.name}} format: {element}")
200
- elif isinstance(v,str):
201
- if not pattern.match(v):
202
- raise ValueError(f"Invalid {{attr.name}} format: {v}")
203
- return v
204
- {% endif -%}
205
- {% endfor %}
206
- {% endfor %}
207
- """
208
- elif pydantic_ver == "2":
209
- template += """
210
- {%- for c in schema.classes.values() %}
211
- class {{ c.name }}
212
- {%- if class_isa_plus_mixins[c.name] -%}
213
- ({{class_isa_plus_mixins[c.name]|join(', ')}})
214
- {%- else -%}
215
- (ConfiguredBaseModel)
216
- {%- endif -%}
217
- :
218
- {% if c.description -%}
219
- \"\"\"
220
- {{ c.description }}
221
- \"\"\"
222
- {%- endif %}
223
- {% for attr in c.attributes.values() if c.attributes -%}
224
- {{attr.name}}: {{ attr.annotations['python_range'].value }} = Field(
225
- {%- if predefined_slot_values[c.name][attr.name] is not callable -%}
226
- {{ predefined_slot_values[c.name][attr.name] }}
227
- {%- elif (attr.required or attr.identifier or attr.key) -%}
228
- ...
229
- {%- else -%}
230
- None
231
- {%- endif -%}
232
- {%- if attr.title != None %}, title="{{attr.title}}"{% endif -%}
233
- {%- if attr.description %}, description=\"\"\"{{attr.description}}\"\"\"{% endif -%}
234
- {%- if attr.equals_number != None %}, le={{attr.equals_number}}, ge={{attr.equals_number}}
235
- {%- else -%}
236
- {%- if attr.minimum_value != None %}, ge={{attr.minimum_value}}{% endif -%}
237
- {%- if attr.maximum_value != None %}, le={{attr.maximum_value}}{% endif -%}
238
- {%- endif -%}
239
- )
240
- {% else -%}
241
- None
242
- {% endfor %}
243
- {% for attr in c.attributes.values() if c.attributes -%}
244
- {%- if attr.pattern %}
245
- @field_validator('{{attr.name}}')
246
- def pattern_{{attr.name}}(cls, v):
247
- pattern=re.compile(r"{{attr.pattern}}")
248
- if isinstance(v,list):
249
- for element in v:
250
- if not pattern.match(element):
251
- raise ValueError(f"Invalid {{attr.name}} format: {element}")
252
- elif isinstance(v,str):
253
- if not pattern.match(v):
254
- raise ValueError(f"Invalid {{attr.name}} format: {v}")
255
- return v
256
- {% endif -%}
257
- {% endfor %}
258
- {% endfor %}
259
- """
260
-
261
- ### FWD REFS / REBUILD MODEL ###
262
- if pydantic_ver == "1":
263
- template += """
264
- # Update forward refs
265
- # see https://pydantic-docs.helpmanual.io/usage/postponed_annotations/
266
- {% for c in schema.classes.values() -%}
267
- {{ c.name }}.update_forward_refs()
268
- {% endfor %}
269
- """
270
- else:
271
- template += """
272
- # Model rebuild
273
- # see https://pydantic-docs.helpmanual.io/usage/models/#rebuilding-a-model
274
- {% for c in schema.classes.values() -%}
275
- {{ c.name }}.model_rebuild()
276
- {% endfor %}
277
- """
278
- return template
279
-
280
-
281
48
  def _get_pyrange(t: TypeDefinition, sv: SchemaView) -> str:
282
49
  pyrange = t.repr if t is not None else None
283
50
  if pyrange is None:
@@ -293,6 +60,42 @@ def _get_pyrange(t: TypeDefinition, sv: SchemaView) -> str:
293
60
  return pyrange
294
61
 
295
62
 
63
+ DEFAULT_IMPORTS = (
64
+ Imports()
65
+ + Import(module="__future__", objects=[ObjectImport(name="annotations")])
66
+ + Import(module="datetime", objects=[ObjectImport(name="datetime"), ObjectImport(name="date")])
67
+ + Import(module="decimal", objects=[ObjectImport(name="Decimal")])
68
+ + Import(module="enum", objects=[ObjectImport(name="Enum")])
69
+ + Import(module="re")
70
+ + Import(
71
+ module="typing",
72
+ objects=[
73
+ ObjectImport(name="Any"),
74
+ ObjectImport(name="List"),
75
+ ObjectImport(name="Literal"),
76
+ ObjectImport(name="Dict"),
77
+ ObjectImport(name="Optional"),
78
+ ObjectImport(name="Union"),
79
+ ],
80
+ )
81
+ + Import(module="pydantic.version", objects=[ObjectImport(name="VERSION", alias="PYDANTIC_VERSION")])
82
+ + ConditionalImport(
83
+ condition="int(PYDANTIC_VERSION[0])>=2",
84
+ module="pydantic",
85
+ objects=[
86
+ ObjectImport(name="BaseModel"),
87
+ ObjectImport(name="ConfigDict"),
88
+ ObjectImport(name="Field"),
89
+ ObjectImport(name="field_validator"),
90
+ ],
91
+ alternative=Import(
92
+ module="pydantic",
93
+ objects=[ObjectImport(name="BaseModel"), ObjectImport(name="Field"), ObjectImport(name="validator")],
94
+ ),
95
+ )
96
+ )
97
+
98
+
296
99
  @dataclass
297
100
  class PydanticGenerator(OOCodeGenerator):
298
101
  """
@@ -308,9 +111,16 @@ class PydanticGenerator(OOCodeGenerator):
308
111
  file_extension = "py"
309
112
 
310
113
  # ObjectVars
311
- pydantic_version: str = field(default_factory=lambda: PYDANTIC_VERSION[0])
312
- template_file: str = None
313
- extra_fields: str = "forbid"
114
+ pydantic_version: int = int(PYDANTIC_VERSION[0])
115
+ template_dir: Optional[Union[str, Path]] = None
116
+ """
117
+ Override templates for each TemplateModel.
118
+
119
+ Directory with templates that override the default :attr:`.TemplateModel.template`
120
+ for each class. If a matching template is not found in the override directory,
121
+ the default templates will be used.
122
+ """
123
+ extra_fields: Literal["allow", "forbid", "ignore"] = "forbid"
314
124
  gen_mixin_inheritance: bool = True
315
125
  injected_classes: Optional[List[Union[Type, str]]] = None
316
126
  """
@@ -334,44 +144,54 @@ class PydanticGenerator(OOCodeGenerator):
334
144
  )
335
145
 
336
146
  """
337
- imports: Optional[Dict[str, Union[None, Dict[str, str], List[Union[str, Dict[str, str]]]]]] = None
147
+ imports: Optional[List[Import]] = None
338
148
  """
339
149
  Additional imports to inject into generated module.
340
150
 
341
- A dictionary mapping a module to a list of objects to import.
342
- If the value of an entry is ``None`` , import the whole module.
343
-
344
- Import Aliases:
345
- If the value of a module import is a dictionary with a key "as",
346
- or a class is specified as a dictionary with a "name" and "as",
347
- then the import is renamed like "from module import class as renamedClass".
348
-
349
151
  Examples:
350
152
 
351
153
  .. code-block:: python
352
154
 
353
- imports = {
354
- 'typing': ['Dict', 'List', 'Union'],
355
- 'types': None,
356
- 'numpy': {'as': 'np'},
357
- 'collections': [{'name': 'OrderedDict', 'as': 'odict'}]
358
- }
155
+ from linkml.generators.pydanticgen.template import (
156
+ ConditionalImport,
157
+ ObjectImport,
158
+ Import,
159
+ Imports
160
+ )
161
+
162
+ imports = (Imports() +
163
+ Import(module='sys') +
164
+ Import(module='numpy', alias='np') +
165
+ Import(module='pathlib', objects=[
166
+ ObjectImport(name="Path"),
167
+ ObjectImport(name="PurePath", alias="RenamedPurePath")
168
+ ]) +
169
+ ConditionalImport(
170
+ module="typing",
171
+ objects=[ObjectImport(name="Literal")],
172
+ condition="sys.version_info >= (3, 8)",
173
+ alternative=Import(
174
+ module="typing_extensions",
175
+ objects=[ObjectImport(name="Literal")]
176
+ ),
177
+ ).imports
178
+ )
359
179
 
360
180
  becomes:
361
181
 
362
182
  .. code-block:: python
363
183
 
364
- from typing import (
365
- Dict,
366
- List,
367
- Union
368
- )
369
- import types
184
+ import sys
370
185
  import numpy as np
371
- from collections import (
372
- OrderedDict as odict
186
+ from pathlib import (
187
+ Path,
188
+ PurePath as RenamedPurePath
373
189
  )
374
-
190
+ if sys.version_info >= (3, 8):
191
+ from typing import Literal
192
+ else:
193
+ from typing_extensions import Literal
194
+
375
195
  """
376
196
 
377
197
  # ObjectVars (identical to pythongen)
@@ -664,13 +484,14 @@ class PydanticGenerator(OOCodeGenerator):
664
484
  return _get_pyrange(value_slot_range_type, sv)
665
485
  return None
666
486
 
667
- def serialize(self) -> str:
668
- if self.template_file is not None:
669
- with open(self.template_file) as template_file:
670
- template_obj = Template(template_file.read())
671
- else:
672
- template_obj = Template(default_template(self.pydantic_version, self.extra_fields, self.injected_classes))
487
+ def _template_environment(self) -> Environment:
488
+ env = TemplateModel.environment()
489
+ if self.template_dir is not None:
490
+ loader = ChoiceLoader([FileSystemLoader(self.template_dir), env.loader])
491
+ env.loader = loader
492
+ return env
673
493
 
494
+ def serialize(self) -> str:
674
495
  sv: SchemaView
675
496
  sv = self.schemaview
676
497
  schema = sv.schema
@@ -681,7 +502,10 @@ class PydanticGenerator(OOCodeGenerator):
681
502
  )
682
503
  enums = self.generate_enums(sv.all_enums())
683
504
 
684
- uses_numpy = False
505
+ imports = DEFAULT_IMPORTS
506
+ if self.imports is not None:
507
+ for i in self.imports:
508
+ imports += i
685
509
 
686
510
  sorted_classes = self.sort_classes(list(sv.all_classes().values()))
687
511
  self.sorted_class_names = [camelcase(c.name) for c in sorted_classes]
@@ -738,9 +562,9 @@ class PydanticGenerator(OOCodeGenerator):
738
562
  if "linkml:elements" in s.implements:
739
563
  # TODO add support for xarray
740
564
  pyrange = "np.ndarray"
565
+ imports += Import(module="numpy", alias="np")
741
566
  if "linkml:ColumnOrderedArray" in class_def.implements:
742
567
  raise NotImplementedError("Cannot generate Pydantic code for ColumnOrderedArrays.")
743
- uses_numpy = True
744
568
  elif s.multivalued:
745
569
  if s.inlined or s.inlined_as_list:
746
570
  collection_key = self.generate_collection_key(slot_ranges, s, class_def)
@@ -761,31 +585,88 @@ class PydanticGenerator(OOCodeGenerator):
761
585
  pyrange = f"Optional[{pyrange}]"
762
586
  ann = Annotation("python_range", pyrange)
763
587
  s.annotations[ann.tag] = ann
764
- code = template_obj.render(
765
- schema=pyschema,
766
- underscore=underscore,
767
- enums=enums,
768
- predefined_slot_values=self.get_predefined_slot_values(),
769
- extra_fields=self.extra_fields,
588
+
589
+ if self.injected_classes is not None:
590
+ injected_classes = [c if isinstance(c, str) else inspect.getsource(c) for c in self.injected_classes]
591
+ else:
592
+ injected_classes = None
593
+
594
+ base_model = PydanticBaseModel(
595
+ pydantic_ver=self.pydantic_version, extra_fields=self.extra_fields, fields=self.injected_fields
596
+ )
597
+
598
+ classes = {}
599
+ predefined = self.get_predefined_slot_values()
600
+ bases = self.get_class_isa_plus_mixins()
601
+ for k, c in pyschema.classes.items():
602
+ attrs = {}
603
+ for attr_name, src_attr in c.attributes.items():
604
+ new_fields = {
605
+ k: src_attr._as_dict.get(k, None)
606
+ for k in PydanticAttribute.model_fields.keys()
607
+ if src_attr._as_dict.get(k, None) is not None
608
+ }
609
+ predef_slot = predefined.get(k, {}).get(attr_name, None)
610
+ if predef_slot is not None:
611
+ predef_slot = str(predef_slot)
612
+ new_fields["predefined"] = predef_slot
613
+ new_fields["name"] = attr_name
614
+ attrs[attr_name] = PydanticAttribute(**new_fields, pydantic_ver=self.pydantic_version)
615
+
616
+ new_class = PydanticClass(
617
+ name=k, attributes=attrs, description=c.description, pydantic_ver=self.pydantic_version
618
+ )
619
+ if k in bases:
620
+ new_class.bases = bases[k]
621
+ classes[k] = new_class
622
+
623
+ module = PydanticModule(
624
+ pydantic_ver=self.pydantic_version,
770
625
  metamodel_version=self.schema.metamodel_version,
771
626
  version=self.schema.version,
772
- class_isa_plus_mixins=self.get_class_isa_plus_mixins(),
773
- imports=self.imports,
774
- injected_fields=self.injected_fields,
775
- uses_numpy=uses_numpy,
627
+ imports=imports.imports,
628
+ base_model=base_model,
629
+ injected_classes=injected_classes,
630
+ enums=enums,
631
+ classes=classes,
776
632
  )
633
+ code = module.render(self._template_environment())
777
634
  return code
778
635
 
779
636
  def default_value_for_type(self, typ: str) -> str:
780
637
  return "None"
781
638
 
782
639
 
640
+ def _subclasses(cls: Type):
641
+ return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in _subclasses(c)])
642
+
643
+
644
+ _TEMPLATE_NAMES = sorted(list(set([c.template for c in _subclasses(TemplateModel)])))
645
+
646
+
783
647
  @shared_arguments(PydanticGenerator)
784
- @click.option("--template-file", help="Optional jinja2 template to use for class generation")
648
+ @click.option("--template-file", hidden=True)
649
+ @click.option(
650
+ "--template-dir",
651
+ type=click.Path(),
652
+ help="""
653
+ Optional jinja2 template directory to use for class generation.
654
+
655
+ Pass a directory containing templates with the same name as any of the default
656
+ :class:`.TemplateModel` templates to override them. The given directory will be
657
+ searched for matching templates, and use the default templates as a fallback
658
+ if an override is not found
659
+
660
+ Available templates to override:
661
+
662
+ \b
663
+ """
664
+ + "\n".join(["- " + name for name in _TEMPLATE_NAMES]),
665
+ )
785
666
  @click.option(
786
667
  "--pydantic-version",
787
- type=click.Choice(["1", "2"]),
788
- default="1",
668
+ type=click.IntRange(1, 2),
669
+ default=1,
789
670
  help="Pydantic version to use (1 or 2)",
790
671
  )
791
672
  @click.option(
@@ -799,25 +680,37 @@ class PydanticGenerator(OOCodeGenerator):
799
680
  def cli(
800
681
  yamlfile,
801
682
  template_file=None,
683
+ template_dir: Optional[str] = None,
802
684
  head=True,
803
- emit_metadata=False,
804
685
  genmeta=False,
805
686
  classvars=True,
806
687
  slots=True,
807
- pydantic_version="1",
688
+ pydantic_version=1,
808
689
  extra_fields="forbid",
809
690
  **args,
810
691
  ):
811
692
  """Generate pydantic classes to represent a LinkML model"""
693
+ if template_file is not None:
694
+ raise DeprecationWarning(
695
+ (
696
+ "Passing a single template_file is deprecated. Pass a directory of template files instead. "
697
+ "See help string for --template-dir"
698
+ )
699
+ )
700
+
701
+ if template_dir is not None:
702
+ if not Path(template_dir).exists():
703
+ raise FileNotFoundError(f"The template directory {template_dir} does not exist!")
704
+
812
705
  gen = PydanticGenerator(
813
706
  yamlfile,
814
- template_file=template_file,
815
707
  pydantic_version=pydantic_version,
816
708
  extra_fields=extra_fields,
817
709
  emit_metadata=head,
818
710
  genmeta=genmeta,
819
711
  gen_classvars=classvars,
820
712
  gen_slots=slots,
713
+ template_dir=template_dir,
821
714
  **args,
822
715
  )
823
716
  print(gen.serialize())