linkml 1.8.4__py3-none-any.whl → 1.8.5__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.
@@ -1,7 +1,13 @@
1
1
  import abc
2
2
  import re
3
+ import sys
3
4
  from abc import ABC
4
- from typing import Any, Optional
5
+ from typing import Any, Optional, Type, Union
6
+
7
+ if sys.version_info < (3, 10):
8
+ from typing_extensions import TypeAlias
9
+ else:
10
+ from typing import TypeAlias
5
11
 
6
12
  from linkml_runtime import SchemaView
7
13
  from linkml_runtime.linkml_model import (
@@ -18,6 +24,7 @@ from linkml_runtime.linkml_model import (
18
24
  String,
19
25
  Time,
20
26
  Uri,
27
+ types,
21
28
  )
22
29
  from linkml_runtime.linkml_model.types import (
23
30
  Curie,
@@ -31,6 +38,34 @@ from linkml_runtime.linkml_model.types import (
31
38
  Uriorcurie,
32
39
  )
33
40
 
41
+ TYPES_TYPE: TypeAlias = Union[
42
+ Type[Boolean],
43
+ Type[Curie],
44
+ Type[Date],
45
+ Type[DateOrDatetime],
46
+ Type[Datetime],
47
+ Type[Decimal],
48
+ Type[Double],
49
+ Type[Float],
50
+ Type[Integer],
51
+ Type[Jsonpath],
52
+ Type[Jsonpointer],
53
+ Type[Ncname],
54
+ Type[Nodeidentifier],
55
+ Type[Objectidentifier],
56
+ Type[Sparqlpath],
57
+ Type[String],
58
+ Type[Time],
59
+ Type[Uri],
60
+ Type[Uriorcurie],
61
+ ]
62
+
63
+ TYPES = [
64
+ t
65
+ for t in types.__dict__.values()
66
+ if isinstance(t, type) and t.__module__ == types.__name__ and hasattr(t, "type_name")
67
+ ]
68
+
34
69
 
35
70
  class IfAbsentProcessor(ABC):
36
71
  """
@@ -39,7 +74,7 @@ class IfAbsentProcessor(ABC):
39
74
  See `<https://w3id.org/linkml/ifabsent>`_.
40
75
  """
41
76
 
42
- ifabsent_regex = re.compile("""(?:(?P<type>\w+)\()?[\"\']?(?P<default_value>[^\(\)\"\')]*)[\"\']?\)?""")
77
+ ifabsent_regex = re.compile(r"""(?:(?P<type>\w+)\()?[\"\']?(?P<default_value>[^\(\)\"\')]*)[\"\']?\)?""")
43
78
 
44
79
  def __init__(self, schema_view: SchemaView):
45
80
  self.schema_view = schema_view
@@ -61,10 +96,12 @@ class IfAbsentProcessor(ABC):
61
96
  if mapped:
62
97
  return custom_default_value
63
98
 
64
- if slot.range == String.type_name:
99
+ base_type = self._base_type(slot.range)
100
+
101
+ if base_type is String:
65
102
  return self.map_string_default_value(ifabsent_default_value, slot, cls)
66
103
 
67
- if slot.range == Boolean.type_name:
104
+ if base_type is Boolean:
68
105
  if re.match(r"^[Tt]rue$", ifabsent_default_value):
69
106
  return self.map_boolean_true_default_value(slot, cls)
70
107
  elif re.match(r"^[Ff]alse$", ifabsent_default_value):
@@ -75,19 +112,19 @@ class IfAbsentProcessor(ABC):
75
112
  f"value"
76
113
  )
77
114
 
78
- if slot.range == Integer.type_name:
115
+ if base_type is Integer:
79
116
  return self.map_integer_default_value(ifabsent_default_value, slot, cls)
80
117
 
81
- if slot.range == Float.type_name:
118
+ if base_type is Float:
82
119
  return self.map_float_default_value(ifabsent_default_value, slot, cls)
83
120
 
84
- if slot.range == Double.type_name:
121
+ if base_type is Double:
85
122
  return self.map_double_default_value(ifabsent_default_value, slot, cls)
86
123
 
87
- if slot.range == Decimal.type_name:
124
+ if base_type is Decimal:
88
125
  return self.map_decimal_default_value(ifabsent_default_value, slot, cls)
89
126
 
90
- if slot.range == Time.type_name:
127
+ if base_type is Time:
91
128
  match = re.match(r"^(\d{2}):(\d{2}):(\d{2}).*$", ifabsent_default_value)
92
129
  if match:
93
130
  return self.map_time_default_value(match[1], match[2], match[3], slot, cls)
@@ -97,7 +134,7 @@ class IfAbsentProcessor(ABC):
97
134
  )
98
135
 
99
136
  # TODO manage timezones and offsets
100
- if slot.range == Date.type_name:
137
+ if base_type is Date:
101
138
  match = re.match(r"^(\d{4})-(\d{2})-(\d{2})$", ifabsent_default_value)
102
139
  if match:
103
140
  return self.map_date_default_value(match[1], match[2], match[3], slot, cls)
@@ -107,7 +144,7 @@ class IfAbsentProcessor(ABC):
107
144
  )
108
145
 
109
146
  # TODO manage timezones and offsets
