Pydantic#

Example Output#

personinfo_pydantic.py

Overview#

The Pydantic Generator produces Pydantic flavored python dataclasses from a linkml model, with optional support for user-supplied jinja2 templates to generate alternate classes.

Example#

Given a definition of a Person class:

Person:
  is_a: NamedThing
  description: >-
    A person (alive, dead, undead, or fictional).
  class_uri: schema:Person
  mixins:
    - HasAliases
  slots:
    - primary_email
    - birth_date
    - age_in_years
    - gender
    - current_address
    - has_employment_history
    - has_familial_relationships
    - has_medical_history

(some details omitted for brevity, including slot definitions and parent classes)

The generate python looks like this:

class Person(NamedThing):
    """
    A person (alive, dead, undead, or fictional).
    """
    primary_email: Optional[str] = Field(None)
    birth_date: Optional[str] = Field(None)
    age_in_years: Optional[int] = Field(None, ge=0, le=999)
    gender: Optional[GenderType] = Field(None)
    current_address: Optional[Address] = Field(None, description="""The address at which a person currently lives""")
    has_employment_history: Optional[List[EmploymentEvent]] = Field(None)
    has_familial_relationships: Optional[List[FamilialRelationship]] = Field(None)
    has_medical_history: Optional[List[MedicalEvent]] = Field(None)
    aliases: Optional[List[str]] = Field(None)
    id: Optional[str] = Field(None)
    name: Optional[str] = Field(None)
    description: Optional[str] = Field(None)
    image: Optional[str] = Field(None)

Command Line#

gen-pydantic#

Generate pydantic classes to represent a LinkML model

gen-pydantic [OPTIONS] YAMLFILE

Options

-V, --version#

Show the version and exit.

--extra-fields <extra_fields>#

How to handle extra fields in BaseModel.

Options:

allow | ignore | forbid

--array-representations <array_representations>#

List of array representations to accept for array slots. Default is list of lists.

Options:

list | nparray

--pydantic-version <pydantic_version>#

Pydantic version to use (1 or 2)

--template-dir <template_dir>#

Optional jinja2 template directory to use for class generation.

Pass a directory containing templates with the same name as any of the default TemplateModel templates to override them. The given directory will be searched for matching templates, and use the default templates as a fallback if an override is not found

Available templates to override:

- attribute.py.jinja
- base_model.py.jinja
- class.py.jinja
- conditional_import.py.jinja
- enum.py.jinja
- imports.py.jinja
- module.py.jinja
- validator.py.jinja
-f, --format <format>#

Output format

Default:

pydantic

Options:

pydantic

--metadata, --no-metadata#

Include metadata in output

Default:

True

--useuris, --metauris#

Use class and slot URIs over model uris

Default:

True

-im, --importmap <importmap>#

Import mapping file

--log_level <log_level>#

Logging level

Default:

WARNING

Options:

CRITICAL | ERROR | WARNING | INFO | DEBUG

-v, --verbose#

Verbosity. Takes precedence over –log_level.

--mergeimports, --no-mergeimports#

Merge imports into source file (default=mergeimports)

--stacktrace, --no-stacktrace#

Print a stack trace when an error occurs

Default:

False

Arguments

YAMLFILE#

Required argument

Generator#

class linkml.generators.pydanticgen.PydanticGenerator(schema: str | ~typing.TextIO | ~linkml_runtime.linkml_model.meta.SchemaDefinition | Generator | ~pathlib.Path, schemaview: ~linkml_runtime.utils.schemaview.SchemaView | None = None, format: str | None = None, metadata: bool = True, useuris: bool | None = None, log_level: int | None = 30, mergeimports: bool | None = True, source_file_date: str | None = None, source_file_size: int | None = None, logger: ~logging.Logger | None = None, verbose: bool | None = None, output: str | None = None, namespaces: ~linkml_runtime.utils.namespaces.Namespaces | None = None, directory_output: bool = False, base_dir: str = None, metamodel_name_map: ~typing.Dict[str, str] = None, importmap: str | ~typing.Mapping[str, str] | None = None, emit_prefixes: ~typing.Set[str] = <factory>, metamodel: ~linkml.utils.schemaloader.SchemaLoader = None, stacktrace: bool = False, template_file: str = None, package: str = 'example', array_representations: ~typing.List[~linkml.generators.pydanticgen.array.ArrayRepresentation] = <factory>, black: bool = False, pydantic_version: int = 2, template_dir: str | ~pathlib.Path | None = None, extra_fields: ~typing.Literal['allow', 'forbid', 'ignore'] = 'forbid', gen_mixin_inheritance: bool = True, injected_classes: ~typing.List[str | ~typing.Type] | None = None, injected_fields: ~typing.List[str] | None = None, imports: ~typing.List[~linkml.generators.pydanticgen.template.Import] | None = None, gen_classvars: bool = True, gen_slots: bool = True, genmeta: bool = False, emit_metadata: bool = True, **_kwargs)[source]#

