linkml 1.7.8__py3-none-any.whl → 1.7.10__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.
@@ -0,0 +1,255 @@
1
+ """
2
+ Utilities for deprecating functionality and dependencies.
3
+
4
+ - Emitting DeprecationWarnings
5
+ - Tracking deprecated and removed in versions
6
+ - Fail tests when something marked as removed_in is still present in the specified version
7
+
8
+ Initial draft for deprecating Pydantic 1, to make more general, needs
9
+ - function wrapper version
10
+ - ...
11
+
12
+ To deprecate something:
13
+
14
+ - Create a :class:`.Deprecation` object within the `DEPRECATIONS` tuple
15
+ - Use the :func:`.deprecation_warning` function wherever the deprecated feature would be used to emit the warning
16
+
17
+ """
18
+
19
+ import re
20
+ import warnings
21
+ from dataclasses import dataclass
22
+ from importlib.metadata import version
23
+ from typing import Optional
24
+
25
+ # Stolen from https://github.com/pypa/packaging/blob/main/src/packaging/version.py
26
+ # Updated to include major, minor, and patch versions
27
+ PEP440_PATTERN = r"""
28
+ v?
29
+ (?:
30
+ (?:(?P<epoch>[0-9]+)!)? # epoch
31
+ (?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)
32
+ (?P<pre> # pre-release
33
+ [-_\.]?
34
+ (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
35
+ [-_\.]?
36
+ (?P<pre_n>[0-9]+)?
37
+ )?
38
+ (?P<post> # post release
39
+ (?:-(?P<post_n1>[0-9]+))
40
+ |
41
+ (?:
42
+ [-_\.]?
43
+ (?P<post_l>post|rev|r)
44
+ [-_\.]?
45
+ (?P<post_n2>[0-9]+)?
46
+ )
47
+ )?
48
+ (?P<dev> # dev release
49
+ [-_\.]?
50
+ (?P<dev_l>dev)
51
+ [-_\.]?
52
+ (?P<dev_n>[0-9]+)?
53
+ )?
54
+ )
55
+ (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
56
+ """
57
+ PEP440 = re.compile(r"^\s*" + PEP440_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
58
+
59
+
60
+ @dataclass
61
+ class SemVer:
62
+ """
63
+ Representation of semantic version that supports inequality comparisons.
64
+
65
+ .. note::
66
+
67
+ The inequality methods _only_ test the numeric major, minor, and patch components
68
+ of the version - ie. they do not evaluate the prerelease versions as described in the semver
69
+ spec. This is not intended to be a general SemVer inequality calculator, but used
70
+ only for testing deprecations
71
+
72
+ """
73
+
74
+ major: int = 0
75
+ minor: int = 0
76
+ patch: int = 0
77
+ epoch: Optional[int] = None
78
+ pre: Optional[str] = None
79
+ pre_l: Optional[str] = None
80
+ pre_n: Optional[str] = None
81
+ post: Optional[str] = None
82
+ post_n1: Optional[str] = None
83
+ post_l: Optional[str] = None
84
+ post_n2: Optional[str] = None
85
+ dev: Optional[str] = None
86
+ dev_l: Optional[str] = None
87
+ dev_n: Optional[str] = None
88
+ local: Optional[str] = None
89
+
90
+ def __post_init__(self):
91
+ self.major = int(self.major)
92
+ self.minor = int(self.minor)
93
+ self.patch = int(self.patch)
94
+
95
+ @classmethod
96
+ def from_str(cls, v: str) -> Optional["SemVer"]:
97
+ """
98
+ Create a SemVer from a string using `PEP 440 <https://peps.python.org/pep-0440/>`_
99
+ syntax.
100
+
101
+ Examples:
102
+
103
+ .. code-block:: python
104
+
105
+ >>> version = SemVer.from_str("v0.1.0")
106
+ >>> print(version)
107
+ 0.1.0
108
+
109
+ """
110
+ match = PEP440.search(v)
111
+ if match is None:
112
+ return None
113
+ return SemVer(**match.groupdict())
114
+
115
+ @classmethod
116
+ def from_package(cls, package: str) -> "SemVer":
117
+ """Get semver from package name"""
118
+ v = version(package)
119
+ return SemVer.from_str(v)
120
+
121
+ def __eq__(self, other: "SemVer"):
122
+ return self.major == other.major and self.minor == other.minor and self.patch == other.patch
123
+
124
+ def __lt__(self, other: "SemVer"):
125
+ # fall through each if elif only if version component is equal
126
+ for field in ("major", "minor", "patch"):
127
+ if getattr(self, field) < getattr(other, field):
128
+ return True
129
+ elif getattr(self, field) > getattr(other, field):
130
+ return False
131
+
132
+ # otherwise, equal (which is False)
133
+ return False
134
+
135
+ def __gt__(self, other: "SemVer"):
136
+ return not (self < other) and not (self == other)
137
+
138
+ def __le__(self, other: "SemVer"):
139
+ return (self < other) or (self == other)
140
+
141
+ def __ge__(self, other: "SemVer"):
142
+ return (self > other) or (self == other)
143
+
144
+ def __str__(self) -> str:
145
+ return ".".join([str(item) for item in [self.major, self.minor, self.patch]])
146
+
147
+
148
+ @dataclass
149
+ class Deprecation:
150
+ """
151
+ Parameterization of a deprecation.
152
+ """
153
+
154
+ name: str
155
+ """Shorthand, unique name used to refer to this deprecation"""
156
+ message: str
157
+ """Message to be displayed explaining the deprecation"""
158
+ deprecated_in: SemVer
159
+ """Version that the feature was deprecated in"""
160
+ removed_in: Optional[SemVer] = None
161
+ """Version that the feature will be removed in"""
162
+ recommendation: Optional[str] = None
163
+ """Recommendation about what to do to replace the deprecated behavior"""
164
+ issue: Optional[int] = None
165
+ """GitHub version describing deprecation"""
166
+
167
+ def __post_init__(self):
168
+ if self.deprecated_in is not None and isinstance(self.deprecated_in, str):
169
+ self.deprecated_in = SemVer.from_str(self.deprecated_in)
170
+ if self.removed_in is not None and isinstance(self.removed_in, str):
171
+ self.removed_in = SemVer.from_str(self.removed_in)
172
+
173
+ def __str__(self) -> str:
174
+ msg = f"[{self.name}] "
175
+ if self.removed:
176
+ msg += "REMOVED"
177
+ elif self.deprecated:
178
+ msg += "DEPRECATED"
179
+
180
+ msg += f"\n{self.message}"
181
+ msg += f"\nDeprecated In: {str(self.deprecated_in)}"
182
+ if self.removed_in is not None:
183
+ msg += f"\nRemoved In: {str(self.removed_in)}"
184
+ if self.recommendation is not None:
185
+ msg += f"\nRecommendation: {self.recommendation}"
186
+ if self.issue is not None:
187
+ msg += f"\nSee: https://github.com/linkml/linkml/issues/{self.issue}"
188
+ return msg
189
+
190
+ @property
191
+ def deprecated(self) -> bool:
192
+ return SemVer.from_package("linkml") >= self.deprecated_in
193
+
194
+ @property
195
+ def removed(self) -> bool:
196
+ if self.removed_in is None:
197
+ return False
198
+ return SemVer.from_package("linkml") >= self.removed_in
199
+
200
+ def warn(self, **kwargs):
201
+ if self.deprecated:
202
+ warnings.warn(message=str(self), category=DeprecationWarning, stacklevel=3, **kwargs)
203
+
204
+
205
+ DEPRECATIONS = (
206
+ Deprecation(
207
+ name="pydanticgen-v1",
208
+ deprecated_in=SemVer.from_str("1.7.5"),
209
+ removed_in=SemVer.from_str("1.8.0"),
210
+ message="Support for generating Pydantic v1.*.* models with pydanticgen is deprecated",
211
+ recommendation="Migrate any existing models to Pydantic v2",
212
+ issue=1925,
213
+ ),
214
+ Deprecation(
215
+ name="pydantic-v1",
216
+ deprecated_in=SemVer.from_str("1.7.5"),
217
+ removed_in=SemVer.from_str("1.9.0"),
218
+ message=(
219
+ "LinkML will set a dependency of pydantic>=2 and become incompatible "
220
+ "with packages with pydantic<2 as a runtime dependency"
221
+ ),
222
+ recommendation="Update dependent packages to use pydantic>=2",
223
+ issue=1925,
224
+ ),
225
+ ) # type: tuple[Deprecation, ...]
226
+
227
+ EMITTED = set() # type: set[str]
228
+
229
+
230
+ def deprecation_warning(name: str):
231
+ """
232
+ Call this with the name of the deprecation object wherever the deprecated functionality will be used
233
+
234
+ This function will
235
+
236
+ - emit a warning if the current version is greater than ``deprecated_in``
237
+ - log that the deprecated feature was accessed in ``EMITTED`` for testing deprecations and muting warnings
238
+
239
+ """
240
+ global DEPRECATIONS
241
+ global EMITTED
242
+
243
+ dep = [dep for dep in DEPRECATIONS if dep.name == name]
244
+ if len(dep) == 1:
245
+ dep = dep[0]
246
+ elif len(dep) > 1:
247
+ raise RuntimeError(f"Duplicate deprecations found with name {name}")
248
+ else:
249
+ EMITTED.add(name)
250
+ return
251
+
252
+ if dep.name not in EMITTED:
253
+ dep.warn()
254
+
255
+ EMITTED.add(name)
linkml/utils/generator.py CHANGED
@@ -20,10 +20,8 @@ import logging
20
20
  import os
21
21
  import re
22
22
  import sys
23
- from contextlib import redirect_stdout
24
23
  from dataclasses import dataclass, field
25
24
  from functools import lru_cache
26
- from io import StringIO
27
25
  from pathlib import Path
28
26
  from typing import Callable, ClassVar, Dict, List, Mapping, Optional, Set, TextIO, Type, Union, cast
29
27
 
@@ -176,6 +174,9 @@ class Generator(metaclass=abc.ABCMeta):
176
174
  stacktrace: bool = False
177
175
  """True means print stack trace, false just error message"""
178
176
 
177
+ include: Optional[Union[str, Path, SchemaDefinition]] = None
178
+ """If set, include extra schema outside of the imports mechanism"""
179
+
179
180
  def __post_init__(self) -> None:
180
181
  if not self.logger:
181
182
  self.logger = logging.getLogger()
@@ -204,7 +205,12 @@ class Generator(metaclass=abc.ABCMeta):
204
205
  else:
205
206
  logging.info(f"Using SchemaView with im={self.importmap} // base_dir={self.base_dir}")
206
207
  self.schemaview = SchemaView(schema, importmap=self.importmap, base_dir=self.base_dir)
208
+ if self.include:
209
+ if isinstance(self.include, (str, Path)):
210
+ self.include = SchemaView(self.include, importmap=self.importmap, base_dir=self.base_dir).schema
211
+ self.schemaview.merge_schema(self.include)
207
212
  self.schema = self.schemaview.schema
213
+
208
214
  self._init_namespaces()
209
215
 
210
216
  def _initialize_using_schemaloader(self, schema: Union[str, TextIO, SchemaDefinition, "Generator"]):
@@ -267,66 +273,82 @@ class Generator(metaclass=abc.ABCMeta):
267
273
  :param kwargs: Generator specific parameters
268
274
  :return: Generated output
269
275
  """
270
- output = StringIO()
271
- # Note: we currently redirect stdout, this means that print statements within
272
- # each generator will be redirected to the StringIO object.
273
- # See https://github.com/linkml/linkml/issues/923 for discussion on simplifying this
274
- with redirect_stdout(output):
275
- # the default is to use the Visitor Pattern; each individual generator may
276
- # choose to override methods {visit,end}_{element}.
277
- # See https://github.com/linkml/linkml/issues/923
278
- self.visit_schema(**kwargs)
279
- for sn, ss in (
280
- sorted(self.schema.subsets.items(), key=lambda s: s[0].lower())
281
- if self.visits_are_sorted
282
- else self.schema.subsets.items()
283
- ):
284
- self.visit_subset(ss)
285
- for tn, typ in (
286
- sorted(self.schema.types.items(), key=lambda s: s[0].lower())
287
- if self.visits_are_sorted
288
- else self.schema.types.items()
289
- ):
290
- self.visit_type(typ)
291
- for enum in (
292
- sorted(self.schema.enums.values(), key=lambda e: e.name.lower())
293
- if self.visits_are_sorted
294
- else self.schema.enums.values()
295
- ):
296
- self.visit_enum(enum)
297
- for sn, slot in (
298
- sorted(self.schema.slots.items(), key=lambda c: c[0].lower())
299
- if self.visits_are_sorted
300
- else self.schema.slots.items()
301
- ):
302
- self.visit_slot(self.aliased_slot_name(slot), slot)
303
- for cls in (
304
- sorted(self.schema.classes.values(), key=lambda c: c.name.lower())
305
- if self.visits_are_sorted
306
- else self.schema.classes.values()
307
- ):
308
- if self.visit_class(cls):
309
- for slot in self.all_slots(cls) if self.visit_all_class_slots else self.own_slots(cls):
310
- self.visit_class_slot(cls, self.aliased_slot_name(slot), slot)
311
- self.end_class(cls)
312
- self.end_schema(**kwargs)
313
- return output.getvalue()
314
-
315
- def visit_schema(self, **kwargs) -> None:
276
+ out = ""
277
+
278
+ # the default is to use the Visitor Pattern; each individual generator may
279
+ # choose to override methods {visit,end}_{element}.
280
+ # See https://github.com/linkml/linkml/issues/923
281
+ sub_out = self.visit_schema(**kwargs)
282
+ if sub_out is not None:
283
+ out += sub_out
284
+ for sn, ss in (
285
+ sorted(self.schema.subsets.items(), key=lambda s: s[0].lower())
286
+ if self.visits_are_sorted
287
+ else self.schema.subsets.items()
288
+ ):
289
+ sub_out = self.visit_subset(ss)
290
+ if sub_out is not None:
291
+ out += sub_out
292
+ for tn, typ in (
293
+ sorted(self.schema.types.items(), key=lambda s: s[0].lower())
294
+ if self.visits_are_sorted
295
+ else self.schema.types.items()
296
+ ):
297
+ sub_out = self.visit_type(typ)
298
+ if sub_out is not None:
299
+ out += sub_out
300
+ for enum in (
301
+ sorted(self.schema.enums.values(), key=lambda e: e.name.lower())
302
+ if self.visits_are_sorted
303
+ else self.schema.enums.values()
304
+ ):
305
+ sub_out = self.visit_enum(enum)
306
+ if sub_out is not None:
307
+ out += sub_out
308
+ for sn, slot in (
309
+ sorted(self.schema.slots.items(), key=lambda c: c[0].lower())
310
+ if self.visits_are_sorted
311
+ else self.schema.slots.items()
312
+ ):
313
+ sub_out = self.visit_slot(self.aliased_slot_name(slot), slot)
314
+ if sub_out is not None:
315
+ out += sub_out
316
+ for cls in (
317
+ sorted(self.schema.classes.values(), key=lambda c: c.name.lower())
318
+ if self.visits_are_sorted
319
+ else self.schema.classes.values()
320
+ ):
321
+ cls_out = self.visit_class(cls)
322
+ if cls_out:
323
+ if isinstance(cls_out, str):
324
+ out += cls_out
325
+ for slot in self.all_slots(cls) if self.visit_all_class_slots else self.own_slots(cls):
326
+ sub_out = self.visit_class_slot(cls, self.aliased_slot_name(slot), slot)
327
+ if sub_out is not None:
328
+ out += sub_out
329
+ sub_out = self.end_class(cls)
330
+ if sub_out is not None:
331
+ out += sub_out
332
+ sub_out = self.end_schema(**kwargs)
333
+ if sub_out is not None:
334
+ out += sub_out
335
+ return out
336
+
337
+ def visit_schema(self, **kwargs) -> Optional[str]:
316
338
  """Visited once at the beginning of generation
317
339
 
318
340
  @param kwargs: Arguments passed through from CLI -- implementation dependent
319
341
  """
320
342
  ...
321
343
 
322
- def end_schema(self, **kwargs) -> None:
344
+ def end_schema(self, **kwargs) -> Optional[str]:
323
345
  """Visited once at the end of generation
324
346
 
325
347
  @param kwargs: Arguments passed through from CLI -- implementation dependent
326
348
  """
327
349
  ...
328
350
 
329
- def visit_class(self, cls: ClassDefinition) -> bool:
351
+ def visit_class(self, cls: ClassDefinition) -> Optional[Union[str, bool]]:
330
352
  """Visited once per schema class
331
353
 
332
354
  @param cls: class being visited
@@ -334,14 +356,14 @@ class Generator(metaclass=abc.ABCMeta):
334
356
  """
335
357
  return True
336
358
 
337
- def end_class(self, cls: ClassDefinition) -> None:
359
+ def end_class(self, cls: ClassDefinition) -> Optional[str]:
338
360
  """Visited after visit_class_slots (if visit_class returned true)
339
361
 
340
362
  @param cls: class being visited
341
363
  """
342
364
  ...
343
365
 
344
- def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> None:
366
+ def visit_class_slot(self, cls: ClassDefinition, aliased_slot_name: str, slot: SlotDefinition) -> Optional[str]:
345
367
  """Visited for each slot in a class. If class level visit_all_slots is true, this is visited once
346
368
  for any class that is inherited (class itself, is_a, mixin, apply_to). Otherwise, just the own slots.
347
369
 
@@ -351,7 +373,7 @@ class Generator(metaclass=abc.ABCMeta):
351
373
  """
352
374
  ...
353
375
 
354
- def visit_slot(self, aliased_slot_name: str, slot: SlotDefinition) -> None:
376
+ def visit_slot(self, aliased_slot_name: str, slot: SlotDefinition) -> Optional[str]:
355
377
  """Visited once for every slot definition in the schema.
356
378
 
357
379
  @param aliased_slot_name: Aliased name of the slot. May not be unique
@@ -359,21 +381,21 @@ class Generator(metaclass=abc.ABCMeta):
359
381
  """
360
382
  ...
361
383
 
362
- def visit_type(self, typ: TypeDefinition) -> None:
384
+ def visit_type(self, typ: TypeDefinition) -> Optional[str]:
363
385
  """Visited once for every type definition in the schema
364
386
 
365
387
  @param typ: Type definition
366
388
  """
367
389
  ...
368
390
 
369
- def visit_subset(self, subset: SubsetDefinition) -> None:
391
+ def visit_subset(self, subset: SubsetDefinition) -> Optional[str]:
370
392
  """Visited once for every subset definition in the schema
371
393
 
372
394
  #param subset: Subset definition
373
395
  """
374
396
  ...
375
397
 
376
- def visit_enum(self, enum: EnumDefinition) -> None:
398
+ def visit_enum(self, enum: EnumDefinition) -> Optional[str]:
377
399
  """Visited once for every enum definition in the schema
378
400
 
379
401
  @param enum: Enum definition
linkml/validator/cli.py CHANGED
@@ -108,6 +108,13 @@ DEPRECATED = "[DEPRECATED: only used in legacy mode]"
108
108
  help=f"{DEPRECATED} When handling range constraints, include all descendants of the range "
109
109
  "class instead of just the range class",
110
110
  )