110
- if slot.range == Datetime.type_name:
147
+ if base_type is Datetime:
111
148
  match = re.match(r"^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).*$", ifabsent_default_value)
112
149
  if match:
113
150
  return self.map_datetime_default_value(
@@ -120,7 +157,7 @@ class IfAbsentProcessor(ABC):
120
157
  )
121
158
 
122
159
  # TODO manage timezones and offsets
123
- if slot.range == DateOrDatetime.type_name:
160
+ if base_type is DateOrDatetime:
124
161
  match = re.match(r"^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}):(\d{2}))?.*$", ifabsent_default_value)
125
162
  if match and (match[4] is None or match[5] is None or match[6] is None):
126
163
  return self.map_date_default_value(match[1], match[2], match[3], slot, cls)
@@ -134,31 +171,31 @@ class IfAbsentProcessor(ABC):
134
171
  f"datetime value"
135
172
  )
136
173
 
137
- if slot.range == Uri.type_name:
174
+ if base_type is Uri:
138
175
  return self.map_uri_default_value(ifabsent_default_value, slot, cls)
139
176
 
140
- if slot.range == Curie.type_name:
177
+ if base_type is Curie:
141
178
  return self.map_curie_default_value(ifabsent_default_value, slot, cls)
142
179
 
143
- if slot.range == Uriorcurie.type_name:
180
+ if base_type is Uriorcurie:
144
181
  return self.map_uri_or_curie_default_value(ifabsent_default_value, slot, cls)
145
182
 
146
- if slot.range == Ncname.type_name:
183
+ if base_type is Ncname:
147
184
  return self.map_nc_name_default_value(ifabsent_default_value, slot, cls)
148
185
 
149
- if slot.range == Objectidentifier.type_name:
186
+ if base_type is Objectidentifier:
150
187
  return self.map_object_identifier_default_value(ifabsent_default_value, slot, cls)
151
188
 
152
- if slot.range == Nodeidentifier.type_name:
189
+ if base_type is Nodeidentifier:
153
190
  return self.map_node_identifier_default_value(ifabsent_default_value, slot, cls)
154
191
 
155
- if slot.range == Jsonpointer.type_name:
192
+ if base_type is Jsonpointer:
156
193
  return self.map_json_pointer_default_value(ifabsent_default_value, slot, cls)
157
194
 
158
- if slot.range == Jsonpath.type_name:
195
+ if base_type is Jsonpath:
159
196
  return self.map_json_path_default_value(ifabsent_default_value, slot, cls)
160
197
 
161
- if slot.range == Sparqlpath.type_name:
198
+ if base_type is Sparqlpath:
162
199
  return self.map_sparql_path_default_value(ifabsent_default_value, slot, cls)
163
200
 
164
201
  # -----------------------
@@ -173,6 +210,46 @@ class IfAbsentProcessor(ABC):
173
210
 
174
211
  raise ValueError(f"The ifabsent value `{slot.ifabsent}` of the `{slot.name}` slot could not be processed")
175
212
 
213
+ def _base_type(self, range_: str) -> Optional[TYPES_TYPE]:
214
+ """
215
+ Find the linkml base type that corresponds to either a matching type_name or custom type
216
+
217
+ First check for an explicit match of the range == TypeDefinition.type_name
218
+ Then check for explicit inheritance via typeof
219
+ Finally check for implicit matching via matching base
220
+
221
+ Don't raise here, just return None in case another method of resolution like enums are
222
+ available
223
+ """
224
+ # first check for matching type using type_name - ie. range is already a base type
225
+
226
+ for typ in TYPES:
227
+ if range_ == typ.type_name:
228
+ return typ
229
+
230
+ # if we're not a type, return None to indicate that, e.g. if an enum's permissible_value
231
+ if range_ not in self.schema_view.all_types(imports=True):
232
+ return
233
+
234
+ # then check explicit descendents of types
235
+ # base types do not inherit from one another, so the last ancestor is always a base type
236
+ # if it is inheriting from a base type
237
+ ancestor = self.schema_view.type_ancestors(range_)[-1]
238
+ for typ in TYPES:
239
+ if ancestor == typ.type_name:
240
+ return typ
241
+
242
+ # finally check if we have a matching base
243
+ induced_typ = self.schema_view.induced_type(range_)
244
+ if induced_typ.repr is None and induced_typ.base is None:
245
+ return None
246
+ for typ in TYPES:
247
+ # types always inherit from a single type, and that type is their base
248
+ # our range can match it with repr or base
249
+ typ_base = typ.__mro__[1].__name__
250
+ if typ_base == induced_typ.base:
251
+ return typ
252
+
176
253
  @abc.abstractmethod
177
254
  def map_custom_default_values(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition) -> (bool, str):