Generates Pydantic-compliant classes from a schema

This is an alternative to the dataclasses-based Pythongen

black: bool = False#

If black is present in the environment, format the serialized code with it

compile_module(**kwargs) module[source]#

Compiles generated python code to a module :return:

generate_collection_key(slot_ranges: List[str], slot_def: SlotDefinition, class_def: ClassDefinition) str | None[source]#

Find the python range value (str, int, etc) for the identifier slot of a class used as a slot range.

If a pyrange value matches a class name, the range of the identifier slot will be returned. If more than one match is found and they don’t match, an exception will be raised.

Parameters:

slot_ranges – list of python range values

generate_python_range(slot_range, slot_def: SlotDefinition, class_def: ClassDefinition) str[source]#

Generate the python range for a slot range value

generatorname: ClassVar[str] = 'pydanticgen.py'#

Name of the generator. Override with os.path.basename(__file__)

generatorversion: ClassVar[str] = '0.0.2'#

Version of the generator. Consider deprecating and instead use overall linkml version

get_array_representations_range(slot: SlotDefinition, range: str) List[SlotResult][source]#

Generate the python range for array representations

get_class_isa_plus_mixins() Dict[str, List[str]][source]#

Generate the inheritance list for each class from is_a plus mixins :return:

get_predefined_slot_values() Dict[str, Dict[str, str]][source]#
Returns:

Dictionary of dictionaries with predefined slot values for each class

imports: List[Import] | None = None#

Additional imports to inject into generated module.

Examples:

from linkml.generators.pydanticgen.template import (
    ConditionalImport,
    ObjectImport,
    Import,
    Imports
)

imports = (Imports() +
    Import(module='sys') +
    Import(module='numpy', alias='np') +
    Import(module='pathlib', objects=[
        ObjectImport(name="Path"),
        ObjectImport(name="PurePath", alias="RenamedPurePath")
    ]) +
    ConditionalImport(
        module="typing",
        objects=[ObjectImport(name="Literal")],
        condition="sys.version_info >= (3, 8)",
        alternative=Import(
            module="typing_extensions",
            objects=[ObjectImport(name="Literal")]
        ),
    ).imports
)

becomes:

import sys
import numpy as np
from pathlib import (
    Path,
    PurePath as RenamedPurePath
)
if sys.version_info >= (3, 8):
    from typing import Literal
else:
    from typing_extensions import Literal
injected_classes: List[str | Type] | None = None#

A list/tuple of classes to inject into the generated module.

Accepts either live classes or strings. Live classes will have their source code extracted with inspect.get - so they need to be standard python classes declared in a source file (ie. the module they are contained in needs a __file__ attr, see: inspect.getsource() )

injected_fields: List[str] | None = None#

A list/tuple of field strings to inject into the base class.

Examples:

injected_fields = (
    'object_id: Optional[str] = Field(None, description="Unique UUID for each object")',
)
range_class_has_identifier_slot(slot)[source]#

Check if the range class of a slot has an identifier slot, via both slot.any_of and slot.range Should return False if the range is not a class, and also if the range is a class but has no identifier slot

Parameters:

slot – SlotDefinition

Returns:

bool

serialize() str[source]#

Generate output in the required format

Parameters:

kwargs – Generator specific parameters

Returns:

Generated output

