linkml 1.8.1__py3-none-any.whl → 1.8.3__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.
Files changed (78) hide show
  1. linkml/cli/__init__.py +0 -0
  2. linkml/cli/__main__.py +4 -0
  3. linkml/cli/main.py +130 -0
  4. linkml/generators/common/build.py +105 -0
  5. linkml/generators/common/ifabsent_processor.py +286 -0
  6. linkml/generators/common/lifecycle.py +124 -0
  7. linkml/generators/common/template.py +89 -0
  8. linkml/generators/csvgen.py +1 -1
  9. linkml/generators/docgen/slot.md.jinja2 +4 -0
  10. linkml/generators/docgen.py +1 -1
  11. linkml/generators/dotgen.py +1 -1
  12. linkml/generators/erdiagramgen.py +1 -1
  13. linkml/generators/excelgen.py +1 -1
  14. linkml/generators/golanggen.py +1 -1
  15. linkml/generators/golrgen.py +1 -1
  16. linkml/generators/graphqlgen.py +1 -1
  17. linkml/generators/javagen.py +1 -1
  18. linkml/generators/jsonldcontextgen.py +4 -5
  19. linkml/generators/jsonldgen.py +1 -1
  20. linkml/generators/jsonschemagen.py +69 -22
  21. linkml/generators/linkmlgen.py +1 -1
  22. linkml/generators/markdowngen.py +1 -1
  23. linkml/generators/namespacegen.py +1 -1
  24. linkml/generators/oocodegen.py +2 -1
  25. linkml/generators/owlgen.py +1 -1
  26. linkml/generators/plantumlgen.py +1 -1
  27. linkml/generators/prefixmapgen.py +1 -1
  28. linkml/generators/projectgen.py +1 -1
  29. linkml/generators/protogen.py +1 -1
  30. linkml/generators/pydanticgen/__init__.py +8 -3
  31. linkml/generators/pydanticgen/array.py +114 -194
  32. linkml/generators/pydanticgen/build.py +64 -25
  33. linkml/generators/pydanticgen/includes.py +1 -31
  34. linkml/generators/pydanticgen/pydanticgen.py +621 -276
  35. linkml/generators/pydanticgen/template.py +152 -184
  36. linkml/generators/pydanticgen/templates/attribute.py.jinja +9 -7
  37. linkml/generators/pydanticgen/templates/base_model.py.jinja +0 -13
  38. linkml/generators/pydanticgen/templates/class.py.jinja +2 -2
  39. linkml/generators/pydanticgen/templates/footer.py.jinja +2 -10
  40. linkml/generators/pydanticgen/templates/module.py.jinja +2 -2
  41. linkml/generators/pydanticgen/templates/validator.py.jinja +0 -4
  42. linkml/generators/python/__init__.py +1 -0
  43. linkml/generators/python/python_ifabsent_processor.py +92 -0
  44. linkml/generators/pythongen.py +19 -23
  45. linkml/generators/rdfgen.py +1 -1
  46. linkml/generators/shacl/__init__.py +1 -3
  47. linkml/generators/shacl/shacl_data_type.py +1 -1
  48. linkml/generators/shacl/shacl_ifabsent_processor.py +89 -0
  49. linkml/generators/shaclgen.py +11 -5
  50. linkml/generators/shexgen.py +1 -1
  51. linkml/generators/sparqlgen.py +1 -1
  52. linkml/generators/sqlalchemygen.py +1 -1
  53. linkml/generators/sqltablegen.py +1 -1
  54. linkml/generators/sssomgen.py +1 -1
  55. linkml/generators/summarygen.py +1 -1
  56. linkml/generators/terminusdbgen.py +7 -4
  57. linkml/generators/typescriptgen.py +1 -1
  58. linkml/generators/yamlgen.py +1 -1
  59. linkml/generators/yumlgen.py +1 -1
  60. linkml/linter/cli.py +1 -1
  61. linkml/transformers/logical_model_transformer.py +117 -18
  62. linkml/utils/converter.py +1 -1
  63. linkml/utils/execute_tutorial.py +2 -0
  64. linkml/utils/logictools.py +142 -29
  65. linkml/utils/schema_builder.py +7 -6
  66. linkml/utils/schema_fixer.py +1 -1
  67. linkml/utils/sqlutils.py +1 -1
  68. linkml/validator/cli.py +4 -1
  69. linkml/validators/jsonschemavalidator.py +1 -1
  70. linkml/validators/sparqlvalidator.py +1 -1
  71. linkml/workspaces/example_runner.py +1 -1
  72. {linkml-1.8.1.dist-info → linkml-1.8.3.dist-info}/METADATA +2 -2
  73. {linkml-1.8.1.dist-info → linkml-1.8.3.dist-info}/RECORD +76 -68
  74. {linkml-1.8.1.dist-info → linkml-1.8.3.dist-info}/entry_points.txt +1 -1
  75. linkml/generators/shacl/ifabsent_processor.py +0 -59
  76. linkml/utils/ifabsent_functions.py +0 -138
  77. {linkml-1.8.1.dist-info → linkml-1.8.3.dist-info}/LICENSE +0 -0
  78. {linkml-1.8.1.dist-info → linkml-1.8.3.dist-info}/WHEEL +0 -0