178
255
  """
@@ -0,0 +1,106 @@
1
+ import logging
2
+ import re
3
+ import unicodedata
4
+ from enum import Enum
5
+
6
+
7
+ class NamingProfiles(str, Enum):
8
+ # GraphQL naming profile ensures compatibility with the GraphQL specification
9
+ # WRT names: https://spec.graphql.org/October2021/#Name
10
+ graphql = "graphql"
11
+
12
+
13
+ class NameCompatibility(object):
14
+ """Make a name compatible to the given profile"""
15
+
16
+ # heading double underscores and digit reserved to names starting with a digit
17
+ re_reserved_heading_digit = re.compile("^__[0-9]")
18
+ # valid name between double underscores is reserved for unicode name transformations
19
+ re_reserved_unicode_name_transformation = re.compile("__[0-9a-zA-Z][0-9a-zA-Z_]*__")
20
+ # something like '__U_xxxx_' is reserved for unicode code transformations
21
+ re_reserved_unicode_code_transformation = re.compile("__U_[0-9a-eA-E]{4}_")
22
+ # strings starting with a digit
23
+ re_heading_digit = re.compile("^[0-9]")
24
+ # character that is neither alphanumeric nor underscore
25
+ re_no_alphanum_underscore = re.compile("[^0-9a-zA-Z_]")
26
+
27
+ def __init__(self, profile: NamingProfiles, do_not_fix: bool = False):
28
+ """Specify the naming policy on instantiation"""
29
+ self.profile = profile
30
+ self.do_not_fix = do_not_fix
31
+
32
+ def _no_heading_digits(self, input: str) -> str:
33
+ """Ensure name does not start with a heading digit"""
34
+ output = input
35
+ if self.re_heading_digit.match(input):
36
+ if self.do_not_fix:
37
+ raise ValueError(f"Name '{input}' starts with digit (illegal GraphQL) and will not be fixed!")
38
+ else:
39
+ logging.warning(
40
+ f"Name '{input}' starts with digit (illegal GraphQL), "
41
+ + f"it has been prefixed with '__', resulting in {output}"
42
+ )
43
+ output = f"__{input}"
44
+ return output
45
+
46
+ def _transform_char(self, char: str) -> str:
47
+ """Transform unsupported characters"""
48
+ if len(char) != 1:
49
+ raise Exception(f"Single character expected, but got '{char}'!!")
50
+ # replace whitespaces with underscores
51
+ # the transformation cannot be inverted, but is a well-established
52
+ # and expected transformation
53
+ if char == " ":
54
+ return "_"
55
+ # try to use names for ASCII characters
56
+ if ord(char) < 128:
57
+ try:
58
+ # unicodedata.lookup should be able to invert the transformation
59
+ return f"__{unicodedata.name(char).replace(' ', '_').replace('-', '_')}__"
60
+ except ValueError:
61
+ pass
62
+ # fallback to code-transformation if none of the previous has worked
63
+ return f"__U_{ord(char):04X}_"
64
+
65
+ def _only_alphanum_underscore(self, input: str) -> str:
66
+ """Ensure name does not contain any unsupported characters"""
67
+ output = input
68
+ # with re.split and re.findall we get in the same order and separated in two arrays
69
+ # the substrings between special characters and the special characters
70
+ no_alphanum_underscore_match = self.re_no_alphanum_underscore.findall(input)
71
+ if no_alphanum_underscore_match:
72
+ if self.do_not_fix:
73
+ raise ValueError(f"Name '{input}' contains a character illegal in GraphQL and will not be fixed!")
74
+ else:
75
+ logging.warning(
76
+ f"Name '{input}' contains a character illegal in GraphQL, "
77
+ + f"the resulting encoded name is {output}"
78
+ )
79
+ to_keep = self.re_no_alphanum_underscore.split(input)
80
+ # first comes first substring to keep
81
+ output = to_keep[0]
82
+ # each char replacement is followed by the next substring to keep
83
+ for offset in range(0, len(to_keep) - 1):
84
+ output = output + self._transform_char(no_alphanum_underscore_match[offset])
85
+ output = output + to_keep[offset + 1]
86
+ return output
87
+
88
+ def _graphql_compatibility(self, input: str) -> str:
89
+ """Ensure name compatibility with GraphQL name policies"""
90
+ # as of now, some (hopefully) very rare patterns are reserved to mark transformations
91
+ if self.re_reserved_heading_digit.match(input):
92
+ raise NotImplementedError("Names starting with a double underscore followed by a digit are not supported!!")
93
+ if self.re_reserved_unicode_name_transformation.match(input):
94
+ raise NotImplementedError("Names containing valid names between double underscores are not supported!!")
95
+ if self.re_reserved_unicode_code_transformation.match(input):
96
+ raise NotImplementedError("Names containing strings like '__U_xxxx_' are not supported!!")
97
+ # apply transformation
98
+ output = input
99
+ output = self._no_heading_digits(output)
100
+ output = self._only_alphanum_underscore(output)
101
+ return output
102
+
103
+ def compatible(self, input: str) -> str:
104
+ """Make given name compatible with the given naming policy."""
105
+ if self.profile == "graphql":
106
+ return self._graphql_compatibility(input)
@@ -164,6 +164,7 @@ class DocGenerator(Generator):
164
164
  use_slot_uris: bool = False
165
165
  use_class_uris: bool = False
166
166
  hierarchical_class_view: bool = False
167
+ render_imports: bool = False
167
168
 
168
169
  def __post_init__(self):
169
170
  dialect = self.dialect
@@ -759,7 +760,7 @@ class DocGenerator(Generator):
759
760
  Ensures rank is non-null
760
761
  :return: iterator
761
762
  """
