linkml 1.9.4rc2__py3-none-any.whl → 1.9.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.
- linkml/cli/main.py +5 -1
- linkml/converter/__init__.py +0 -0
- linkml/generators/__init__.py +2 -0
- linkml/generators/common/build.py +5 -20
- linkml/generators/common/template.py +289 -3
- linkml/generators/docgen.py +55 -10
- linkml/generators/erdiagramgen.py +9 -5
- linkml/generators/graphqlgen.py +32 -6
- linkml/generators/jsonldcontextgen.py +78 -12
- linkml/generators/jsonschemagen.py +29 -12
- linkml/generators/mermaidclassdiagramgen.py +21 -3
- linkml/generators/owlgen.py +13 -2
- linkml/generators/panderagen/dataframe_class.py +13 -0
- linkml/generators/panderagen/dataframe_field.py +50 -0
- linkml/generators/panderagen/linkml_pandera_validator.py +186 -0
- linkml/generators/panderagen/panderagen.py +22 -5
- linkml/generators/panderagen/panderagen_class_based/class.jinja2 +70 -13
- linkml/generators/panderagen/panderagen_class_based/custom_checks.jinja2 +27 -0
- linkml/generators/panderagen/panderagen_class_based/enums.jinja2 +3 -3
- linkml/generators/panderagen/panderagen_class_based/pandera.jinja2 +12 -2
- linkml/generators/panderagen/panderagen_class_based/slots.jinja2 +19 -17
- linkml/generators/panderagen/slot_generator_mixin.py +143 -16
- linkml/generators/panderagen/transforms/__init__.py +19 -0
- linkml/generators/panderagen/transforms/collection_dict_model_transform.py +62 -0
- linkml/generators/panderagen/transforms/list_dict_model_transform.py +66 -0
- linkml/generators/panderagen/transforms/model_transform.py +8 -0
- linkml/generators/panderagen/transforms/nested_struct_model_transform.py +27 -0
- linkml/generators/panderagen/transforms/simple_dict_model_transform.py +86 -0
- linkml/generators/plantumlgen.py +17 -11
- linkml/generators/pydanticgen/pydanticgen.py +53 -2
- linkml/generators/pydanticgen/template.py +45 -233
- linkml/generators/pydanticgen/templates/attribute.py.jinja +1 -0
- linkml/generators/pydanticgen/templates/base_model.py.jinja +16 -2
- linkml/generators/pydanticgen/templates/imports.py.jinja +1 -1
- linkml/generators/rdfgen.py +11 -2
- linkml/generators/rustgen/__init__.py +3 -0
- linkml/generators/rustgen/build.py +97 -0
- linkml/generators/rustgen/cli.py +83 -0
- linkml/generators/rustgen/rustgen.py +1186 -0
- linkml/generators/rustgen/template.py +910 -0
- linkml/generators/rustgen/templates/Cargo.toml.jinja +42 -0
- linkml/generators/rustgen/templates/anything.rs.jinja +149 -0
- linkml/generators/rustgen/templates/as_key_value.rs.jinja +86 -0
- linkml/generators/rustgen/templates/class_module.rs.jinja +8 -0
- linkml/generators/rustgen/templates/enum.rs.jinja +70 -0
- linkml/generators/rustgen/templates/file.rs.jinja +75 -0
- linkml/generators/rustgen/templates/import.rs.jinja +4 -0
- linkml/generators/rustgen/templates/imports.rs.jinja +8 -0
- linkml/generators/rustgen/templates/lib_shim.rs.jinja +52 -0
- linkml/generators/rustgen/templates/poly.rs.jinja +9 -0
- linkml/generators/rustgen/templates/poly_containers.rs.jinja +439 -0
- linkml/generators/rustgen/templates/poly_trait.rs.jinja +15 -0
- linkml/generators/rustgen/templates/poly_trait_impl.rs.jinja +5 -0
- linkml/generators/rustgen/templates/poly_trait_impl_orsubtype.rs.jinja +5 -0
- linkml/generators/rustgen/templates/poly_trait_property.rs.jinja +8 -0
- linkml/generators/rustgen/templates/poly_trait_property_impl.rs.jinja +134 -0
- linkml/generators/rustgen/templates/poly_trait_property_match.rs.jinja +10 -0
- linkml/generators/rustgen/templates/property.rs.jinja +28 -0
- linkml/generators/rustgen/templates/pyproject.toml.jinja +10 -0
- linkml/generators/rustgen/templates/serde_utils.rs.jinja +490 -0
- linkml/generators/rustgen/templates/slot_range_as_union.rs.jinja +64 -0
- linkml/generators/rustgen/templates/struct.rs.jinja +81 -0
- linkml/generators/rustgen/templates/struct_or_subtype_enum.rs.jinja +111 -0
- linkml/generators/rustgen/templates/stub_gen.rs.jinja +71 -0
- linkml/generators/rustgen/templates/stub_utils.rs.jinja +76 -0
- linkml/generators/rustgen/templates/typealias.rs.jinja +13 -0
- linkml/generators/sqltablegen.py +18 -16
- linkml/generators/yarrrmlgen.py +173 -0
- linkml/linter/config/datamodel/config.py +160 -293
- linkml/linter/config/datamodel/config.yaml +34 -26
- linkml/linter/config/default.yaml +4 -0
- linkml/linter/config/recommended.yaml +4 -0
- linkml/linter/linter.py +1 -2
- linkml/linter/rules.py +37 -0
- linkml/utils/schema_builder.py +2 -0
- linkml/utils/schemaloader.py +76 -3
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5.dist-info}/METADATA +1 -1
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5.dist-info}/RECORD +82 -40
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5.dist-info}/entry_points.txt +2 -1
- linkml/generators/panderagen/panderagen_class_based/mixins.jinja2 +0 -26
- /linkml/{utils/converter.py → converter/cli.py} +0 -0
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5.dist-info}/WHEEL +0 -0
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5.dist-info}/licenses/LICENSE +0 -0
linkml/cli/main.py
CHANGED
|
@@ -7,6 +7,7 @@ Gathers all the other linkml click entrypoints and puts them under ``linkml`` :)
|
|
|
7
7
|
import click
|
|
8
8
|
|
|
9
9
|
from linkml._version import __version__
|
|
10
|
+
from linkml.converter.cli import cli as linkml_convert
|
|
10
11
|
from linkml.generators.csvgen import cli as gen_csv
|
|
11
12
|
from linkml.generators.dbmlgen import cli as gen_dbml
|
|
12
13
|
from linkml.generators.docgen import cli as gen_doc
|
|
@@ -32,6 +33,7 @@ from linkml.generators.protogen import cli as gen_proto
|
|
|
32
33
|
from linkml.generators.pydanticgen import cli as gen_pydantic
|
|
33
34
|
from linkml.generators.pythongen import cli as gen_python
|
|
34
35
|
from linkml.generators.rdfgen import cli as gen_rdf
|
|
36
|
+
from linkml.generators.rustgen.cli import cli as gen_rust
|
|
35
37
|
from linkml.generators.shaclgen import cli as gen_shacl
|
|
36
38
|
from linkml.generators.shexgen import cli as gen_shex
|
|
37
39
|
from linkml.generators.sparqlgen import cli as gen_sparql
|
|
@@ -42,9 +44,9 @@ from linkml.generators.summarygen import cli as gen_summary
|
|
|
42
44
|
from linkml.generators.terminusdbgen import cli as gen_terminusdb
|
|
43
45
|
from linkml.generators.typescriptgen import cli as gen_typescript
|
|
44
46
|
from linkml.generators.yamlgen import cli as gen_yaml
|
|
47
|
+
from linkml.generators.yarrrmlgen import cli as gen_yarrrml
|
|
45
48
|
from linkml.generators.yumlgen import cli as gen_yuml
|
|
46
49
|
from linkml.linter.cli import main as linkml_lint
|
|
47
|
-
from linkml.utils.converter import cli as linkml_convert
|
|
48
50
|
from linkml.utils.execute_tutorial import cli as run_tutorial
|
|
49
51
|
from linkml.utils.schema_fixer import main as linkml_schema_fixer
|
|
50
52
|
from linkml.utils.sqlutils import main as linkml_sqldb
|
|
@@ -113,6 +115,7 @@ generate.add_command(gen_python, name="python")
|
|
|
113
115
|
generate.add_command(gen_pydantic, name="pydantic")
|
|
114
116
|
generate.add_command(gen_pandera, name="pandera")
|
|
115
117
|
generate.add_command(gen_rdf, name="rdf")
|
|
118
|
+
generate.add_command(gen_rust, name="rust")
|
|
116
119
|
generate.add_command(gen_shex, name="shex")
|
|
117
120
|
generate.add_command(gen_shacl, name="shacl")
|
|
118
121
|
generate.add_command(gen_sparql, name="sparql")
|
|
@@ -129,6 +132,7 @@ generate.add_command(gen_excel, name="excel")
|
|
|
129
132
|
generate.add_command(gen_sssom, name="sssom")
|
|
130
133
|
generate.add_command(gen_linkml, name="linkml")
|
|
131
134
|
generate.add_command(gen_dbml, name="dbml")
|
|
135
|
+
generate.add_command(gen_yarrrml, name="yarrrml")
|
|
132
136
|
|
|
133
137
|
# Dev helpers
|
|
134
138
|
dev.add_command(run_tutorial, name="tutorial")
|
|
File without changes
|
linkml/generators/__init__.py
CHANGED
|
@@ -12,6 +12,7 @@ from linkml.generators.panderagen import PanderaGenerator
|
|
|
12
12
|
from linkml.generators.pydanticgen import PydanticGenerator
|
|
13
13
|
from linkml.generators.pythongen import PythonGenerator
|
|
14
14
|
from linkml.generators.rdfgen import RDFGenerator
|
|
15
|
+
from linkml.generators.rustgen import RustGenerator
|
|
15
16
|
from linkml.generators.shaclgen import ShaclGenerator
|
|
16
17
|
from linkml.generators.shexgen import ShExGenerator
|
|
17
18
|
from linkml.generators.sqlalchemygen import SQLAlchemyGenerator
|
|
@@ -49,6 +50,7 @@ __all__ = [
|
|
|
49
50
|
"ContextGenerator",
|
|
50
51
|
"JSONLDGenerator",
|
|
51
52
|
"JsonSchemaGenerator",
|
|
53
|
+
"RustGenerator",
|
|
52
54
|
"ShaclGenerator",
|
|
53
55
|
"ShExGenerator",
|
|
54
56
|
"SQLAlchemyGenerator",
|
|
@@ -4,38 +4,23 @@ Models for intermediate build results
|
|
|
4
4
|
(see PydanticGenerator for example implementation and use)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
from typing import Annotated, Any, TypeVar
|
|
7
|
+
from typing import Annotated, TypeVar
|
|
9
8
|
|
|
10
9
|
from linkml_runtime.linkml_model import (
|
|
11
10
|
ClassDefinition,
|
|
11
|
+
Definition,
|
|
12
12
|
EnumDefinition,
|
|
13
13
|
SchemaDefinition,
|
|
14
14
|
SlotDefinition,
|
|
15
15
|
TypeDefinition,
|
|
16
16
|
)
|
|
17
|
-
from pydantic import BaseModel, ConfigDict,
|
|
17
|
+
from pydantic import BaseModel, ConfigDict, GetPydanticSchema
|
|
18
18
|
from pydantic_core import core_schema
|
|
19
19
|
|
|
20
20
|
T = TypeVar("T", bound="BuildResult", covariant=True)
|
|
21
|
+
Tsv = TypeVar("Tsv", bound=Definition, covariant=True)
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
@dataclasses.dataclass()
|
|
24
|
-
class SkipValidation:
|
|
25
|
-
"""
|
|
26
|
-
A version of :class:`pydantic.SkipValidation` that actually skips generating the
|
|
27
|
-
schema for the field entirely - useful for including types that don't need to be validated
|
|
28
|
-
like the metamodel dataclasses
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
def __class_getitem__(cls, item: Any) -> Any:
|
|
32
|
-
return Annotated[item, SkipValidation()]
|
|
33
|
-
|
|
34
|
-
@classmethod
|
|
35
|
-
def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
|
|
36
|
-
return core_schema.any_schema()
|
|
37
|
-
|
|
38
|
-
__hash__ = object.__hash__
|
|
23
|
+
SkipValidation = Annotated[Tsv, GetPydanticSchema(lambda tp, handler: core_schema.any_schema())]
|
|
39
24
|
|
|
40
25
|
|
|
41
26
|
class BuildResult(BaseModel):
|
|
@@ -4,11 +4,12 @@ Base classes for jinja template handling classes.
|
|
|
4
4
|
See :mod:`.linkml.generators.pydanticgen.template` for example implementation
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from collections.abc import Generator
|
|
7
8
|
from copy import copy
|
|
8
9
|
from typing import Any, ClassVar, Optional, Union
|
|
9
10
|
|
|
10
11
|
from jinja2 import Environment
|
|
11
|
-
from pydantic import BaseModel
|
|
12
|
+
from pydantic import BaseModel, Field, field_validator
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class TemplateModel(BaseModel):
|
|
@@ -42,7 +43,7 @@ class TemplateModel(BaseModel):
|
|
|
42
43
|
environment (:class:`jinja2.Environment`): Template environment - see :meth:`.environment`
|
|
43
44
|
"""
|
|
44
45
|
if environment is None:
|
|
45
|
-
environment =
|
|
46
|
+
environment = type(self).environment()
|
|
46
47
|
|
|
47
48
|
fields = {**self.model_fields, **self.model_computed_fields}
|
|
48
49
|
|
|
@@ -72,6 +73,291 @@ class TemplateModel(BaseModel):
|
|
|
72
73
|
return ret
|
|
73
74
|
|
|
74
75
|
|
|
76
|
+
class ObjectImport(BaseModel):
|
|
77
|
+
"""
|
|
78
|
+
An object to be imported from within a module.
|
|
79
|
+
|
|
80
|
+
See :class:`.Import` for examples
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
name: str
|
|
84
|
+
alias: Optional[str] = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class Import(TemplateModel):
|
|
88
|
+
"""
|
|
89
|
+
A module or module and classes to be imported.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
module: str
|
|
93
|
+
alias: Optional[str] = None
|
|
94
|
+
objects: Optional[list[ObjectImport]] = None
|
|
95
|
+
is_schema: bool = False
|
|
96
|
+
"""
|
|
97
|
+
Whether or not this ``Import`` is importing another schema imported by the main schema --
|
|
98
|
+
ie. that it is not expected to be provided by the environment, but imported locally from within the package.
|
|
99
|
+
Used primarily in split schema generation, see :func:`.pydanticgen.generate_split` for example usage.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def merge(self, other: "Import") -> list["Import"]:
|
|
103
|
+
"""
|
|
104
|
+
Merge one import with another, see :meth:`.Imports` for an example.
|
|
105
|
+
|
|
106
|
+
* If module don't match, return both
|
|
107
|
+
* If one or the other are a :class:`.ConditionalImport`, return both
|
|
108
|
+
* If modules match, neither contain objects, but the other has an alias, return the other
|
|
109
|
+
* If modules match, one contains objects but the other doesn't, return both
|
|
110
|
+
* If modules match, both contain objects, merge the object lists, preferring objects with aliases
|
|
111
|
+
"""
|
|
112
|
+
# return both if we are orthogonal
|
|
113
|
+
if self.module != other.module:
|
|
114
|
+
return [self, other]
|
|
115
|
+
|
|
116
|
+
# handle conditionals
|
|
117
|
+
if isinstance(self, ConditionalImport) and isinstance(other, ConditionalImport):
|
|
118
|
+
# If our condition is the same, return the newer version
|
|
119
|
+
if self.condition == other.condition:
|
|
120
|
+
return [other]
|
|
121
|
+
if isinstance(self, ConditionalImport) or isinstance(other, ConditionalImport):
|
|
122
|
+
# we don't have a good way of combining conditionals, just return both
|
|
123
|
+
return [self, other]
|
|
124
|
+
|
|
125
|
+
# handle module vs. object imports
|
|
126
|
+
elif other.objects is None and self.objects is None:
|
|
127
|
+
# both are modules, return the other only if it updates the alias
|
|
128
|
+
if other.alias:
|
|
129
|
+
return [other]
|
|
130
|
+
else:
|
|
131
|
+
return [self]
|
|
132
|
+
elif other.objects is not None and self.objects is not None:
|
|
133
|
+
# both are object imports, merge and return
|
|
134
|
+
alias = self.alias if other.alias is None else other.alias
|
|
135
|
+
# FIXME: super awkward implementation
|
|
136
|
+
# keep ours if it has an alias and the other doesn't,
|
|
137
|
+
# otherwise take the other's version
|
|
138
|
+
self_objs = {obj.name: obj for obj in self.objects}
|
|
139
|
+
other_objs = {
|
|
140
|
+
obj.name: obj for obj in other.objects if obj.name not in self_objs or self_objs[obj.name].alias is None
|
|
141
|
+
}
|
|
142
|
+
self_objs.update(other_objs)
|
|
143
|
+
|
|
144
|
+
return [
|
|
145
|
+
type(self)(
|
|
146
|
+
module=self.module,
|
|
147
|
+
alias=alias,
|
|
148
|
+
objects=list(self_objs.values()),
|
|
149
|
+
is_schema=self.is_schema or other.is_schema,
|
|
150
|
+
**{
|
|
151
|
+
k: getattr(other, k)
|
|
152
|
+
for k in other.model_fields
|
|
153
|
+
if k not in ("module", "alias", "objects", "is_schema")
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
]
|
|
157
|
+
else:
|
|
158
|
+
# one is a module, the other imports objects, keep both
|
|
159
|
+
return [self, other]
|
|
160
|
+
|
|
161
|
+
def sort(self) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Sort imported objects
|
|
164
|
+
|
|
165
|
+
* First by whether the first letter is capitalized or not,
|
|
166
|
+
* Then alphabetically (by object name rather than alias)
|
|
167
|
+
"""
|
|
168
|
+
if self.objects:
|
|
169
|
+
self.objects = sorted(self.objects, key=lambda obj: (obj.name[0].islower(), obj.name))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ConditionalImport(Import):
|
|
173
|
+
"""
|
|
174
|
+
Import that depends on some condition in the environment, common when
|
|
175
|
+
using backported features or straddling dependency versions.
|
|
176
|
+
|
|
177
|
+
Make sure that everything that is needed to evaluate the condition is imported
|
|
178
|
+
before this is added to the injected imports!
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
condition: str
|
|
182
|
+
alternative: Import
|
|
183
|
+
|
|
184
|
+
def sort(self) -> None:
|
|
185
|
+
"""
|
|
186
|
+
:meth:`.Import.sort` called for self and :attr:`.alternative`
|
|
187
|
+
"""
|
|
188
|
+
super().sort()
|
|
189
|
+
self.alternative.sort()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Imports(TemplateModel):
|
|
193
|
+
"""
|
|
194
|
+
Container class for imports that can handle merging!
|
|
195
|
+
|
|
196
|
+
See :class:`.Import` and :class:`.ConditionalImport` for examples of declaring individual imports
|
|
197
|
+
|
|
198
|
+
Useful for generation, because each build stage will potentially generate
|
|
199
|
+
overlapping imports. This ensures that we can keep a collection of imports
|
|
200
|
+
without having many duplicates.
|
|
201
|
+
|
|
202
|
+
Defines methods for adding, iterating, and indexing from within the :attr:`Imports.imports` list.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
imports: list[Union[Import, ConditionalImport]] = Field(default_factory=list)
|
|
206
|
+
render_sorted: bool = True
|
|
207
|
+
"""When rendering, render in sorted groups"""
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def _merge(
|
|
211
|
+
cls, imports: list[Union[Import, ConditionalImport]], other: Union[Import, "Imports", list[Import]]
|
|
212
|
+
) -> list[Union[Import, ConditionalImport]]:
|
|
213
|
+
"""
|
|
214
|
+
Add a new import to an existing imports list, handling deduplication and flattening.
|
|
215
|
+
|
|
216
|
+
Mutates and returns ``imports``
|
|
217
|
+
|
|
218
|
+
Generally will prefer the imports in ``other`` , updating those in ``imports``.
|
|
219
|
+
If ``other`` ...
|
|
220
|
+
- doesn't match any ``module`` in ``imports``, add it!
|
|
221
|
+
- matches a single ``module`` in imports, :meth:`.Import.merge` the object imports
|
|
222
|
+
- matches multiple ``module``s in imports, then there must have been another
|
|
223
|
+
:class:`.ConditionalImport` already present, so we :meth:`.Import.merge` the existing
|
|
224
|
+
:class:`.Import` if it is one, and if it's a :class:`.ConditionalImport` just YOLO
|
|
225
|
+
and append it since there isn't a principled way to merge them from strings.
|
|
226
|
+
- is :class:`.Imports` or a list of :class:`.Import` s, call this recursively for each
|
|
227
|
+
item.
|
|
228
|
+
|
|
229
|
+
Since imports can be merged in an undefined order depending on the generator configuration,
|
|
230
|
+
default behavior for imports with matching ``module`` is to remove them and append to the
|
|
231
|
+
end of the imports list (rather than keeping it in the position of the existing
|
|
232
|
+
:class:`.Import` ). :class:`.ConditionalImports` make it possible to have namespace
|
|
233
|
+
conflicts, so in imperative import style we assume the most recently added :class:`.Import`
|
|
234
|
+
is the one that should prevail.
|
|
235
|
+
"""
|
|
236
|
+
#
|
|
237
|
+
if isinstance(other, cls) or (isinstance(other, list) and all([isinstance(i, Import) for i in other])):
|
|
238
|
+
for i in other:
|
|
239
|
+
imports = cls._merge(imports, i)
|
|
240
|
+
return imports
|
|
241
|
+
|
|
242
|
+
existing = [i for i in imports if i.module == other.module]
|
|
243
|
+
|
|
244
|
+
# if we have nothing importing from this module yet, add it!
|
|
245
|
+
if len(existing) == 0:
|
|
246
|
+
imports.append(other)
|
|
247
|
+
elif len(existing) == 1:
|
|
248
|
+
imports.remove(existing[0])
|
|
249
|
+
imports.extend(existing[0].merge(other))
|
|
250
|
+
else:
|
|
251
|
+
# we have both a conditional and at least one nonconditional already.
|
|
252
|
+
# If this is another conditional, we just add it, otherwise, we merge it
|
|
253
|
+
# with the single nonconditional
|
|
254
|
+
if isinstance(other, ConditionalImport):
|
|
255
|
+
imports.append(other)
|
|
256
|
+
else:
|
|
257
|
+
for e in existing:
|
|
258
|
+
if isinstance(e, Import):
|
|
259
|
+
imports.remove(e)
|
|
260
|
+
merged = e.merge(other)
|
|
261
|
+
imports.extend(merged)
|
|
262
|
+
break
|
|
263
|
+
|
|
264
|
+
# SPECIAL CASE - __future__ annotations must happen at the top of a file
|
|
265
|
+
# sort here outside of sort method because our imports are invalid without it,
|
|
266
|
+
# where calling ``sort`` should be optional.
|
|
267
|
+
imports = sorted(imports, key=lambda i: i.module == "__future__", reverse=True)
|
|
268
|
+
return imports
|
|
269
|
+
|
|
270
|
+
def __add__(self, other: Union[Import, "Imports", list[Import]]) -> "Imports":
|
|
271
|
+
imports = self.imports.copy()
|
|
272
|
+
imports = self._merge(imports, other)
|
|
273
|
+
return type(self).model_construct(
|
|
274
|
+
imports=imports, **{k: getattr(self, k, None) for k in self.model_fields if k != "imports"}
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def __len__(self) -> int:
|
|
278
|
+
return len(self.imports)
|
|
279
|
+
|
|
280
|
+
def __iter__(self) -> Generator[Import, None, None]:
|
|
281
|
+
yield from self.imports
|
|
282
|
+
|
|
283
|
+
def __getitem__(self, item: Union[int, str]) -> Import:
|
|
284
|
+
if isinstance(item, int):
|
|
285
|
+
return self.imports[item]
|
|
286
|
+
elif isinstance(item, str):
|
|
287
|
+
# the name of the module
|
|
288
|
+
an_import = [i for i in self.imports if i.module == item]
|
|
289
|
+
if len(an_import) == 0:
|
|
290
|
+
raise KeyError(f"No import with module {item} was found.\nWe have: {self.imports}")
|
|
291
|
+
return an_import[0]
|
|
292
|
+
else:
|
|
293
|
+
raise TypeError(f"Can only index with an int or a string as the name of the module,\nGot: {type(item)}")
|
|
294
|
+
|
|
295
|
+
def __contains__(self, item: Union[Import, "Imports", list[Import]]) -> bool:
|
|
296
|
+
"""
|
|
297
|
+
Check if all the objects are imported from the given module(s)
|
|
298
|
+
|
|
299
|
+
If the import is a bare module import (ie its :attr:`~.Import.objects` is ``None`` )
|
|
300
|
+
then we must also have a bare module import in this Imports (because even if
|
|
301
|
+
we import from the module, unless we import it specifically its name won't be
|
|
302
|
+
available in the namespace.
|
|
303
|
+
|
|
304
|
+
:attr:`.Import.alias` must always match for the same reason.
|
|
305
|
+
"""
|
|
306
|
+
if isinstance(item, Imports):
|
|
307
|
+
return all([i in self for i in item.imports])
|
|
308
|
+
elif isinstance(item, list):
|
|
309
|
+
return all([i in self for i in item])
|
|
310
|
+
elif isinstance(item, Import):
|
|
311
|
+
try:
|
|
312
|
+
an_import = self[item.module]
|
|
313
|
+
except KeyError:
|
|
314
|
+
return False
|
|
315
|
+
if item.objects is None:
|
|
316
|
+
return an_import == item
|
|
317
|
+
else:
|
|
318
|
+
return all([obj in an_import.objects for obj in item.objects])
|
|
319
|
+
else:
|
|
320
|
+
raise TypeError(f"Imports only contains single Import objects or other Imports\nGot: {type(item)}")
|
|
321
|
+
|
|
322
|
+
@field_validator("imports", mode="after")
|
|
323
|
+
@classmethod
|
|
324
|
+
def imports_are_merged(
|
|
325
|
+
cls, imports: list[Union[Import, ConditionalImport]]
|
|
326
|
+
) -> list[Union[Import, ConditionalImport]]:
|
|
327
|
+
"""
|
|
328
|
+
When creating from a list of imports, construct model as if we have done so by iteratively
|
|
329
|
+
constructing with __add__ calls
|
|
330
|
+
"""
|
|
331
|
+
merged_imports = []
|
|
332
|
+
for i in imports:
|
|
333
|
+
merged_imports = cls._merge(merged_imports, i)
|
|
334
|
+
return merged_imports
|
|
335
|
+
|
|
336
|
+
def sort(self) -> None:
|
|
337
|
+
"""
|
|
338
|
+
Sort imports recursively, mimicking isort:
|
|
339
|
+
|
|
340
|
+
* First by :attr:`.Import.group` according to :attr:`.Imports.group_order`
|
|
341
|
+
* Then by whether the :class:`.Import` has any objects
|
|
342
|
+
(``import module`` comes before ``from module import name``)
|
|
343
|
+
* Then alphabetically by module name
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
def _sort_key(i: Import) -> tuple[int, str]:
|
|
347
|
+
return (int(i.objects is not None), i.module)
|
|
348
|
+
|
|
349
|
+
imports = sorted(self.imports, key=_sort_key)
|
|
350
|
+
for i in imports:
|
|
351
|
+
i.sort()
|
|
352
|
+
self.imports = imports
|
|
353
|
+
|
|
354
|
+
def render(self, environment: Optional[Environment] = None, black: bool = False) -> str:
|
|
355
|
+
if self.render_sorted:
|
|
356
|
+
self.sort()
|
|
357
|
+
rendered = super().render(environment=environment, black=black)
|
|
358
|
+
return rendered
|
|
359
|
+
|
|
360
|
+
|
|
75
361
|
def _render(
|
|
76
362
|
item: Union[TemplateModel, Any, list[Union[Any, TemplateModel]], dict[str, Union[Any, TemplateModel]]],
|
|
77
363
|
environment: Environment,
|
|
@@ -83,7 +369,7 @@ def _render(
|
|
|
83
369
|
elif isinstance(item, dict):
|
|
84
370
|
return {k: _render(v, environment) for k, v in item.items()}
|
|
85
371
|
elif isinstance(item, BaseModel):
|
|
86
|
-
fields = item.model_fields
|
|
372
|
+
fields = {**item.model_fields, **getattr(item, "model_computed_fields", {})}
|
|
87
373
|
return {k: _render(getattr(item, k, None), environment) for k in fields.keys()}
|
|
88
374
|
else:
|
|
89
375
|
return item
|
linkml/generators/docgen.py
CHANGED
|
@@ -173,6 +173,9 @@ class DocGenerator(Generator):
|
|
|
173
173
|
hierarchical_class_view: bool = False
|
|
174
174
|
render_imports: bool = False
|
|
175
175
|
|
|
176
|
+
preserve_names: bool = False
|
|
177
|
+
"""If true, preserve LinkML element names in docs instead of camelcase/underscore."""
|
|
178
|
+
|
|
176
179
|
def __post_init__(self):
|
|
177
180
|
dialect = self.dialect
|
|
178
181
|
if dialect is not None:
|
|
@@ -193,6 +196,27 @@ class DocGenerator(Generator):
|
|
|
193
196
|
self.logger = logging.getLogger(__name__)
|
|
194
197
|
self.schemaview = SchemaView(self.schema, merge_imports=self.mergeimports)
|
|
195
198
|
|
|
199
|
+
# Override schemaview's get_mappings to use preserved URIs when needed
|
|
200
|
+
if self.preserve_names:
|
|
201
|
+
original_get_mappings = self.schemaview.get_mappings
|
|
202
|
+
|
|
203
|
+
def get_mappings_with_preserved_uris(element_name, **kwargs):
|
|
204
|
+
mappings = original_get_mappings(element_name, **kwargs)
|
|
205
|
+
if mappings and element_name:
|
|
206
|
+
try:
|
|
207
|
+
element = self.schemaview.get_element(element_name)
|
|
208
|
+
if element:
|
|
209
|
+
preserved_uri = self.uri(element, expand=True)
|
|
210
|
+
# Fix self and native mappings to use preserved URI
|
|
211
|
+
for key in ("self", "native"):
|
|
212
|
+
if key in mappings:
|
|
213
|
+
mappings[key] = [preserved_uri]
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
return mappings
|
|
217
|
+
|
|
218
|
+
self.schemaview.get_mappings = get_mappings_with_preserved_uris
|
|
219
|
+
|
|
196
220
|
def serialize(self, directory: str = None) -> None:
|
|
197
221
|
"""
|
|
198
222
|
Serialize a schema as a collection of documents
|
|
@@ -363,19 +387,21 @@ class DocGenerator(Generator):
|
|
|
363
387
|
:return: slot name or numeric portion of CURIE prefixed
|
|
364
388
|
slot_uri
|
|
365
389
|
"""
|
|
366
|
-
if
|
|
390
|
+
if element is None:
|
|
391
|
+
return ""
|
|
392
|
+
if self.preserve_names:
|
|
393
|
+
return element.name
|
|
394
|
+
elif type(element).class_name == "slot_definition":
|
|
367
395
|
if self.use_slot_uris:
|
|
368
396
|
curie = self.schemaview.get_uri(element)
|
|
369
397
|
if curie:
|
|
370
398
|
return curie.split(":")[1]
|
|
371
|
-
|
|
372
399
|
return underscore(element.name)
|
|
373
400
|
elif type(element).class_name == "class_definition":
|
|
374
401
|
if self.use_class_uris:
|
|
375
402
|
curie = self.schemaview.get_uri(element)
|
|
376
403
|
if curie:
|
|
377
404
|
return curie.split(":")[1]
|
|
378
|
-
|
|
379
405
|
return camelcase(element.name)
|
|
380
406
|
else:
|
|
381
407
|
return camelcase(element.name)
|
|
@@ -391,6 +417,13 @@ class DocGenerator(Generator):
|
|
|
391
417
|
# Subsets have no uri attribute
|
|
392
418
|
return self.name(element)
|
|
393
419
|
|
|
420
|
+
# Simple URI composition for preserved names
|
|
421
|
+
if self.preserve_names and not (self.use_class_uris or self.use_slot_uris):
|
|
422
|
+
base = self.schemaview.schema.id
|
|
423
|
+
if base and ("://" in base or base.startswith("http")):
|
|
424
|
+
sep = "" if base.endswith(("/", "#", "_", ":")) else "/"
|
|
425
|
+
return f"{base}{sep}{self.name(element)}"
|
|
426
|
+
|
|
394
427
|
if self.subfolder_type_separation:
|
|
395
428
|
return self.schemaview.get_uri(element, expand=expand, use_element_type=True)
|
|
396
429
|
|
|
@@ -442,6 +475,8 @@ class DocGenerator(Generator):
|
|
|
442
475
|
return "NONE"
|
|
443
476
|
if not isinstance(e, Definition):
|
|
444
477
|
e = self.schemaview.get_element(e)
|
|
478
|
+
if e is None:
|
|
479
|
+
return "NONE"
|
|
445
480
|
if self._is_external(e):
|
|
446
481
|
return self.uri_link(e)
|
|
447
482
|
elif isinstance(e, ClassDefinition):
|
|
@@ -454,12 +489,12 @@ class DocGenerator(Generator):
|
|
|
454
489
|
subfolder=subfolder + CLASS_SUBFOLDER if self.subfolder_type_separation else None,
|
|
455
490
|
)
|
|
456
491
|
return self._markdown_link(
|
|
457
|
-
|
|
492
|
+
self.name(e),
|
|
458
493
|
subfolder=subfolder + CLASS_SUBFOLDER if self.subfolder_type_separation else None,
|
|
459
494
|
)
|
|
460
495
|
elif isinstance(e, EnumDefinition):
|
|
461
496
|
return self._markdown_link(
|
|
462
|
-
|
|
497
|
+
self.name(e),
|
|
463
498
|
subfolder=subfolder + ENUM_SUBFOLDER if self.subfolder_type_separation else None,
|
|
464
499
|
)
|
|
465
500
|
elif isinstance(e, SlotDefinition):
|
|
@@ -472,17 +507,17 @@ class DocGenerator(Generator):
|
|
|
472
507
|
subfolder=subfolder + SLOT_SUBFOLDER if self.subfolder_type_separation else None,
|
|
473
508
|
)
|
|
474
509
|
return self._markdown_link(
|
|
475
|
-
|
|
510
|
+
self.name(e),
|
|
476
511
|
subfolder=subfolder + SLOT_SUBFOLDER if self.subfolder_type_separation else None,
|
|
477
512
|
)
|
|
478
513
|
elif isinstance(e, TypeDefinition):
|
|
479
514
|
return self._markdown_link(
|
|
480
|
-
|
|
515
|
+
self.name(e),
|
|
481
516
|
subfolder=subfolder + TYPE_SUBFOLDER if self.subfolder_type_separation else None,
|
|
482
517
|
)
|
|
483
518
|
elif isinstance(e, SubsetDefinition):
|
|
484
519
|
return self._markdown_link(
|
|
485
|
-
|
|
520
|
+
self.name(e),
|
|
486
521
|
subfolder=subfolder + SUBSET_SUBFOLDER if self.subfolder_type_separation else None,
|
|
487
522
|
)
|
|
488
523
|
else:
|
|
@@ -500,6 +535,8 @@ class DocGenerator(Generator):
|
|
|
500
535
|
return self._is_external(t) and not self.schemaview.schema.id.startswith("https://w3id.org/linkml/")
|
|
501
536
|
|
|
502
537
|
def _is_external(self, element: Element) -> bool:
|
|
538
|
+
if element is None:
|
|
539
|
+
return False
|
|
503
540
|
if element.from_schema == "https://w3id.org/linkml/types" and not self.genmeta:
|
|
504
541
|
return True
|
|
505
542
|
else:
|
|
@@ -714,7 +751,7 @@ class DocGenerator(Generator):
|
|
|
714
751
|
:return:
|
|
715
752
|
"""
|
|
716
753
|
if self.diagram_type.value == DiagramType.er_diagram.value:
|
|
717
|
-
erdgen = ERDiagramGenerator(self.schemaview.schema, format="mermaid")
|
|
754
|
+
erdgen = ERDiagramGenerator(self.schemaview.schema, format="mermaid", preserve_names=self.preserve_names)
|
|
718
755
|
if class_names:
|
|
719
756
|
return erdgen.serialize_classes(class_names, follow_references=True, max_hops=2)
|
|
720
757
|
else:
|
|
@@ -722,7 +759,7 @@ class DocGenerator(Generator):
|
|
|
722
759
|
elif self.diagram_type.value == DiagramType.mermaid_class_diagram.value:
|
|
723
760
|
self.logger.info("This is currently handled in the jinja templates")
|
|
724
761
|
elif self.diagram_type.value == DiagramType.plantuml_class_diagram.value:
|
|
725
|
-
plantumlgen = PlantumlGenerator(self.schema)
|
|
762
|
+
plantumlgen = PlantumlGenerator(self.schema, preserve_names=self.preserve_names)
|
|
726
763
|
plantuml_diagram = plantumlgen.serialize(classes=class_names)
|
|
727
764
|
self.logger.debug(f"Created PlantUML diagram for class: {class_names}")
|
|
728
765
|
return plantuml_diagram
|
|
@@ -1138,6 +1175,12 @@ YAML, and including it when necessary but not by default (e.g. in documentation
|
|
|
1138
1175
|
Whether to truncate long (potentially spanning multiple lines) descriptions of classes, slots, etc., in the docs.
|
|
1139
1176
|
Set to true for truncated descriptions, and false to display full descriptions.""",
|
|
1140
1177
|
)
|
|
1178
|
+
@click.option(
|
|
1179
|
+
"--preserve-names/--normalize-names",
|
|
1180
|
+
default=False,
|
|
1181
|
+
show_default=True,
|
|
1182
|
+
help="Preserve original LinkML names in documentation output (e.g., for page titles, links, and file names).",
|
|
1183
|
+
)
|
|
1141
1184
|
@click.version_option(__version__, "-V", "--version")
|
|
1142
1185
|
@click.command(name="doc")
|
|
1143
1186
|
def cli(
|
|
@@ -1152,6 +1195,7 @@ def cli(
|
|
|
1152
1195
|
subfolder_type_separation,
|
|
1153
1196
|
render_imports,
|
|
1154
1197
|
truncate_descriptions,
|
|
1198
|
+
preserve_names,
|
|
1155
1199
|
**args,
|
|
1156
1200
|
):
|
|
1157
1201
|
"""Generate documentation folder from a LinkML YAML schema
|
|
@@ -1185,6 +1229,7 @@ def cli(
|
|
|
1185
1229
|
subfolder_type_separation=subfolder_type_separation,
|
|
1186
1230
|
render_imports=render_imports,
|
|
1187
1231
|
truncate_descriptions=truncate_descriptions,
|
|
1232
|
+
preserve_names=preserve_names,
|
|
1188
1233
|
**args,
|
|
1189
1234
|
)
|
|
1190
1235
|
print(gen.serialize())
|
|
@@ -142,6 +142,9 @@ class ERDiagramGenerator(Generator):
|
|
|
142
142
|
no_types_dir: bool = False
|
|
143
143
|
use_slot_uris: bool = False
|
|
144
144
|
|
|
145
|
+
preserve_names: bool = False
|
|
146
|
+
"""If true, preserve LinkML element names (classes/slots) in diagram labels."""
|
|
147
|
+
|
|
145
148
|
def __post_init__(self):
|
|
146
149
|
self.schemaview = SchemaView(self.schema)
|
|
147
150
|
super().__post_init__()
|
|
@@ -232,7 +235,7 @@ class ERDiagramGenerator(Generator):
|
|
|
232
235
|
cls = sv.get_class(class_name)
|
|
233
236
|
if self.exclude_abstract_classes and cls.abstract:
|
|
234
237
|
return
|
|
235
|
-
entity = Entity(name=camelcase(cls.name))
|
|
238
|
+
entity = Entity(name=cls.name if self.preserve_names else camelcase(cls.name))
|
|
236
239
|
diagram.entities.append(entity)
|
|
237
240
|
for slot in sv.class_induced_slots(class_name):
|
|
238
241
|
if slot.range in targets:
|
|
@@ -250,7 +253,7 @@ class ERDiagramGenerator(Generator):
|
|
|
250
253
|
cls = sv.get_class(class_name)
|
|
251
254
|
if self.exclude_abstract_classes and cls.abstract:
|
|
252
255
|
return
|
|
253
|
-
entity = Entity(name=camelcase(cls.name))
|
|
256
|
+
entity = Entity(name=cls.name if self.preserve_names else camelcase(cls.name))
|
|
254
257
|
diagram.entities.append(entity)
|
|
255
258
|
for slot in sv.class_induced_slots(class_name):
|
|
256
259
|
# TODO: schemaview should infer this
|
|
@@ -280,7 +283,9 @@ class ERDiagramGenerator(Generator):
|
|
|
280
283
|
rel = Relationship(
|
|
281
284
|
first_entity=entity.name,
|
|
282
285
|
relationship_type=rel_type,
|
|
283
|
-
second_entity=
|
|
286
|
+
second_entity=(
|
|
287
|
+
sv.get_class(slot.range).name if self.preserve_names else camelcase(sv.get_class(slot.range).name)
|
|
288
|
+
),
|
|
284
289
|
relationship_label=slot.name,
|
|
285
290
|
)
|
|
286
291
|
diagram.relationships.append(rel)
|
|
@@ -299,7 +304,7 @@ class ERDiagramGenerator(Generator):
|
|
|
299
304
|
if slot.multivalued:
|
|
300
305
|
# NOTE: mermaid does not support []s or *s in attribute types
|
|
301
306
|
dt = f"{dt}List"
|
|
302
|
-
attr = Attribute(name=underscore(slot.name), datatype=dt)
|
|
307
|
+
attr = Attribute(name=(slot.name if self.preserve_names else underscore(slot.name)), datatype=dt)
|
|
303
308
|
entity.attributes.append(attr)
|
|
304
309
|
|
|
305
310
|
|
|
@@ -324,7 +329,6 @@ class ERDiagramGenerator(Generator):
|
|
|
324
329
|
default=False,
|
|
325
330
|
help="If True, follow references even if not inlined",
|
|
326
331
|
)
|
|
327
|
-
@click.option("--format", "-f", default="markdown", type=click.Choice(ERDiagramGenerator.valid_formats))
|
|
328
332
|
@click.option("--max-hops", default=None, type=click.INT, help="Maximum number of hops")
|
|
329
333
|
@click.option("--classes", "-c", multiple=True, help="List of classes to serialize")
|
|
330
334
|
@click.option("--include-upstream", is_flag=True, help="Include upstream classes")
|