@@ -1,23 +1,67 @@
1
1
  """
2
- A model transformer that transforms a schema into logical form with inheritance remaoved.
2
+ A model transformer that transforms a schema into logical form.
3
3
 
4
4
  Formally, this transformer generates a logical model of the schema in which inheritance
5
- of slots/attributes is replaced by boolean conjunctions.
5
+ of slots/attributes is replaced by boolean conjunctions, and the final form is simplified.
6
+
7
+ For example, given a slot `s`:
8
+
9
+ ```
10
+ slots:
11
+ s:
12
+ range: integer
13
+ minimum_value: 1
14
+ ```
15
+
16
+ Which is reused and refined by C and C (where C is_a D):
17
+
18
+ ```
19
+ classes:
20
+ D:
21
+ slots:
22
+ - s
23
+ slot_usage:
24
+ s:
25
+ minimum_value: 2
26
+ C:
27
+ is_a: D
28
+ slot_usage:
29
+ s:
30
+ minimum_value: 3
31
+ ```
32
+
33
+ The hierarchy unrolling step will create an attribute `s` for C that is the conjunction of usages for that
34
+ class, ancestors, and the base slot:
35
+
36
+ ```
37
+ s:
38
+ range: integer
39
+ all_of:
40
+ - minimum_value: 1
41
+ - minimum_value: 2
42
+ - minimum_value: 3
43
+ ```
44
+
45
+ This is then simplified using symbolic reasoning to:
46
+
47
+ ```
48
+ s:
49
+ range: integer
50
+ minimum_value: 3
51
+ ```
52
+
53
+ See logictools.py for the symbolic reasoning engine.
54
+
6
55
 
7
- For example, given a slot s and two classes C and D, where C is_a D. If both C and D
8
- place constraints on s (for example, C may refine the range, or make more restricted
9
- value constraints), then an attribute is generated for C that for which the conjunction
10
- of constraints in both classes hold.
11
56
 
12
- These logical constraints are then simplified by translating to disjunctive normal form,
13
- and applying simplification rules.
14
57
  """
15
58
 
16
59
  import logging
17
60
  from copy import deepcopy
18
61
  from dataclasses import dataclass
19
- from typing import Any, Callable, Dict, Iterator, List, Union
62
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Union
20
63
 
64
+ from linkml_runtime import SchemaView
21
65
  from linkml_runtime.dumpers import json_dumper
22
66
  from linkml_runtime.linkml_model import ClassDefinitionName, SchemaDefinition