762
- elts = self.schemaview.all_classes(imports=self.mergeimports).values()
763
+ elts = self.schemaview.all_classes(imports=self.render_imports).values()
763
764
  _ensure_ranked(elts)
764
765
  for e in elts:
765
766
  yield e
@@ -771,7 +772,7 @@ class DocGenerator(Generator):
771
772
  Ensures rank is non-null
772
773
  :return: iterator
773
774
  """
774
- elts = self.schemaview.all_slots(imports=self.mergeimports).values()
775
+ elts = self.schemaview.all_slots(imports=self.render_imports).values()
775
776
  _ensure_ranked(elts)
776
777
  for e in elts:
777
778
  yield e
@@ -783,7 +784,7 @@ class DocGenerator(Generator):
783
784
  Ensures rank is non-null
784
785
  :return: iterator
785
786
  """
786
- elts = self.schemaview.all_types(imports=self.mergeimports).values()
787
+ elts = self.schemaview.all_types(imports=self.render_imports).values()
787
788
  _ensure_ranked(elts)
788
789
  for e in elts:
789
790
  yield e
@@ -798,7 +799,7 @@ class DocGenerator(Generator):
798
799
  Ensures rank is non-null
799
800
  :return: iterator
800
801
  """
801
- elts = self.schemaview.all_enums(imports=self.mergeimports).values()
802
+ elts = self.schemaview.all_enums(imports=self.render_imports).values()
802
803
  _ensure_ranked(elts)
803
804
  for e in elts:
804
805
  yield e
@@ -810,7 +811,7 @@ class DocGenerator(Generator):
810
811
  Ensures rank is non-null
811
812
  :return: iterator
812
813
  """
813
- elts = self.schemaview.all_subsets(imports=self.mergeimports).values()
814
+ elts = self.schemaview.all_subsets(imports=self.render_imports).values()
814
815
  _ensure_ranked(elts)
815
816
  for e in elts:
816
817
  yield e
@@ -834,7 +835,7 @@ class DocGenerator(Generator):
834
835
  :return: tuples (depth: int, cls: ClassDefinitionName)
835
836
  """
836
837
  sv = self.schemaview
837
- roots = sv.class_roots(mixins=False, imports=self.mergeimports)
838
+ roots = sv.class_roots(mixins=False, imports=self.render_imports)
838
839
 
839
840
  # by default the classes are sorted alphabetically
840
841
  roots = sorted(roots, key=str.casefold, reverse=True)
@@ -848,7 +849,7 @@ class DocGenerator(Generator):
848
849
  depth, class_name = stack.pop()
849
850
  yield depth, class_name
850
851
  children = sorted(
851
- sv.class_children(class_name=class_name, mixins=False, imports=self.mergeimports),
852
+ sv.class_children(class_name=class_name, mixins=False, imports=self.render_imports),
852
853
  key=str.casefold,
853
854
  reverse=True,
854
855
  )
@@ -1008,6 +1009,12 @@ class DocGenerator(Generator):
1008
1009
  default=True,
1009
1010
  help="Render class table on index page in a hierarchically indented view",
1010
1011
  )
1012
+ @click.option(
1013
+ "--render-imports/--no-render-imports",
1014
+ default=False,
1015
+ show_default=True,
1016
+ help="Render also the documentation of elements from imported schemas",
1017
+ )
1011
1018
  @click.option(
1012
1019
  "--example-directory",
1013
1020
  help="Folder in which example files are found. These are used to make inline examples",
@@ -1037,6 +1044,7 @@ def cli(
1037
1044
  use_class_uris,
1038
1045
  hierarchical_class_view,
1039
1046
  subfolder_type_separation,
1047
+ render_imports,
1040
1048
  **args,
1041
1049
  ):
1042
1050
  """Generate documentation folder from a LinkML YAML schema
@@ -1068,6 +1076,7 @@ def cli(
1068
1076
  hierarchical_class_view=hierarchical_class_view,
1069
1077
  index_name=index_name,
1070
1078
  subfolder_type_separation=subfolder_type_separation,
1079
+ render_imports=render_imports,
1071
1080
  **args,
1072
1081
  )
1073
1082
  print(gen.serialize())
@@ -1,11 +1,14 @@
1
+ import logging
1
2
  import os
3
+ import re
2
4
  from dataclasses import dataclass
3
5
 
4
6
  import click
5
- from linkml_runtime.linkml_model.meta import ClassDefinition, SlotDefinition
7
+ from linkml_runtime.linkml_model.meta import ClassDefinition, EnumDefinition, SlotDefinition
6
8
  from linkml_runtime.utils.formatutils import camelcase, lcamelcase
7
9
 
8
10
  from linkml._version import __version__
11
+ from linkml.generators.common.naming import NameCompatibility, NamingProfiles
9
12
  from linkml.utils.generator import Generator, shared_arguments
10
13
 
11
14
 
@@ -19,6 +22,13 @@ class GraphqlGenerator(Generator):
19
22
  uses_schemaloader = True
20
23
  requires_metamodel = False
21
24
 