static sort_classes(clist: List[ClassDefinition]) List[ClassDefinition][source]#

sort classes such that if C is a child of P then C appears after P in the list

Overridden method include mixin classes

TODO: This should move to SchemaView

template_dir: str | Path | None = None#

Override templates for each TemplateModel.

Directory with templates that override the default TemplateModel.template for each class. If a matching template is not found in the override directory, the default templates will be used.

valid_formats: ClassVar[List[str]] = ['pydantic']#

Allowed formats - first format is default

Templates#

The pydanticgen module has a templating system that allows each part of a schema to be generated independently and customized. See the documentation for the individual classes, but in short - each part of the output pydantic domain has a model with a corresponding template. At render time, each model is recursively rendered.

The PydanticGenerator then serves as a translation layer between the source models from linkml_runtime and the target models in pydanticgen.template , making clear what is needed to generate pydantic code as well as what parts of the linkml metamodel are supported.

Usage example:

Imports:

imports = (Imports() +
    Import(module="sys") +
    Import(module="pydantic", objects=[{"name": "BaseModel"}, {"name": "Field"}])
)

renders to:

import sys
from pydantic import (
    BaseModel,
    Field
)

Attributes:

attr = PydanticAttribute(
    name="my_field",
    annotations={"python_range": {"value": "str"}},
    title="My Field!",
    description="A Field that is mine!",
    pattern="my_.*",
)

By itself, renders to:

my_field: str = Field(None, title="My Field!", description="""A Field that is mine!""")

Classes:

cls = PydanticClass(
    name="MyClass",
    bases="BaseModel",
    description="A Class I Made!",
    attributes={"my_field": attr},
)

Renders to (along with the validator for the attribute):

class MyClass(BaseModel):
    my_field: str = Field(None, title="My Field!", description="""A Field that is mine!""")

    @validator('my_field', allow_reuse=True)
    def pattern_my_field(cls, v):
        pattern=re.compile(r"my_.*")
        if isinstance(v,list):
            for element in v:
                if not pattern.match(element):
                    raise ValueError(f"Invalid my_field format: {element}")
        elif isinstance(v,str):
            if not pattern.match(v):
                raise ValueError(f"Invalid my_field format: {v}")
        return v

Modules:

module = PydanticModule(imports=imports, classes={cls.name: cls})

Combine all the pieces:

import sys
from pydantic import (
    BaseModel,
    Field
)

metamodel_version = "None"
version = "None"

class WeakRefShimBaseModel(BaseModel):
    __slots__ = '__weakref__'


class ConfiguredBaseModel(WeakRefShimBaseModel,
                validate_assignment = True,
                validate_all = True,
                underscore_attrs_are_private = True,
                extra = "forbid",
                arbitrary_types_allowed = True,
                use_enum_values = True):
    pass


class MyClass(BaseModel):
    my_field: str = Field(None, title="My Field!", description="""A Field that is mine!""")

    @validator('my_field', allow_reuse=True)
    def pattern_my_field(cls, v):
        pattern=re.compile(r"my_.*")
        if isinstance(v,list):
            for element in v:
                if not pattern.match(element):
                    raise ValueError(f"Invalid my_field format: {element}")
        elif isinstance(v,str):
            if not pattern.match(v):
                raise ValueError(f"Invalid my_field format: {v}")
        return v


# Update forward refs
# see https://pydantic-docs.helpmanual.io/usage/postponed_annotations/
MyClass.update_forward_refs()
class linkml.generators.pydanticgen.template.TemplateModel(*, pydantic_ver: int = 2)[source]#

Metaclass to render pydantic models with jinja templates.

Each subclass needs to declare a typing.ClassVar for a jinja template within the templates directory.

Templates are written expecting each of the other TemplateModels to already be rendered to strings - ie. rather than the class.py.jinja template receiving a full PydanticAttribute object or dictionary, it receives it having already been rendered to a string. See the render() method.

Black Formatting

Template models will try to use black to format results when it is available in the environment when render is called with black = True . If it isn’t, then the string is returned without any formatting beyond the template. This is mostly important for complex annotations like those produced for arrays, as otherwise the templates are acceptable looking.