23
67
  from linkml_runtime.linkml_model.meta import (
@@ -37,6 +81,8 @@ HERITABLE_METASLOT = [
37
81
  "range_expression",
38
82
  "minimum_value",
39
83
  "maximum_value",
84
+ "minimum_cardinality",
85
+ "maximum_cardinality",
40
86
  "pattern",
41
87
  "structured_pattern",
42
88
  "required",
@@ -64,6 +110,8 @@ DIRECT_ROLL_DOWN = ["multivalued", "key", "identifier", "designates_type"]
64
110
  HERITABLE_TYPE_METASLOT = [
65
111
  "minimum_value",
66
112
  "maximum_value",
113
+ # "minimum_cardinality",
114
+ # "maximum_cardinality",
67
115
  "pattern",
68
116
  "structured_pattern",
69
117
  "all_of",
@@ -103,6 +151,7 @@ class LogicalModelTransformer(ModelTransformer):
103
151
  >>> _ = sb.add_class("Person", slots={"age": {"range": "integer"}}, is_a="Thing")
104
152
  >>> _ = sb.add_class("Organization", slots={"category": {"range": "OrganizationType"}}, is_a="Thing")
105
153
  >>> _ = sb.add_enum("OrganizationType", ["commercial", "non-profit"])
154
+ >>> _ = sb.add_defaults()
106
155
  >>> from linkml.transformers.logical_model_transformer import LogicalModelTransformer
107
156
  >>> tr = LogicalModelTransformer()
108
157
  >>> tr.set_schema(sb.schema)
@@ -113,8 +162,10 @@ class LogicalModelTransformer(ModelTransformer):
113
162
  attributes:
114
163
  id:
115
164
  name: id
165
+ range: string
116
166
  name:
117
167
  name: name
168
+ range: string
118
169
  age:
119
170
  name: age
120
171
  range: integer
@@ -142,6 +193,10 @@ class LogicalModelTransformer(ModelTransformer):
142
193
 
143
194
  ...
144
195
 
196
+ This has "compiled away" the hierarchy. The type of the values of 'entities' for Container can
197
+ be checked against the any_of expression, without needing to know the full class hierarchy.
198
+ This is useful when compiling to a framework that does not support inheritance.
199
+
145
200
  If you are compiling to a framework that does have inheritance, then you can specify that these are
146
201
  preserved:
147
202
 
@@ -173,6 +228,9 @@ class LogicalModelTransformer(ModelTransformer):
173
228
  name: entities2
174
229
  multivalued: true
175
230
  any_of:
231
+ - range: Organization
232
+ - range: Person
233
+
176
234
  ...
177
235
 
178
236
  Note that defaults are always applied after reasoning. So if you set a default_range to
@@ -211,6 +269,10 @@ class LogicalModelTransformer(ModelTransformer):
211
269
  minimum_value: 40
212
270
  maximum_value: 60
213
271
 
272
+ Here the slot-level constraints seem to have been "overridden". In fact, LinkML is monotonic, and there
273
+ is no overriding. In fact the constraints have been combined as a conjunction (And), and then the resulting
274
+ entailed attribute has been *simplified* (age >= 40 & age >= 0 ==> age >= 40).
275
+
214
276
  The reasoning engine can also detect unsatisfiable constraints:
215
277
 
216
278
  >>> from linkml.transformers.logical_model_transformer import UnsatisfiableAttribute
@@ -227,6 +289,9 @@ class LogicalModelTransformer(ModelTransformer):
227
289
  Let's get rid of the problematic class:
228
290
 
229
291
  >>> del sb.schema.classes["YoungAdult"]
292
+
293
+ And try a different example - we will try and override the range of the age slot with a string:
294
+
230
295
  >>> _ = sb.add_class("Person2", is_a="Person", slot_usage={"age": {"range": "string"}})
231
296
  >>> tr.set_schema(sb.schema)
232
297
  >>> try:
@@ -237,8 +302,6 @@ class LogicalModelTransformer(ModelTransformer):
237
302
  Attribute Person2.age is unsatisfiable
238
303
  <BLANKLINE>
239
304
 
240
-
241
-
242
305
  """
243
306
 
244
307
  preserve_class_is_a: bool = False
@@ -277,16 +340,24 @@ class LogicalModelTransformer(ModelTransformer):
277
340
  a lack of range assignment.
278
341
  """
279
342
 
280
- def transform(self, tgt_schema_name: str = None, simplify=True, **kwargs) -> Any:
343
+ reason_over_metamodel_slots: Optional[List[str]] = None
344
+ """
345
+ If set, only reason over the specified metamodel slots.
346
+ """
347
+
348
+ def transform(self, tgt_schema_name: str = None, simplify=True, force_any_of=False, **kwargs) -> Any:
281
349
  """
282
- Transform the schema to a logical model, translating inheritance to boolean expressions.
350
+ Transform the schema using logical reasoning, materializing entailed simple attributes.
283
351
 
284
352
  :param tgt_schema_name:
285
353
  :param simplify: If True (default), simplify the schema after transformation
354
+ :param force_any_of: If True, force the use of any_of expressions for range, even for singletons
286
355
  :return:
287
356
  """
288
357
  sv = self.schemaview
289
- target_schema = deepcopy(self.source_schema)
358
+ target_schemaview = SchemaView(deepcopy(self.source_schema))
359
+ target_schemaview.merge_imports()
360
+ target_schema = target_schemaview.schema
290
361
  for tn, typ in target_schema.types.items():
291
362
  ancs = sv.type_ancestors(tn, reflexive=False)
292
363
  logging.debug(f"Unrolling type {tn}, merging {len(ancs)}")
@@ -312,7 +383,7 @@ class LogicalModelTransformer(ModelTransformer):
312
383
  self._roll_down(target_schema, cn, ancs)
313
384
  self.apply_defaults(target_schema)
314
385
  if simplify:
315
- self.simplify(target_schema)
386
+ self.simplify(target_schema, force_any_of=force_any_of)
316
387
  if self.tidy_slots:
317
388
  target_schema.slots = {}
318
389
  if self.tidy_inheritance:
@@ -366,7 +437,8 @@ class LogicalModelTransformer(ModelTransformer):
366
437
  :return:
367
438
  """
368
439
  new_slot_dict = {}
369
- for p in HERITABLE_METASLOT:
440
+ heritable_metaslots = self.reason_over_metamodel_slots or HERITABLE_METASLOT
441
+ for p in heritable_metaslots:
370
442
  pv = getattr(source, p)
371
443
  if pv is not None and pv != [] and pv != {} and pv is not False:
372
444
  new_slot_dict[p] = pv
@@ -377,7 +449,7 @@ class LogicalModelTransformer(ModelTransformer):
377
449
  sv = self.schemaview
378
450
  if source.range in sv.all_types():
379
451
  typ = sv.get_type(source.range)
380
- for p in set(HERITABLE_TYPE_METASLOT).intersection(HERITABLE_METASLOT):
452
+ for p in set(HERITABLE_TYPE_METASLOT).intersection(heritable_metaslots):
381
453
  pv = getattr(typ, p)
382
454
  if pv is not None and pv != [] and pv != {} and pv is not False:
383
455
  new_slot_dict[p] = pv
@@ -395,6 +467,9 @@ class LogicalModelTransformer(ModelTransformer):
395
467
  """
396
468
  new_slot_dict = {}
397
469
  for p in HERITABLE_TYPE_METASLOT:
470
+ if p in ["repr"]:
471
+ # not on anonymous types
472
+ continue
398
473
  pv = getattr(source, p)
399
474
  if pv is not None and pv != [] and pv != {} and pv is not False:
400
475
  new_slot_dict[p] = pv
@@ -431,7 +506,7 @@ class LogicalModelTransformer(ModelTransformer):
431
506
  for sx in att.all_of + att.exactly_one_of + att.none_of + att.any_of:
432
507
  yield from self._collection_slot_expressions(sx, filter_function)
433
508
 
434
- def simplify(self, target_schema: SchemaDefinition):
509
+ def simplify(self, target_schema: SchemaDefinition, force_any_of=False):
435
510
  for cls in target_schema.classes.values():
436
511
  for att in cls.attributes.values():
437
512
  x = self._as_logical_expression(att)
@@ -441,6 +516,9 @@ class LogicalModelTransformer(ModelTransformer):
441
516
  if logictools.is_contradiction(x):
442
517
  raise UnsatisfiableAttribute(f"Attribute {cls.name}.{att.name} is unsatisfiable")
443
518
  self._simplify_member_ofs(x)
519
+ if force_any_of:
520
+ if not isinstance(x, logictools.Or):
521
+ x = logictools.Or(x)
444
522
  logger.debug(f"Simplified member of: {x}")
445
523
  simplified_att = self._from_logical_expression(x)
446
524
  for k, v in simplified_att.__dict__.items():
@@ -524,6 +602,10 @@ class LogicalModelTransformer(ModelTransformer):
524
602
  def _value_var(self) -> logictools.Variable:
525
603
  return logictools.Variable("value")
526
604
 
605
+ @property
606
+ def _length_var(self) -> logictools.Variable:
607
+ return logictools.Variable("length")
608
+
527
609
  @property
528
610
  def _type_var(self) -> logictools.Variable:
529
611
  return logictools.Variable("type")
@@ -535,10 +617,15 @@ class LogicalModelTransformer(ModelTransformer):
535
617
  exprs = []
536
618
  value_var = self._value_var
537
619
  type_var = self._type_var
620
+ length_var = self._length_var
538
621
  if slot_expression.minimum_value is not None:
539
622
  exprs.append(value_var >= slot_expression.minimum_value)
540
623
  if slot_expression.maximum_value is not None:
541
624
  exprs.append(value_var <= slot_expression.maximum_value)
625
+ if slot_expression.minimum_cardinality is not None:
626
+ exprs.append(length_var >= slot_expression.minimum_cardinality)
627
+ if slot_expression.maximum_cardinality is not None:
628
+ exprs.append(length_var <= slot_expression.maximum_cardinality)
542
629
  if slot_expression.pattern is not None:
543
630
  exprs.append(logictools.Term(MATCHES_PREDICATE, value_var, slot_expression.pattern))
544
631
  if slot_expression.range:
@@ -576,6 +663,7 @@ class LogicalModelTransformer(ModelTransformer):
576
663
  "none_of",
577
664
  "any_of",
578
665
  ]:
666
+ # already accounted for
579
667
  continue
580
668
  v = getattr(slot_expression, p, None)
581
669
  if v is not None and v != [] and v != {}:
@@ -624,6 +712,17 @@ class LogicalModelTransformer(ModelTransformer):
624
712
  return AnonymousSlotExpression(maximum_value=val - 1)
625
713
  else:
626
714
  raise ValueError(f"Unknown predicate {expr.predicate} for {var}")
715
+ elif var == self._length_var:
716
+ if expr.predicate == ">=":
717
+ return AnonymousSlotExpression(minimum_cardinality=val)
718
+ elif expr.predicate == ">":
719
+ return AnonymousSlotExpression(minimum_cardinality=val + 1)
720
+ elif expr.predicate == "<=":
721
+ return AnonymousSlotExpression(maximum_cardinality=val)
722
+ elif expr.predicate == "<":
723
+ return AnonymousSlotExpression(maximum_cardinality=val - 1)
724
+ else:
725
+ raise ValueError(f"Unknown predicate {expr.predicate} for {var}")
627
726
  elif var == self._type_var:
628
727
  if expr.predicate == "in":
629
728
  if val == logictools.UniversalSet():
linkml/utils/converter.py CHANGED
@@ -24,7 +24,7 @@ from linkml.utils.datautils import (
24
24
  )
25
25
 
26
26
 
27
- @click.command()
27
+ @click.command(name="convert")
28
28
  @click.option("--module", "-m", help="Path to python datamodel module")
29
29
  @click.option("--output", "-o", help="Path to output file")
30
30
  @click.option(
@@ -189,6 +189,8 @@ def parse_file_to_blocks(input) -> List[Block]:
189
189
  @click.version_option(__version__, "-V", "--version")
190
190
  def cli(inputs, directory):
191
191
  """
192
+ Execute a tutorial markdown file (eg. those in the /docs/intro/ directory) and
193
+ save the outputs in the given directory
192
194
 
193
195
  Example:
194
196
 
@@ -1,7 +1,7 @@
1
1
  import operator
2
2
  from copy import deepcopy
3
3
  from itertools import product
4
- from typing import Any, Collection, List, Optional, Tuple
4
+ from typing import Any, Collection, List, Optional, Tuple, Type
5
5
 
6
6
 
7
7
  def member_of(item: Any, collection: Collection[Any]) -> bool:
@@ -25,6 +25,8 @@ OPS = {
25
25
 
26
26
 
27
27
  class Expression:
28
+ """Base class for logic expressions"""
29
+
28
30
  def __eq__(self, other: "Expression"):
29
31
  if isinstance(other, Expression):
30
32
  return self._ordered_str() == other._ordered_str()
@@ -60,6 +62,7 @@ class Expression:
60
62
 
61
63
  def __contains__(self, other: Any):
62
64
  return Term("contains", self, other)
65
+ return Term("contains", self, other)
63
66
 
64
67
  def _ordered_str(self, **kwargs):
65
68
  return str(self)
@@ -69,6 +72,8 @@ class Expression:
69
72
 
70
73
 
71
74
  class Variable(Expression):
75
+ """A variable in a logic expression"""
76
+
72
77
  def __init__(self, name: str):
73
78
  self.name = name
74
79
 
@@ -80,6 +85,8 @@ class Variable(Expression):
80
85
 
81
86
 
82
87
  class Top(Expression):
88
+ """True expression"""
89
+
83
90
  def __str__(self):
84
91
  return "T"
85
92
 
@@ -88,6 +95,8 @@ class Top(Expression):
88
95
 
89
96
 
90
97
  class Bottom(Expression):
98
+ """False expression"""
99
+
91
100
  def __str__(self):
92
101
  return "F"
93
102
 
@@ -96,6 +105,8 @@ class Bottom(Expression):
96
105
 
97
106
 
98
107
  class Not(Expression):
108
+ """Negation of an expression"""
109
+
99
110
  def __init__(self, operand: Expression):
100
111
  self.operand = operand
101
112
 
@@ -107,6 +118,8 @@ class Not(Expression):
107
118
 
108
119
 
109
120
  class And(Expression):
121
+ """Conjunction of expressions"""
122
+
110
123
  def __init__(self, *operands: Expression):
111
124
  self.operands: List[Expression] = list(operands)
112
125
 
@@ -306,6 +319,45 @@ def distribute_and_over_or(expr: Expression) -> Expression:
306
319
 
307
320
 
308
321
  def is_contradiction(expr: Expression, apply_dnf=False) -> Optional[bool]:
322
+ """
323
+ Check if the given expression is a contradiction.
324
+
325
+ >>> a = Variable("a")
326
+ >>> b = Variable("b")
327
+ >>> c = Variable("c")
328
+ >>> d = Variable("d")
329
+ >>> F = Bottom()
330
+ >>> T = Top()
331
+ >>> is_contradiction(T)
332
+ False
333
+ >>> is_contradiction(F)
334
+ True
335
+ >>> is_contradiction(~T)
336
+ True
337
+ >>> is_contradiction(~F)
338
+ False
339
+ >>> is_contradiction(~~T)
340
+ False
341
+ >>> is_contradiction(~~F)
342
+ True
343
+ >>> is_contradiction(F & F)
344
+ True
345
+ >>> is_contradiction(F & T)
346
+ True
347
+ >>> is_contradiction(F | T)
348
+ False
349
+ >>> is_contradiction(a & F )
350
+ True
351
+ >>> is_contradiction(a & ~a)
352
+ True
353
+ >>> assert is_contradiction(a & T) is None
354
+ >>> assert is_contradiction(a) is None
355
+ >>> assert is_contradiction(And(a)) is None
356
+
357
+ :param expr:
358
+ :param apply_dnf:
359
+ :return:
360
+ """
309
361
  if apply_dnf:
310
362
  expr = to_dnf(expr)
311
363
  expr = simplify(expr)
@@ -354,6 +406,32 @@ def evals_to_true(expr: Expression, apply_dnf=False) -> Optional[bool]:
354
406
  """
355
407
  Two-valued logic
356
408
 
409
+ >>> a = Variable("a")
410
+ >>> b = Variable("b")
411
+ >>> c = Variable("c")
412
+ >>> d = Variable("d")
413
+ >>> F = Bottom()
414
+ >>> T = Top()
415
+ >>> evals_to_true(T)
416
+ True
417
+ >>> evals_to_true(F)
418
+ False
419
+ >>> evals_to_true(~T)
420
+ False
421
+ >>> evals_to_true(~F)
422
+ True
423
+ >>> evals_to_true(a & b)
424
+ False
425
+ >>> evals_to_true(a | b)
426
+ False
427
+ >>> evals_to_true(a & a)
428
+ False
429
+
430
+ Note that this does not perform unification, so the following does not evaluate to True
431
+
432
+ >>> evals_to_true(a & ~a)
433
+ False
434
+
357
435
  :param expr:
358
436
  :param apply_dnf:
359
437
  :return:
@@ -363,7 +441,7 @@ def evals_to_true(expr: Expression, apply_dnf=False) -> Optional[bool]:
363
441
  if isinstance(expr, And):
364
442
  return all([evals_to_true(operand) for operand in expr.operands])
365
443
  elif isinstance(expr, Not):
366
- if is_contradiction(expr.operand) is False:
444
+ if is_contradiction(expr.operand) is True:
367
445
  return True
368
446
  else:
369
447
  return False
@@ -378,32 +456,6 @@ def evals_to_true(expr: Expression, apply_dnf=False) -> Optional[bool]:
378
456
  return False
379
457
 
380
458
 
381
- def is_tautology(expr):
382
- """
383
- Checks if the given propositional logic formula is a tautology.
384
-
385
- A formula is a tautology if it is true under every possible valuation of its variables.
386
-
387
- Args:
388
- expr (Expression): The input formula which is an instance of Expression or its subclasses.
389
-
390
- Returns:
391
- bool: True if the formula is a tautology, False otherwise.
392
- """
393
- if isinstance(expr, Or):
394
- for i in range(len(expr.operands)):
395
- for j in range(i + 1, len(expr.operands)):
396
- if isinstance(expr.operands[i], Not) and expr.operands[i].operand == expr.operands[j]:
397
- return True
398
- elif isinstance(expr.operands[j], Not) and expr.operands[j].operand == expr.operands[i]:
399
- return True
400
- elif isinstance(expr, And):
401
- for operand in expr.operands:
402
- if is_tautology(operand):
403
- return True
404
- return False
405
-
406
-
407
459
  def eliminate_redundant(expr: Expression) -> Expression:
408
460
  if isinstance(expr, And):
409
461
  visited = set()
@@ -444,6 +496,23 @@ def simplify(expr: Expression) -> Expression:
444
496
  """
445
497
  Simplifies the given propositional logic formula.
446
498
 
499
+ >>> a = Variable("a")
500
+ >>> b = Variable("b")
501
+ >>> c = Variable("c")
502
+ >>> d = Variable("d")
503
+ >>> simplify(a & b)
504
+ (a & b)
505
+ >>> simplify(a | b)
506
+ (a | b)
507
+ >>> simplify(a & a)
508
+ a
509
+ >>> simplify(a | a)
510
+ a
511
+ >>> simplify(a & ~a)
512
+ F
513
+ >>> simplify(a | ~a)
514
+ T
515
+
447
516
  :param expr:
448
517
  :return:
449
518
  """
@@ -521,6 +590,30 @@ COMPOSITIONS = {
521
590
 
522
591
 
523
592
  def _unsat(x: Expression, y: Expression) -> bool:
593
+ """
594
+ Check if two terms are unsatisfiable when combined.
595
+
596
+ >>> a = Variable("a")
597
+ >>> b = Variable("b")
598
+ >>> _unsat( a < b, a > b)
599
+ True
600
+ >>> _unsat( a < b, a < b)
601
+ False
602
+ >>> _unsat( a <= b, b >= a)
603
+ False
604
+ >>> _unsat( a < b, a <= b)
605
+ False
606
+ >>> _unsat( a < b, b <= a)
607
+ False
608
+ >>> _unsat( IsIn(a, [1, 2]), IsIn(a, [2, 3]))
609
+ False
610
+ >>> _unsat( IsIn(a, [1, 2]), IsIn(a, [3, 4]))
611
+ True
612
+
613
+ :param x:
614
+ :param y:
615
+ :return:
616
+ """
524
617
  if isinstance(x, Term) and isinstance(y, Term):
525
618
  if x.operands[0] == y.operands[0]:
526
619
  xp = x.predicate
@@ -547,7 +640,27 @@ def _unsat(x: Expression, y: Expression) -> bool:
547
640
  return False
548
641
 
549
642
 
550
- def compose_operators(boolean_op, op1, v1, op2, v2) -> Optional[Tuple]:
643
+ def compose_operators(boolean_op: Type[Expression], op1: str, v1: Any, op2: str, v2: Any) -> Optional[Tuple]:
644
+ """
645
+ Compose two expressions.
646
+
647
+ >>> assert compose_operators(And, '<', 1, '<', 2) == (operator.__lt__, 1)
648
+ >>> assert compose_operators(Or, '<', 1, '<', 2) == (operator.__lt__, 2)
649
+ >>> assert compose_operators(Or, '=', 1, '=', 2) is None
650
+ >>> assert compose_operators(And, '=', 1, '=', 2) is None
651
+ >>> assert compose_operators(And, "in", [1, 2], "in", [2, 3]) == (member_of, [2])
652
+ >>> assert compose_operators(Or, "in", [1, 2], "in", [2, 3]) == (member_of, [1, 2, 3])
653
+ >>> assert compose_operators(And, "in", [1], "in", [3]) == (member_of, [])
654
+ >>> assert compose_operators(And, "in", [1], "in", UniversalSet()) == (member_of, [1])
655
+ >>> assert compose_operators(Or, "in", [1], "in", UniversalSet()) == (member_of, UniversalSet())
656
+
657
+ :param boolean_op:
658
+ :param op1:
659
+ :param v1:
660
+ :param op2:
661
+ :param v2:
662
+ :return: New expression OR None if the expressions cannot be reduced
663
+ """
551
664
  rev_op_map = {v: k for k, v in OPS.items()}
552
665
  if not (op1 in rev_op_map and op2 in rev_op_map):
553
666
  return None
@@ -101,12 +101,13 @@ class SchemaBuilder:
101
101
  for k, v in slots.items():
102
102
  cls.slots.append(k)
103
103
  self.add_slot(SlotDefinition(k, **v), replace_if_present=replace_if_present)
104
- for s in slots:
105
- cls.slots.append(s.name if isinstance(s, SlotDefinition) else s)
106
- if isinstance(s, str) and s in self.schema.slots:
107
- # top-level slot already exists
108
- continue
109
- self.add_slot(s, replace_if_present=replace_if_present)
104
+ else:
105
+ for s in slots:
106
+ cls.slots.append(s.name if isinstance(s, SlotDefinition) else s)
107
+ if isinstance(s, str) and s in self.schema.slots:
108
+ # top-level slot already exists
109
+ continue
110
+ self.add_slot(s, replace_if_present=replace_if_present)
110
111
  if slot_usage:
111
112
  if isinstance(slot_usage, dict):
112
113
  for k, v in slot_usage.items():
@@ -382,7 +382,7 @@ class SchemaFixer:
382
382
  return schema
383
383
 
384
384
 
385
- @click.group()
385
+ @click.group(name="fix")
386
386
  @click.option("-v", "--verbose", count=True)
387
387
  @click.option("-q", "--quiet")
388
388
  def main(verbose: int, quiet: bool):
linkml/utils/sqlutils.py CHANGED
@@ -270,7 +270,7 @@ class SQLStore:
270
270
  return obj
271
271
 
272
272
 
273
- @click.group()
273
+ @click.group(name="sqldb")
274
274
  @click.option("-v", "--verbose", count=True)
275
275
  @click.option("-q", "--quiet")
276
276
  @click.option(
linkml/validator/cli.py CHANGED
@@ -64,7 +64,7 @@ def _resolve_loaders(loader_config: Iterable[Union[str, Dict[str, Dict[str, str]
64
64
  DEPRECATED = "[DEPRECATED: only used in legacy mode]"
65
65
 
66
66
 
67
- @click.command()
67
+ @click.command(name="validate")
68
68
  @click.option(
69
69
  "-s",
70
70
  "--schema",
@@ -132,6 +132,9 @@ def cli(
132
132
  include_range_class_descendants: bool,
133
133
  include_context: bool,
134
134
  ):
135
+ """
136
+ Validate data according to a LinkML Schema
137
+ """
135
138
  if legacy_mode:
136
139
  from linkml.validators import jsonschemavalidator
137
140
 
@@ -114,7 +114,7 @@ class JsonSchemaDataValidator(DataValidator):
114
114
  yield f"{best_error.message} in {best_error.json_path}"
115
115
 
116
116
 
117
- @click.command()
117
+ @click.command(name="jsonschema")
118
118
  @click.option("--module", "-m", help="Path to python datamodel module")
119
119
  @click.option(
120
120
  "--input-format",
@@ -85,7 +85,7 @@ class SparqlDataValidator(DataValidator):
85
85
  return self.schema
86
86
 
87
87
 
88
- @click.command()
88
+ @click.command(name="sparql")
89
89
  @click.option("--named-graph", "-G", multiple=True, help="Constrain query to a named graph")
90
90
  @click.option("--input", "-i", help="Input file to validate")
91
91
  @click.option("--endpoint-url", "-U", help="URL of sparql endpoint")
@@ -255,7 +255,7 @@ class ExampleRunner:
255
255
  return dict_obj
256
256
 
257
257
 
258
- @click.command()
258
+ @click.command(name="examples")
259
259
  @click.option("--schema", "-s", required=True, help="Path to linkml schema yaml file")
260
260
  @click.option("--prefixes", "-P", help="Path to prefixes")
261
261
  @click.option("--input-directory", "-e", help="folder containing positive examples that MUST pass validation")