25
+ strict_naming: bool = False
26
+ _permissible_value_valid_characters = re.compile("^[_A-Za-z][_0-9A-Za-z]*?$")
27
+
28
+ def __post_init__(self):
29
+ self.name_compatiblity = NameCompatibility(profile=NamingProfiles.graphql, do_not_fix=self.strict_naming)
30
+ super().__post_init__()
31
+
22
32
  def visit_schema(self, **kwargs) -> str:
23
33
  return self.generate_header()
24
34
 
@@ -50,13 +60,35 @@ class GraphqlGenerator(Generator):
50
60
  slotrange = slotrange + "!"
51
61
  return f"\n {lcamelcase(aliased_slot_name)}: {slotrange}"
52
62
 
63
+ def visit_enum(self, enum: EnumDefinition):
64
+ if enum.permissible_values:
65
+ permissible_values = []
66
+ for value in enum.permissible_values:
67
+ permissible_values.append(self.name_compatiblity.compatible(value))
68
+ values = "\n ".join(permissible_values)
69
+ return f"enum {camelcase(enum.name).replace(' ','')}\n {{\n {values}\n }}\n\n"
70
+ else:
71
+ logging.warning(
72
+ f"Enumeration {enum.name} using `reachable_from` instead of `permissible_values` "
73
+ + "to specify permissible values is not supported yet."
74
+ + "Enumeration {enum.name} will be silently ignored!!"
75
+ )
76
+ return ""
77
+
53
78
 
54
79
  @shared_arguments(GraphqlGenerator)
55
80
  @click.command(name="graphql")
81
+ @click.option(
82
+ "--strict-naming",
83
+ is_flag=True,
84
+ show_default=True,
85
+ help="Treat warnings about invalid names or schema elements as errors.",
86
+ )
56
87
  @click.version_option(__version__, "-V", "--version")
57
88
  def cli(yamlfile, **args):
58
89
  """Generate graphql representation of a LinkML model"""
59
- print(GraphqlGenerator(yamlfile, **args).serialize(**args))
90
+ generator = GraphqlGenerator(yamlfile, **args)
91
+ print(generator.serialize(**args))
60
92
 
61
93
 
62
94
  if __name__ == "__main__":
@@ -675,7 +675,18 @@ class PydanticModule(PydanticTemplateModel):
675
675
  return [c.name for c in self.classes.values()]
676
676
 
677
677
 