To install linkml with black, use the extra black dependency.

e.g. with pip:

pip install linkml[black]

or with poetry:

poetry install -E black
template: ClassVar[str]#
pydantic_ver: int#
render(environment: Environment | None = None, black: bool = False) str[source]#

Recursively render a template model to a string.

For each field in the model, recurse through, rendering each TemplateModel using the template set in TemplateModel.template , but preserving the structure of lists and dictionaries. Regular BaseModel s are rendered to dictionaries. Any other value is passed through unchanged.

Parameters:
classmethod environment() Environment[source]#

Default environment for Template models. uses a jinja2.PackageLoader for the templates directory within this module with the trim_blocks and lstrip_blocks parameters set to True so that the default templates could be written in a more readable way.

model_fields: ClassVar[Dict[str, 'ModelField']] = {'pydantic_ver': FieldInfo(annotation=int, required=False, default=2)}#

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo].

This replaces Model.__fields__ from Pydantic V1.

model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] = {}#

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

model_config: ClassVar[ConfigDict] = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class linkml.generators.pydanticgen.template.EnumValue(*, label: str, value: str, description: str | None = None)[source]#

A single value within an Enum

label: str#
value: str#
description: str | None#
model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] = {}#

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

model_config: ClassVar[ConfigDict] = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[dict[str, FieldInfo]] = {'description': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'label': FieldInfo(annotation=str, required=True), 'value': FieldInfo(annotation=str, required=True)}#

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo].

This replaces Model.__fields__ from Pydantic V1.

class linkml.generators.pydanticgen.template.PydanticEnum(*, pydantic_ver: int = 2, name: str, description: str | None = None, values: Dict[str, EnumValue] = None)[source]#

Model used to render a enum.Enum

template: ClassVar[str] = 'enum.py.jinja'#
name: str#
description: str | None#
values: Dict[str, EnumValue]#
model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] = {}#

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

model_config: ClassVar[ConfigDict] = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, 'ModelField']] = {'description': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'name': FieldInfo(annotation=str, required=True), 'pydantic_ver': FieldInfo(annotation=int, required=False, default=2), 'values': FieldInfo(annotation=Dict[str, linkml.generators.pydanticgen.template.EnumValue], required=False, default_factory=dict)}#

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo].

This replaces Model.__fields__ from Pydantic V1.

class linkml.generators.pydanticgen.template.PydanticBaseModel(*, pydantic_ver: int = 2, name: str = None, extra_fields: Literal['allow', 'forbid', 'ignore'] = 'forbid', fields: List[str] | None = None, strict: bool = False)[source]#

Parameterization of the base model that generated pydantic classes inherit from

template: ClassVar[str] = 'base_model.py.jinja'#
default_name: ClassVar[str] = 'ConfiguredBaseModel'#
name: str#
extra_fields: Literal['allow', 'forbid', 'ignore']#

Sets the extra model for pydantic models

fields: List[str] | None#

Extra fields that are typically injected into the base model via injected_fields

strict: bool#

Enable strict mode in the base model.

Note

Pydantic 2 only! Pydantic 1 only has strict types, not strict mode. See: https://github.com/linkml/linkml/issues/1955

References

https://docs.pydantic.dev/latest/concepts/strict_mode

model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] = {}#

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

model_config: ClassVar[ConfigDict] = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, 'ModelField']] = {'extra_fields': FieldInfo(annotation=Literal['allow', 'forbid', 'ignore'], required=False, default='forbid'), 'fields': FieldInfo(annotation=Union[List[str], NoneType], required=False, default=None), 'name': FieldInfo(annotation=str, required=False, default_factory=<lambda>), 'pydantic_ver': FieldInfo(annotation=int, required=False, default=2), 'strict': FieldInfo(annotation=bool, required=False, default=False)}#

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo].

This replaces Model.__fields__ from Pydantic V1.

class linkml.generators.pydanticgen.template.PydanticAttribute(*, pydantic_ver: int = 2, name: str, required: bool = False, identifier: bool = False, key: bool = False, predefined: str | None = None, annotations: dict | None = None, title: str | None = None, description: str | None = None, equals_number: int | float | None = None, minimum_value: int | float | None = None, maximum_value: int | float | None = None, pattern: str | None = None)[source]#