111
+ @click.option(
112
+ "--include-context/--no-include-context",
113
+ "-D",
114
+ default=False,
115
+ show_default=True,
116
+ help="Include additional context when reporting of validation errors.",
117
+ )
111
118
  @click.argument("data_sources", nargs=-1, type=click.Path(exists=True))
112
119
  @click.version_option(__version__, "-V", "--version")
113
120
  @click.pass_context
@@ -123,6 +130,7 @@ def cli(
123
130
  input_format: Optional[str],
124
131
  index_slot: Optional[str],
125
132
  include_range_class_descendants: bool,
133
+ include_context: bool,
126
134
  ):
127
135
  if legacy_mode:
128
136
  from linkml.validators import jsonschemavalidator
@@ -183,6 +191,9 @@ def cli(
183
191
  for result in validator.iter_results_from_source(loader, config.target_class):
184
192
  severity_counter[result.severity] += 1
185
193
  click.echo(f"[{result.severity.value}] [{loader.source}/{result.instance_index}] {result.message}")
194
+ if include_context:
195
+ for ctx in result.context:
196
+ click.echo(f"[CONTEXT] {ctx}")
186
197
 
187
198
  if sum(severity_counter.values()) == 0:
188
199
  click.echo("No issues found")
@@ -46,6 +46,7 @@ class JsonschemaValidationPlugin(ValidationPlugin):
46
46
  path_override=self.json_schema_path,
47
47
  )