678
- _some_stdlib_module_names = {"copy", "datetime", "decimal", "enum", "inspect", "os", "re", "sys", "typing"}
678
+ _some_stdlib_module_names = {
679
+ "copy",
680
+ "datetime",
681
+ "decimal",
682
+ "enum",
683
+ "inspect",
684
+ "os",
685
+ "re",
686
+ "sys",
687
+ "typing",
688
+ "dataclasses",
689
+ }
679
690
  """
680
691
  sys.stdlib_module_names is only present in 3.10 and later
681
692
  so we make a cheap copy of the stdlib modules that we commonly use here,
@@ -71,7 +71,7 @@ class PythonIfAbsentProcessor(IfAbsentProcessor):
71
71
  def map_enum_default_value(
72
72
  self, enum_name: EnumDefinitionName, permissible_value_name: str, slot: SlotDefinition, cls: ClassDefinition
73
73
  ):
74
- return f"{enum_name}.{permissible_value_name}"
74
+ return f"'{permissible_value_name}'"
75
75
 
76
76
  def map_nc_name_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
77
77
  raise NotImplementedError()
@@ -29,6 +29,7 @@ from rdflib import URIRef
29
29
 
30
30
  import linkml
31
31
  from linkml._version import __version__
32
+ from linkml.generators.pydanticgen.template import Import, Imports, ObjectImport
32
33
  from linkml.generators.python.python_ifabsent_processor import PythonIfAbsentProcessor
33
34
  from linkml.utils.generator import Generator, shared_arguments
34
35
 
@@ -127,13 +128,120 @@ class PythonGenerator(Generator):
127
128
  self.emit_prefixes.add(type_prefix)
128
129
 
129
130
  def gen_schema(self) -> str:
131
+ all_imports = Imports()
132
+ # generic imports
133
+ all_imports = (
134
+ all_imports
135
+ + Import(module="dataclasses")
136
+ + Import(module="re")
137
+ + Import(
138
+ module="jsonasobj2",
139
+ objects=[
140
+ ObjectImport(name="JsonObj"),
141
+ ObjectImport(name="as_dict"),
142
+ ],
143
+ )
144
+ + Import(
145
+ module="typing",
146
+ objects=[
147
+ ObjectImport(name="Optional"),
148
+ ObjectImport(name="List"),
149
+ ObjectImport(name="Union"),
150
+ ObjectImport(name="Dict"),
151
+ ObjectImport(name="ClassVar"),
152
+ ObjectImport(name="Any"),
153
+ ],
154
+ )
155
+ + Import(
156
+ module="dataclasses",
157
+ objects=[
158
+ ObjectImport(name="dataclass"),
159
+ ],
160
+ )
161
+ + Import(
162
+ module="datetime",
163
+ objects=[
164
+ ObjectImport(name="date"),
165
+ ObjectImport(name="datetime"),
166
+ ObjectImport(name="time"),
167
+ ],
168
+ )
169
+ )
170
+
130
171
  # The metamodel uses Enumerations to define itself, so don't import if we are generating the metamodel
131
- enumimports = (
132
- ""
133
- if self.genmeta
134
- else "from linkml_runtime.linkml_model.meta import EnumDefinition, PermissibleValue, PvFormulaOptions\n"
172
+ if not self.genmeta:
173
+ all_imports = all_imports + Import(
174
+ module="linkml_runtime.linkml_model.meta",
175
+ objects=[
176
+ ObjectImport(name="EnumDefinition"),
177
+ ObjectImport(name="PermissibleValue"),
178
+ ObjectImport(name="PvFormulaOptions"),
179
+ ],
180
+ )
181
+ # linkml imports
182
+ all_imports = (
183
+ all_imports
184
+ + Import(
185
+ module="linkml_runtime.utils.slot",
186
+ objects=[
187
+ ObjectImport(name="Slot"),
188
+ ],
189
+ )
190
+ + Import(
191
+ module="linkml_runtime.utils.metamodelcore",
192
+ objects=[
193
+ ObjectImport(name="empty_list"),
194
+ ObjectImport(name="empty_dict"),
195
+ ObjectImport(name="bnode"),
196
+ ],
197
+ )
198
+ + Import(
199
+ module="linkml_runtime.utils.yamlutils",
200
+ objects=[
201
+ ObjectImport(name="YAMLRoot"),
202
+ ObjectImport(name="extended_str"),
203
+ ObjectImport(name="extended_float"),
204
+ ObjectImport(name="extended_int"),
205
+ ],
206
+ )
207
+ + Import(
208
+ module="linkml_runtime.utils.dataclass_extensions_376",
209
+ objects=[
210
+ ObjectImport(name="dataclasses_init_fn_with_kwargs"),
211
+ ],
212
+ )
213
+ + Import(
214
+ module="linkml_runtime.utils.formatutils",
215
+ objects=[
216
+ ObjectImport(name="camelcase"),
217
+ ObjectImport(name="underscore"),
218
+ ObjectImport(name="sfx"),
219
+ ],
220
+ )
221
+ )
222
+
223
+ # handler import
224
+ all_imports = all_imports + Import(
225
+ module="linkml_runtime.utils.enumerations", objects=[ObjectImport(name="EnumDefinitionImpl")]
226
+ )
227
+ # other imports
228
+ all_imports = (
229
+ all_imports
230
+ + Import(
231
+ module="rdflib",
232
+ objects=[
233
+ ObjectImport(name="Namespace"),
234
+ ObjectImport(name="URIRef"),
235
+ ],
236
+ )
237
+ + Import(
238
+ module="linkml_runtime.utils.curienamespace",
239
+ objects=[
240
+ ObjectImport(name="CurieNamespace"),
241
+ ],
242
+ )
135
243
  )
136
- handlerimport = "from linkml_runtime.utils.enumerations import EnumDefinitionImpl"
244
+
137
245
  split_description = ""
138
246
  if self.schema.description:
139
247
  split_description = "\n# ".join(d for d in self.schema.description.split("\n") if d is not None)
@@ -151,21 +259,7 @@ class PythonGenerator(Generator):
151
259
  # description: {split_description}
152
260
  # license: {be(self.schema.license)}
153
261
 
154
- import dataclasses
155
- import re
156
- from jsonasobj2 import JsonObj, as_dict
157
- from typing import Optional, List, Union, Dict, ClassVar, Any
158
- from dataclasses import dataclass
159
- from datetime import date, datetime, time
160
- {enumimports}
161
- from linkml_runtime.utils.slot import Slot
162
- from linkml_runtime.utils.metamodelcore import empty_list, empty_dict, bnode
163
- from linkml_runtime.utils.yamlutils import YAMLRoot, extended_str, extended_float, extended_int
164
- from linkml_runtime.utils.dataclass_extensions_376 import dataclasses_init_fn_with_kwargs
165
- from linkml_runtime.utils.formatutils import camelcase, underscore, sfx
166
- {handlerimport}
167
- from rdflib import Namespace, URIRef
168
- from linkml_runtime.utils.curienamespace import CurieNamespace
262
+ {all_imports.render()}
169
263
  {self.gen_imports()}
170
264
 
171
265
  metamodel_version = "{self.schema.metamodel_version}"
@@ -836,7 +930,15 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
836
930
  ):
837
931
  rlines.append(f"\tself.{aliased_slot_name} = {base_type_name}()")
838
932
  else:
839
- if (
933
+ if slot.range in self.schema.enums and slot.ifabsent:
934
+ # `ifabsent` for an enumeration cannot be assigned to
935
+ # the dataclass field default, because it would be a
936
+ # mutable. `python_ifabsent_processor.py` can specify
937
+ # the default as string and here that string gets
938
+ # converted into an object attribute invocation
939
+ # TODO: fix according https://github.com/linkml/linkml/pull/2329#discussion_r1797534588
940
+ rlines.append(f"\tself.{aliased_slot_name} = getattr({slot.range}, self.{aliased_slot_name})")
941
+ elif (
840
942
  (self.class_identifier(slot.range) and not slot.inlined)
841
943
  or slot.range in self.schema.types
842
944
  or slot.range in self.schema.enums
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: linkml
3
- Version: 1.8.4
3
+ Version: 1.8.5
4
4
  Summary: Linked Open Data Modeling Language
5
5
  Home-page: https://linkml.io/linkml/
6
6
  Keywords: schema,linked data,data modeling,rdf,owl,biolink
@@ -8,8 +8,9 @@ linkml/generators/README.md,sha256=RMzT8EblC_GEdPy5WyfXHDBXlFI6k6mz3Cx2sdpcyWI,4
8
8
  linkml/generators/__init__.py,sha256=2qSOnibL6KtzBW3KNqVX1gx1LQttrXlOPbbVDLLV2go,1587
9
9
  linkml/generators/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  linkml/generators/common/build.py,sha256=hXf2gf1ox2hiR-D6N-qXt5w5QkISLlo1A-_GurY3yHo,2897
11
- linkml/generators/common/ifabsent_processor.py,sha256=D8vKZLa15mc_SUyvFsE3NZfWi-c4kawYW6WdGozJLRQ,11275
11
+ linkml/generators/common/ifabsent_processor.py,sha256=hVm6mKq8qciL5Fa53TAtGO8ZJnbRuktvJnaA6-HutTk,13533
12
12
  linkml/generators/common/lifecycle.py,sha256=UOJXlIyWQkvsn0Dao9_ZDzlamJtiINyVJQ6SRj66L9s,4910
13
+ linkml/generators/common/naming.py,sha256=AsLJ7e87TU_SnAM0Sy7q9HZc-qXvyIuqHt1CoHV-Y3Y,5089
13
14
  linkml/generators/common/template.py,sha256=bjcSNiC85MYl9oGhk0ZpqwZpRxxy_NqxG8XBV-TXXDY,3338
14
15
  linkml/generators/common/type_designators.py,sha256=lgVcIRJJ-yCvVGeP9U_gQZpm77UmJBQo9Bh3lGJITno,1956
15
16
  linkml/generators/csvgen.py,sha256=O20K3IuDxomGJAyNFJmm1_HbPyl6xUK0l-aKY0A3KcM,3253
@@ -23,13 +24,13 @@ linkml/generators/docgen/schema.md.jinja2,sha256=xlENfnzNRYgPT_0tdqNFxgklVM4Qf5B
23
24
  linkml/generators/docgen/slot.md.jinja2,sha256=dJHugIv9OPTi3FIn8Iez7CzjQizdXrcn0hcmCmXSgV4,3339
24
25
  linkml/generators/docgen/subset.md.jinja2,sha256=EPln_fjlhlkmwEGEp2iPX-9Xm2vVodPZLlRBlYCB_sA,2705
25
26
  linkml/generators/docgen/type.md.jinja2,sha256=QmCMJZrFwP33eHkggBVtypbyrxTb-XZn9vHOYojVaYk,635
26
- linkml/generators/docgen.py,sha256=SAkqAZxbzHqzKrX3JQpRJr51bdr2gQKs6Bs1h-aVrmI,38711
27
+ linkml/generators/docgen.py,sha256=iDSu2LsZ7MStrzXdxFwK-oBlVevq2MwBc1n5WzHzTyw,38996
27
28
  linkml/generators/dotgen.py,sha256=vjHveFtKBItHRkowi1I4FHffcn4u4wLpBydYgVxB-vE,5028
28
29
  linkml/generators/erdiagramgen.py,sha256=5_VEPvRL0I819wCjk7T5hWffdBPc_qw3XpQdOYpO7uE,11357
29
30
  linkml/generators/excelgen.py,sha256=MQRRm1wDbg7auUQH9tGb5aRyrp-e4gjXsY10aoLb9LI,7708
30
31
  linkml/generators/golanggen.py,sha256=Qx1GZRIrKn8jNoO-8fJMjkrwV9ZLwR4NwYBW5kGGp9c,5840
31
32
  linkml/generators/golrgen.py,sha256=JB92VDhTuwyaOyxHxB4goIsjZbq4NMf36ESprC3GeBs,3454
32
- linkml/generators/graphqlgen.py,sha256=-BS-UHPyHqauUgtgcFCO4xSxxogk7BPAQFSXHIrKJT0,2163
33
+ linkml/generators/graphqlgen.py,sha256=belU4f89vBHoHBipaMkHiE_ND42IV7L0Zp4akL9sQWk,3484
33
34
  linkml/generators/javagen/example_template.java.jinja2,sha256=ec4CVTv_0zS7V5Y-1E6H4lRraya10gfX7BEMBlu38X4,444
34
35
  linkml/generators/javagen/java_record_template.jinja2,sha256=OQZffLSy_xR3FIhQMltvrYyVeut7l2Q-tzK7AOiVmWs,1729
35
36
  linkml/generators/javagen.py,sha256=BTAXgvOOvRa3AOfaaiVFyuyaqg4XFR_JbO6_7tT_AnQ,5335
@@ -52,7 +53,7 @@ linkml/generators/pydanticgen/black.py,sha256=BaJ7b-kMUbIN_cKRT3yCaMEbFwxzj1_U7w
52
53
  linkml/generators/pydanticgen/build.py,sha256=xWNnxaNz0LqfNpgUuaUeVdvmXyFASwv5QED3bEaUVaM,4093
53
54
  linkml/generators/pydanticgen/includes.py,sha256=kDAwonKbhTFzrFa2TrmTYuOSHXuzDy5WHgBaI5nVY6c,504
54
55
  linkml/generators/pydanticgen/pydanticgen.py,sha256=IEmSPr9zu1dkgIMFvVKDswokhQvM_weXdmVNQ3l3eNQ,49938
55
- linkml/generators/pydanticgen/template.py,sha256=7EZD_niU9bnkmtNG7zTBjLtQ5kUbKkBsidQP0X8UxFI,24828
56
+ linkml/generators/pydanticgen/template.py,sha256=wjUMiTay_EFh8zp5LfytBsgZJGlKjV96PVnBgI21KVc,24886
56
57
  linkml/generators/pydanticgen/templates/attribute.py.jinja,sha256=uMyVKpO5uQkAFlIvIBwA6FFZl_BSIotfwCorrSwMA3Q,873
57
58
  linkml/generators/pydanticgen/templates/base_model.py.jinja,sha256=6KRQ1ZVHh1uwZnYL3qxBnyit8_DNE7D-4l2-1wsRsG0,406
58
59
  linkml/generators/pydanticgen/templates/class.py.jinja,sha256=GqR2t5y0vLmvUpT2vyP5VX42PO9AkAnGE-U2RZPa9AI,690
@@ -63,8 +64,8 @@ linkml/generators/pydanticgen/templates/imports.py.jinja,sha256=vwAOWV98rM4reAUX
63
64
  linkml/generators/pydanticgen/templates/module.py.jinja,sha256=hqZKsJ72yUioBkZAunscVgLSe58OFHf0z6-so_b43U4,526
64
65
  linkml/generators/pydanticgen/templates/validator.py.jinja,sha256=5vDpqdfxLpie4BzcyjE82agO6NsThIZjNlcz6gq_kFg,465
65
66
  linkml/generators/python/__init__.py,sha256=NS9fPEuAo0punDplD91F9mcViXMJJv04Q5NkFcKPNvY,40
66
- linkml/generators/python/python_ifabsent_processor.py,sha256=a98JCbMsR_Fsdaw8s299-0jFhE0V6F4zLl326ITA7lQ,3848
67
- linkml/generators/pythongen.py,sha256=pOGoLvF-MxshwS-UJbIvqogWSwr4-sqhhpW5LMzRhbQ,52634
67
+ linkml/generators/python/python_ifabsent_processor.py,sha256=io-bLevkdy2xgKl0kqGB3o5iR7O7P1uE4qWEhKt-GcM,3838
68
+ linkml/generators/pythongen.py,sha256=-W8ZgvgDP-Hm4Q9weQiB7xtXMJyadtyfZ6BHC1CmClw,56127
68
69
  linkml/generators/rdfgen.py,sha256=OT8oS8MPUVf5UK3OKvRWgAh9iQiWwdyWJSA69PncOw4,2969
69
70
  linkml/generators/shacl/__init__.py,sha256=KgMOyESGTdKk2Vhx9uuUYgEJPuJQ-iT1vDQVIMoiXCM,58
70
71
  linkml/generators/shacl/shacl_data_type.py,sha256=gX3Y2KyTVPwMyef0cJMTEpU9h0oH-H0ThVDORdNW170,1828
@@ -152,8 +153,8 @@ linkml/workspaces/datamodel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
152
153
  linkml/workspaces/datamodel/workspaces.py,sha256=4HdkqweGNfMPqnB1_Onc9DcTfkhoagTRcqruh08nRoI,14905
153
154
  linkml/workspaces/datamodel/workspaces.yaml,sha256=EjVrwPpeRZqJRjuGyyDRxxFzuv55SiLIXPBRUG6HStU,4233
154
155
  linkml/workspaces/example_runner.py,sha256=T7sHQetV6a-dG5Kjuh2L4rHQ6-SPG3Kj1J1nPB_1uoQ,11757
155
- linkml-1.8.4.dist-info/LICENSE,sha256=kORMoywK6j9_iy0UvLR-a80P1Rvc9AOM4gsKlUNZABg,535
156
- linkml-1.8.4.dist-info/METADATA,sha256=G7CFrUedBRAKCg2p1-QuO9aA7KgVCh0n48KlZ1RjuZc,3873
157
- linkml-1.8.4.dist-info/entry_points.txt,sha256=jvnPJ8UTca4_L-Dh8OdUm5td8I3ZFlKRhMPjqRLtaSg,2084
158
- linkml-1.8.4.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
159
- linkml-1.8.4.dist-info/RECORD,,
156
+ linkml-1.8.5.dist-info/LICENSE,sha256=kORMoywK6j9_iy0UvLR-a80P1Rvc9AOM4gsKlUNZABg,535
157
+ linkml-1.8.5.dist-info/entry_points.txt,sha256=jvnPJ8UTca4_L-Dh8OdUm5td8I3ZFlKRhMPjqRLtaSg,2084
158
+ linkml-1.8.5.dist-info/METADATA,sha256=9isA7CyNTTGO9A1qH-RvsfN0TH2JGw6ss0RBEwuRa5k,3873
159
+ linkml-1.8.5.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
160
+ linkml-1.8.5.dist-info/RECORD,,
File without changes