Reduced version of SlotDefinition that carries all and only the information needed by the template

template: ClassVar[str] = 'attribute.py.jinja'#
name: str#
required: bool#
identifier: bool#
key: bool#
predefined: str | None#

Fixed string to use in body of field

annotations: dict | None#

Of the form:

annotations = {'python_range': {'value': 'int'}}
title: str | None#
description: str | None#
equals_number: int | float | None#
minimum_value: int | float | None#
maximum_value: int | float | None#
pattern: str | None#
property field: str#

Computed value to use inside of the generated Field

model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] = {'field': ComputedFieldInfo(wrapped_property=<property object>, return_type=<class 'str'>, alias=None, alias_priority=None, title=None, description='Computed value to use inside of the generated Field', deprecated=None, examples=None, json_schema_extra=None, repr=True)}#

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

model_config: ClassVar[ConfigDict] = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, 'ModelField']] = {'annotations': FieldInfo(annotation=Union[dict, NoneType], required=False, default=None), 'description': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'equals_number': FieldInfo(annotation=Union[int, float, NoneType], required=False, default=None), 'identifier': FieldInfo(annotation=bool, required=False, default=False), 'key': FieldInfo(annotation=bool, required=False, default=False), 'maximum_value': FieldInfo(annotation=Union[int, float, NoneType], required=False, default=None), 'minimum_value': FieldInfo(annotation=Union[int, float, NoneType], required=False, default=None), 'name': FieldInfo(annotation=str, required=True), 'pattern': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'predefined': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'pydantic_ver': FieldInfo(annotation=int, required=False, default=2), 'required': FieldInfo(annotation=bool, required=False, default=False), 'title': FieldInfo(annotation=Union[str, NoneType], required=False, default=None)}#

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo].

This replaces Model.__fields__ from Pydantic V1.

class linkml.generators.pydanticgen.template.PydanticValidator(*, pydantic_ver: int = 2, name: str, required: bool = False, identifier: bool = False, key: bool = False, predefined: str | None = None, annotations: dict | None = None, title: str | None = None, description: str | None = None, equals_number: int | float | None = None, minimum_value: int | float | None = None, maximum_value: int | float | None = None, pattern: str | None = None)[source]#

Trivial subclass of PydanticAttribute that uses the validator.py.jinja template instead

template: ClassVar[str] = 'validator.py.jinja'#
model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] = {'field': ComputedFieldInfo(wrapped_property=<property object>, return_type=<class 'str'>, alias=None, alias_priority=None, title=None, description='Computed value to use inside of the generated Field', deprecated=None, examples=None, json_schema_extra=None, repr=True)}#

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

model_config: ClassVar[ConfigDict] = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, 'ModelField']] = {'annotations': FieldInfo(annotation=Union[dict, NoneType], required=False, default=None), 'description': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'equals_number': FieldInfo(annotation=Union[int, float, NoneType], required=False, default=None), 'identifier': FieldInfo(annotation=bool, required=False, default=False), 'key': FieldInfo(annotation=bool, required=False, default=False), 'maximum_value': FieldInfo(annotation=Union[int, float, NoneType], required=False, default=None), 'minimum_value': FieldInfo(annotation=Union[int, float, NoneType], required=False, default=None), 'name': FieldInfo(annotation=str, required=True), 'pattern': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'predefined': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'pydantic_ver': FieldInfo(annotation=int, required=False, default=2), 'required': FieldInfo(annotation=bool, required=False, default=False), 'title': FieldInfo(annotation=Union[str, NoneType], required=False, default=None)}#

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo].

This replaces Model.__fields__ from Pydantic V1.

class linkml.generators.pydanticgen.template.PydanticClass(*, pydantic_ver: int = 2, name: str, bases: List[str] | str = 'ConfiguredBaseModel', description: str | None = None, attributes: Dict[str, PydanticAttribute] | None = None)[source]#

Reduced version of ClassDefinition that carries all and only the information needed by the template.