48
48
  for error in validator.iter_errors(instance):
49
+ error_context = [ctx.message for ctx in error.context]
49
50
  best_error = best_match([error])
50
51
  yield ValidationResult(
51
52
  type="jsonschema validation",
@@ -53,4 +54,5 @@ class JsonschemaValidationPlugin(ValidationPlugin):
53
54
  instance=instance,
54
55
  instantiates=context.target_class,
55
56
  message=f"{best_error.message} in /{'/'.join(str(p) for p in best_error.absolute_path)}",
57
+ context=error_context,
56
58
  )
@@ -28,6 +28,7 @@ class ValidationResult(BaseModel):
28
28
  instance: Optional[Any] = None
29
29
  instance_index: Optional[int] = None
30
30
  instantiates: Optional[str] = None
31
+ context: List[str] = []
31
32
 
32
33
 
33
34
  class ValidationReport(BaseModel):
@@ -20,6 +20,7 @@ from linkml_runtime.dumpers import json_dumper, rdflib_dumper, yaml_dumper
20
20
  from linkml_runtime.linkml_model import ElementName
21
21
  from linkml_runtime.utils.formatutils import camelcase
22
22
 
23
+ from linkml._version import __version__
23
24
  from linkml.generators.pythongen import PythonGenerator
24
25
  from linkml.validator import Validator, _get_default_validator
25
26
 
@@ -298,6 +299,7 @@ class ExampleRunner:
298
299
  show_default=True,
299
300
  help="If true use type_designators to deepen ranges",
300
301
  )
302
+ @click.version_option(__version__, "-V", "--version")
301
303
  def cli(schema, prefixes, output: TextIO, **kwargs):
302
304
  """Process a folder of examples and a folder of counter examples.
303
305
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: linkml
3
- Version: 1.7.8
3
+ Version: 1.7.10
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