Repository: linkml/linkml

Issue:
Add pattern/regexp validation for pydanticgen
pydantic supports regexes/patterns:

https://docs.pydantic.dev/latest/concepts/fields/#string-constraints

We should generate this in pydanticgen

Should this be optional, to avoid potentially expensive checks at time of object generation?

This issue should be closed by removing PYDANTICGEN from:

https://github.com/linkml/linkml/blob/5c0dfa25f65b7170768b52c94f67ec609afeb745/tests/test_compliance/test_pattern_compliance.py#L57-L59

Comments:

Patch:
diff --git a/linkml/generators/pydanticgen.py b/linkml/generators/pydanticgen.py
index c9ea5d6d..b933db57 100644
--- a/linkml/generators/pydanticgen.py
+++ b/linkml/generators/pydanticgen.py
@@ -108,7 +108,46 @@ class {{ e.name }}(str{% if e['values'] %}, Enum{% endif %}):
 {% endfor %}
 """
     ### CLASSES ###
-    template += """
+    if pydantic_ver == "1":
+        template += """
+{%- for c in schema.classes.values() %}
+class {{ c.name }}
+    {%- if class_isa_plus_mixins[c.name] -%}
+        ({{class_isa_plus_mixins[c.name]|join(', ')}})
+    {%- else -%}
+        (ConfiguredBaseModel)
+    {%- endif -%}
+                  :
+    {% if c.description -%}
+    \"\"\"
+    {{ c.description }}
+    \"\"\"
+    {%- endif %}
+    {% for attr in c.attributes.values() if c.attributes -%}
+    {{attr.name}}: {{ attr.annotations['python_range'].value }} = Field(
+    {%- if predefined_slot_values[c.name][attr.name] -%}
+        {{ predefined_slot_values[c.name][attr.name] }}
+    {%- elif (attr.required or attr.identifier or attr.key) -%}
+        ...
+    {%- else -%}
+        None
+    {%- endif -%}
+    {%- if attr.title != None %}, title="{{attr.title}}"{% endif -%}
+    {%- if attr.description %}, description=\"\"\"{{attr.description}}\"\"\"{% endif -%}
+    {%- if attr.pattern %}, regex=\"{{attr.pattern}}\"{% endif -%}
+    {%- if attr.equals_number != None %}, le={{attr.equals_number}}, ge={{attr.equals_number}}
+    {%- else -%}
+     {%- if attr.minimum_value != None %}, ge={{attr.minimum_value}}{% endif -%}
+     {%- if attr.maximum_value != None %}, le={{attr.maximum_value}}{% endif -%}
+    {%- endif -%}
+    )
+    {% else -%}
+    None
+    {% endfor %}
+{% endfor %}
+"""
+    elif pydantic_ver == "2":
+        template += """
 {%- for c in schema.classes.values() %}
 class {{ c.name }}
     {%- if class_isa_plus_mixins[c.name] -%}
@@ -133,6 +172,7 @@ class {{ c.name }}
     {%- endif -%}
     {%- if attr.title != None %}, title="{{attr.title}}"{% endif -%}
     {%- if attr.description %}, description=\"\"\"{{attr.description}}\"\"\"{% endif -%}
+    {%- if attr.pattern %}, pattern=\"{{attr.pattern}}\"{% endif -%}
     {%- if attr.equals_number != None %}, le={{attr.equals_number}}, ge={{attr.equals_number}}
     {%- else -%}
      {%- if attr.minimum_value != None %}, ge={{attr.minimum_value}}{% endif -%}
@@ -144,6 +184,7 @@ class {{ c.name }}
     {% endfor %}
 {% endfor %}
 """
+
     ### FWD REFS / REBUILD MODEL ###
     if pydantic_ver == "1":
         template += """


Test patch:
diff --git a/tests/test_generators/test_pydanticgen.py b/tests/test_generators/test_pydanticgen.py
index a7ea9b5e..b0f984ed 100644
--- a/tests/test_generators/test_pydanticgen.py
+++ b/tests/test_generators/test_pydanticgen.py
@@ -326,3 +326,14 @@ def test_multiline_module(input_path):
     )
 
     assert 'INTERNAL "REORGANIZATION"' in gen.schema.enums["EmploymentEventType"].permissible_values
+
+
+def test_pydantic_pattern(kitchen_sink_path, tmp_path, input_path):
+    """Generate pydantic classes"""
+    gen = PydanticGenerator(kitchen_sink_path, package=PACKAGE)
+    code = gen.serialize()
+    module = compile_python(code, PACKAGE)
+    p1 = module.Person(id="01", name="John Doe")
+    assert p1.name == "John Doe"
+    with pytest.raises(ValidationError):
+        module.Person(id="01", name="x")