On instantiation and rendering, will create any additional validators that are implied by the given attributes. Currently the only kind of slot-level validators that are created are for those slots that have a pattern property.

template: ClassVar[str] = 'class.py.jinja'#
name: str#
bases: List[str] | str#
description: str | None#
attributes: Dict[str, PydanticAttribute] | None#
property validators: Dict[str, PydanticValidator] | None#
model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] = {'validators': ComputedFieldInfo(wrapped_property=<property object>, return_type=typing.Optional[typing.Dict[str, linkml.generators.pydanticgen.template.PydanticValidator]], alias=None, alias_priority=None, title=None, description=None, deprecated=None, examples=None, json_schema_extra=None, repr=True)}#

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

model_config: ClassVar[ConfigDict] = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, 'ModelField']] = {'attributes': FieldInfo(annotation=Union[Dict[str, linkml.generators.pydanticgen.template.PydanticAttribute], NoneType], required=False, default=None), 'bases': FieldInfo(annotation=Union[List[str], str], required=False, default='ConfiguredBaseModel'), 'description': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'name': FieldInfo(annotation=str, required=True), 'pydantic_ver': FieldInfo(annotation=int, required=False, default=2)}#

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo].

This replaces Model.__fields__ from Pydantic V1.

class linkml.generators.pydanticgen.template.ObjectImport(*, name: str, alias: str | None = None)[source]#

An object to be imported from within a module.

See Import for examples

name: str#
alias: str | None#
model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] = {}#

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

model_config: ClassVar[ConfigDict] = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[dict[str, FieldInfo]] = {'alias': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'name': FieldInfo(annotation=str, required=True)}#

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo].

This replaces Model.__fields__ from Pydantic V1.

class linkml.generators.pydanticgen.template.Import(*, pydantic_ver: int = 2, module: str, alias: str | None = None, objects: List[ObjectImport] | None = None)[source]#

A python module, or module and classes to be imported.

Examples

Module import:

>>> Import(module='sys').render()
import sys
>>> Import(module='numpy', alias='np').render()
import numpy as np

Class import:

>>> Import(module='pathlib', objects=[
>>>     ObjectImport(name="Path"),
>>>     ObjectImport(name="PurePath", alias="RenamedPurePath")
>>> ]).render()
from pathlib import (
    Path,
    PurePath as RenamedPurePath
)
template: ClassVar[str] = 'imports.py.jinja'#
module: str#
alias: str | None#
objects: List[ObjectImport] | None#
merge(other: Import) List[Import][source]#

Merge one import with another, see Imports() for an example.

  • If module don’t match, return both

  • If one or the other are a ConditionalImport, return both

  • If modules match, neither contain objects, but the other has an alias, return the other

  • If modules match, one contains objects but the other doesn’t, return both

  • If modules match, both contain objects, merge the object lists, preferring objects with aliases

model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] = {}#

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

model_config: ClassVar[ConfigDict] = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, 'ModelField']] = {'alias': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'module': FieldInfo(annotation=str, required=True), 'objects': FieldInfo(annotation=Union[List[linkml.generators.pydanticgen.template.ObjectImport], NoneType], required=False, default=None), 'pydantic_ver': FieldInfo(annotation=int, required=False, default=2)}#

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo].

This replaces Model.__fields__ from Pydantic V1.

class linkml.generators.pydanticgen.template.ConditionalImport(*, pydantic_ver: int = 2, module: str, alias: str | None = None, objects: List[ObjectImport] | None = None, condition: str, alternative: Import)[source]#

Import that depends on some condition in the environment, common when using backported features or straddling dependency versions.

Make sure that everything that is needed to evaluate the condition is imported before this is added to the injected imports!

Examples

conditionally import Literal from typing_extensions if on python <= 3.8

imports = (Imports() +
     Import(module='sys') +
     ConditionalImport(
     module="typing",
     objects=[ObjectImport(name="Literal")],
     condition="sys.version_info >= (3, 8)",
     alternative=Import(
         module="typing_extensions",
         objects=[ObjectImport(name="Literal")]
     )
 )

Renders to:

import sys
if sys.version_info >= (3, 8):
    from typing import Literal
else:
    from typing_extensions import Literal
template: ClassVar[str] = 'conditional_import.py.jinja'#
condition: str#
alternative: Import#
model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] = {}#

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

model_config: ClassVar[ConfigDict] = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, 'ModelField']] = {'alias': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'alternative': FieldInfo(annotation=Import, required=True), 'condition': FieldInfo(annotation=str, required=True), 'module': FieldInfo(annotation=str, required=True), 'objects': FieldInfo(annotation=Union[List[linkml.generators.pydanticgen.template.ObjectImport], NoneType], required=False, default=None), 'pydantic_ver': FieldInfo(annotation=int, required=False, default=2)}#

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo].

This replaces Model.__fields__ from Pydantic V1.

class linkml.generators.pydanticgen.template.Imports(*, pydantic_ver: int = 2, imports: List[Import | ConditionalImport] = None)[source]#

Container class for imports that can handle merging!

See Import and ConditionalImport for examples of declaring individual imports

Useful for generation, because each build stage will potentially generate overlapping imports. This ensures that we can keep a collection of imports without having many duplicates.

Defines methods for adding, iterating, and indexing from within the Imports.imports list.

Examples

imports = (Imports() +
    Import(module="sys") +
    Import(module="pathlib", objects=[ObjectImport(name="Path")]) +
    Import(module="sys")
)

Renders to:

from pathlib import Path
import sys
template: ClassVar[str] = 'imports.py.jinja'#
imports: List[Import | ConditionalImport]#
model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] = {}#

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

model_config: ClassVar[ConfigDict] = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, 'ModelField']] = {'imports': FieldInfo(annotation=List[Union[linkml.generators.pydanticgen.template.Import, linkml.generators.pydanticgen.template.ConditionalImport]], required=False, default_factory=list), 'pydantic_ver': FieldInfo(annotation=int, required=False, default=2)}#

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo].

This replaces Model.__fields__ from Pydantic V1.

class linkml.generators.pydanticgen.template.PydanticModule(*, pydantic_ver: int = 2, metamodel_version: str | None = None, version: str | None = None, base_model: PydanticBaseModel = PydanticBaseModel(pydantic_ver=2, name='ConfiguredBaseModel', extra_fields='forbid', fields=None, strict=False), injected_classes: List[str] | None = None, imports: List[Import | ConditionalImport] = None, enums: Dict[str, PydanticEnum] = None, classes: Dict[str, PydanticClass] = None)[source]#

Top-level container model for generating a pydantic module :)

model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] = {'class_names': ComputedFieldInfo(wrapped_property=<property object>, return_type=typing.List[str], alias=None, alias_priority=None, title=None, description=None, deprecated=None, examples=None, json_schema_extra=None, repr=True)}#

A dictionary of computed field names and their corresponding ComputedFieldInfo objects.

model_config: ClassVar[ConfigDict] = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_fields: ClassVar[Dict[str, 'ModelField']] = {'base_model': FieldInfo(annotation=PydanticBaseModel, required=False, default=PydanticBaseModel(pydantic_ver=2, name='ConfiguredBaseModel', extra_fields='forbid', fields=None, strict=False)), 'classes': FieldInfo(annotation=Dict[str, linkml.generators.pydanticgen.template.PydanticClass], required=False, default_factory=dict), 'enums': FieldInfo(annotation=Dict[str, linkml.generators.pydanticgen.template.PydanticEnum], required=False, default_factory=dict), 'imports': FieldInfo(annotation=List[Union[linkml.generators.pydanticgen.template.Import, linkml.generators.pydanticgen.template.ConditionalImport]], required=False, default_factory=list), 'injected_classes': FieldInfo(annotation=Union[List[str], NoneType], required=False, default=None), 'metamodel_version': FieldInfo(annotation=Union[str, NoneType], required=False, default=None), 'pydantic_ver': FieldInfo(annotation=int, required=False, default=2), 'version': FieldInfo(annotation=Union[str, NoneType], required=False, default=None)}#

Metadata about the fields defined on the model, mapping of field names to [FieldInfo][pydantic.fields.FieldInfo].

This replaces Model.__fields__ from Pydantic V1.

template: ClassVar[str] = 'module.py.jinja'#
metamodel_version: str | None#
version: str | None#
base_model: PydanticBaseModel#
injected_classes: List[str] | None#
imports: List[Import | ConditionalImport]#
enums: Dict[str, PydanticEnum]#
classes: Dict[str, PydanticClass]#
property class_names: List[str]#

Arrays#

TODO

Narrative documentation for pydantic LoL Arrays. Subsection this by different array reps

See Schemas/Arrays

class linkml.generators.pydanticgen.array.ArrayRepresentation(value)[source]#

An enumeration.

class linkml.generators.pydanticgen.array.AnyShapeArrayType[source]#
class linkml.generators.pydanticgen.array.ArrayRangeGenerator(array: ArrayExpression | None, dtype: str | Element, pydantic_ver: str = '2.7.0')[source]#

Metaclass for generating a given format of array annotation.

See Shape Forms for more details on array range forms.

These classes do only enough validation of the array specification to decide which kind of representation to generate. Proper value validation should happen elsewhere (ie. in the metamodel and generated ArrayExpression class.)

Each of the array representation generation methods should be able to handle the supported pydantic versions (currently still 1 and 2).

Notes

When checking for array specification, recall that there is a semantic difference between None and False , particularly for ArrayExpression.max_number_dimensions - check for absence of specification with is None rather than checking for truthiness/falsiness (unless that’s what you intend to do ofc ;)

array#

Array to create an annotation for

Type:

ArrayExpression

dtype#

dtype of the entire array as a string

Type:

Union[str, Element

pydantic_ver#

Pydantic version to generate array form for - currently only pydantic 1 and 2 are differentiated, and pydantic 1 will be deprecated soon.

Type:

str

make() SlotResult[source]#

Create the string form of the array representation

property has_bounded_dimensions: bool#

Whether the ArrayExpression has some shape specification aside from dimensions

classmethod get_generator(repr: ArrayRepresentation) Type[ArrayRangeGenerator][source]#

Get the generator class for a given array representation

abstract any_shape(array: ArrayRepresentation | None = None) SlotResult[source]#

Any shaped array!

abstract bounded_dimensions(array: ArrayExpression) SlotResult[source]#

Array shape specified numerically, without axis parameterization

abstract parameterized_dimensions(array: ArrayExpression) SlotResult[source]#

Array shape specified with dimensions without additional parameterized dimensions

abstract complex_dimensions(array: ArrayExpression) SlotResult[source]#

Array shape with both parameterized and bounded dimensions

class linkml.generators.pydanticgen.array.ListOfListsArray(array: ArrayExpression | None, dtype: str | Element, pydantic_ver: str = '2.7.0')[source]#

Represent arrays as lists of lists!

TODO: Move all validation of values (eg. anywhere we raise a ValueError) to the ArrayExpression dataclass and out of the generator class

any_shape(array: ArrayExpression | None = None, with_inner_union: bool = False) SlotResult[source]#

An AnyShaped array (using AnyShapeArray )

Parameters:
  • array (ArrayExpression) – The array expression (not used)

  • with_inner_union (bool) – If True , the innermost type is a Union of the AnyShapeArray class and dtype (default: False )

bounded_dimensions(array: ArrayExpression) SlotResult[source]#

A nested series of List[] annotations with dtype at the center.

When an array expression allows for a range of dimensions, each set of List s is joined by a Union .

parameterized_dimensions(array: ArrayExpression) SlotResult[source]#

Constrained shapes using pydantic.conlist()

TODO: - preservation of aliases - (what other metadata is allowable on labeled dimensions?)

complex_dimensions(array: ArrayExpression) SlotResult[source]#

Mixture of parameterized dimensions with a max or min (or both) shape for anonymous dimensions.

A mixture of List , conlist , and AnyShapeArray .

class linkml.generators.pydanticgen.array.NPTypingArray(**kwargs)[source]#

Represent array range with nptyping, and serialization/loading with an ArrayProxy

Additional Notes#

LinkML contains two Python generators. The Pydantic dataclass generator is specifically useful for FastAPI, but is newer and less full featured than the standard Python generator.