iolanta 2.0.6__py3-none-any.whl → 2.0.8__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.
iolanta/__init__.py CHANGED
@@ -1,4 +1,3 @@
1
1
  from iolanta.base_plugin import IolantaBase
2
2
  from iolanta.facets import Facet
3
3
  from iolanta.plugin import Plugin
4
- from iolanta.shortcuts import as_document
iolanta/facets/facet.py CHANGED
@@ -85,13 +85,6 @@ class Facet(Generic[FacetOutput]):
85
85
  """Preferred language for Iolanta output."""
86
86
  return self.iolanta.language
87
87
 
88
- def find_triple(
89
- self,
90
- triple: TripleTemplate,
91
- ) -> Triple | None:
92
- """Lightweight procedure to find a triple by template."""
93
- return self.iolanta.find_triple(triple_template=triple)
94
-
95
88
  @cached_property
96
89
  def logger(self):
97
90
  """Logger."""
@@ -291,7 +291,7 @@ class LiteralPropertyValue(Widget, can_focus=True, inherit_bindings=False):
291
291
  self.property_iri = property_iri
292
292
  super().__init__()
293
293
  self.renderable = Text( # noqa: WPS601
294
- str(property_value.value),
294
+ str(property_value),
295
295
  style='#696969',
296
296
  )
297
297
 
@@ -1,5 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
+ import funcy
3
4
  from rich.markdown import Markdown
4
5
  from textual.containers import Vertical
5
6
  from yarl import URL
@@ -15,6 +16,7 @@ TEXT = """
15
16
  * Or, no edges might exist which involve it;
16
17
  * Or maybe Iolanta does not know of such edges.
17
18
  {content}
19
+ {subgraphs}
18
20
  **What can you do?**
19
21
 
20
22
  * If you feel this might indicate a bug 🐛, please do let us know at GitHub
@@ -29,6 +31,12 @@ CONTENT_TEMPLATE = """
29
31
  ```
30
32
  """
31
33
 
34
+ SUBGRAPHS_TEMPLATE = """
35
+ **Subgraphs**
36
+
37
+ {formatted_subgraphs}
38
+ """
39
+
32
40
 
33
41
  class TextualNoFacetFound(Facet):
34
42
  """Facet to handle the case when no facet is found."""
@@ -55,14 +63,38 @@ class TextualNoFacetFound(Facet):
55
63
  content=file_content,
56
64
  type={
57
65
  '.yamlld': 'yaml',
66
+ '.jsonld': 'json',
58
67
  }.get(path.suffix, ''),
59
68
  )
60
69
 
70
+ @property
71
+ def subgraphs_description(self) -> str:
72
+ """Return a formatted description of subgraphs, if any exist."""
73
+ rows = self.query(
74
+ 'SELECT ?subgraph WHERE { $this iolanta:has-sub-graph ?subgraph }',
75
+ this=self.this,
76
+ )
77
+ subgraphs = funcy.lpluck('subgraph', rows)
78
+ if subgraphs:
79
+ return SUBGRAPHS_TEMPLATE.format(
80
+ formatted_subgraphs='\n'.join([
81
+ f'- {subgraph}'
82
+ for subgraph in subgraphs
83
+ ]),
84
+ )
85
+
86
+ return ''
87
+
61
88
  def show(self):
62
89
  """Compose the page."""
63
90
  return Vertical(
64
91
  PageTitle(self.this),
65
92
  Description(
66
- Markdown(TEXT.format(content=self.raw_content or '')),
93
+ Markdown(
94
+ TEXT.format(
95
+ content=self.raw_content or '',
96
+ subgraphs=self.subgraphs_description or '',
97
+ ),
98
+ ),
67
99
  ),
68
100
  )
@@ -9,6 +9,7 @@ PRIORITIES = [ # noqa: WPS407
9
9
  'schema_name',
10
10
  'rdfs_label',
11
11
  'foaf_name',
12
+ 'literal_form',
12
13
  ]
13
14
 
14
15
 
@@ -23,4 +23,9 @@ SELECT * WHERE {
23
23
  $iri <https://xmlns.com/foaf/0.1/name> ?foaf_name .
24
24
  FILTER (!lang(?foaf_name) || lang(?foaf_name) = $language)
25
25
  }
26
- }
26
+
27
+ OPTIONAL {
28
+ $iri <https://www.w3.org/2008/05/skos-xl#literalForm> ?literal_form .
29
+ FILTER (!lang(?literal_form) || lang(?literal_form) = $language)
30
+ }
31
+ }
iolanta/iolanta.py CHANGED
@@ -4,22 +4,19 @@ from pathlib import Path
4
4
  from typing import ( # noqa: WPS235
5
5
  Annotated,
6
6
  Any,
7
- Dict,
8
7
  Iterable,
9
8
  List,
10
9
  Mapping,
11
10
  Optional,
12
11
  Protocol,
13
12
  Set,
14
- Tuple,
15
13
  Type,
16
14
  )
17
15
 
18
- import funcy
19
16
  import loguru
20
17
  import yaml_ld
21
18
  from pyparsing import ParseException
22
- from rdflib import ConjunctiveGraph, Graph, Literal, Namespace, URIRef
19
+ from rdflib import ConjunctiveGraph, Graph, Literal, URIRef
23
20
  from rdflib.namespace import NamespaceManager
24
21
  from rdflib.plugins.sparql.processor import SPARQLResult
25
22
  from rdflib.term import Node
@@ -32,18 +29,9 @@ from iolanta.errors import UnresolvedIRI
32
29
  from iolanta.facets.errors import FacetError
33
30
  from iolanta.facets.facet import Facet
34
31
  from iolanta.facets.locator import FacetFinder
35
- from iolanta.loaders.base import SourceType
36
- from iolanta.loaders.local_directory import merge_contexts
37
- from iolanta.models import (
38
- ComputedQName,
39
- LDContext,
40
- NotLiteralNode,
41
- Triple,
42
- TripleTemplate,
43
- )
32
+ from iolanta.models import ComputedQName, LDContext, NotLiteralNode
44
33
  from iolanta.node_to_qname import node_to_qname
45
34
  from iolanta.parse_quads import parse_quads
46
- from iolanta.parsers.yaml import YAML
47
35
  from iolanta.plugin import Plugin
48
36
  from iolanta.query_result import (
49
37
  QueryResult,
@@ -103,12 +91,6 @@ class Iolanta: # noqa: WPS214
103
91
 
104
92
  logger: LoggerProtocol = loguru.logger
105
93
 
106
- sources_added_not_yet_inferred: list[SourceType] = field(
107
- default_factory=list,
108
- init=False,
109
- repr=False,
110
- )
111
-
112
94
  could_not_retrieve_nodes: Set[Node] = field(
113
95
  default_factory=set,
114
96
  init=False,
@@ -182,23 +164,6 @@ class Iolanta: # noqa: WPS214
182
164
 
183
165
  return format_query_bindings(sparql_result.bindings)
184
166
 
185
- @functools.cached_property
186
- def namespaces_to_bind(self) -> Dict[str, Namespace]:
187
- """
188
- Namespaces globally specified for the graph.
189
-
190
- FIXME: Probably get rid of this, I do not know.
191
- """
192
- return {
193
- key: Namespace(value)
194
- for key, value in self.default_context['@context'].items() # noqa
195
- if (
196
- isinstance(value, str)
197
- and not value.startswith('@') # noqa: W503
198
- and not key.startswith('@') # noqa: W503
199
- )
200
- }
201
-
202
167
  def reset(self):
203
168
  """Reset Iolanta graph."""
204
169
  self.graph = _create_default_graph() # noqa: WPS601
@@ -212,7 +177,6 @@ class Iolanta: # noqa: WPS214
212
177
  ) -> 'Iolanta':
213
178
  """Parse & load information from given URL into the graph."""
214
179
  self.logger.info(f'Adding to graph: {source}')
215
- self.sources_added_not_yet_inferred.append(source)
216
180
 
217
181
  if not isinstance(source, Path):
218
182
  source = Path(source)
@@ -262,17 +226,7 @@ class Iolanta: # noqa: WPS214
262
226
  self.logger.info(f'{source_file} | No data found')
263
227
  continue
264
228
 
265
- quad_tuples = [
266
- tuple([
267
- normalize_term(term) for term in replace(
268
- quad,
269
- graph=graph,
270
- ).as_tuple()
271
- ])
272
- for quad in quads
273
- ]
274
-
275
- self.graph.addN(quad_tuples)
229
+ self.graph.addN(quads)
276
230
 
277
231
  return self
278
232
 
@@ -317,20 +271,6 @@ class Iolanta: # noqa: WPS214
317
271
  if path := plugin.context_path:
318
272
  yield path
319
273
 
320
- @functools.cached_property
321
- def default_context(self) -> LDContext:
322
- """Construct default context from plugins."""
323
- context_documents = [
324
- YAML().as_jsonld_document(path.open('r'))
325
- for path in self.context_paths
326
- ]
327
-
328
- for context in context_documents:
329
- if isinstance(context, list):
330
- raise ValueError('Context cannot be a list: %s', context)
331
-
332
- return merge_contexts(*context_documents) # type: ignore
333
-
334
274
  def add_files_from_plugins(self):
335
275
  """
336
276
  Load files from plugins.
@@ -434,44 +374,6 @@ class Iolanta: # noqa: WPS214
434
374
  error=err,
435
375
  ) from err
436
376
 
437
- def retrieve_triple(self, triple_template: TripleTemplate) -> Triple:
438
- """Retrieve remote data to project directory."""
439
- for plugin in self.plugins:
440
- # FIXME Parallelization?
441
- plugin.retrieve_triple(triple_template)
442
-
443
- if not downloaded_files:
444
- self.could_not_retrieve_nodes.add(node)
445
-
446
- for path in downloaded_files:
447
- self.add(path)
448
-
449
- return self
450
-
451
- def maybe_infer(self):
452
- """
453
- Apply inference lazily.
454
-
455
- Only run inference if there are new files added after last inference.
456
- """
457
- if self.sources_added_not_yet_inferred:
458
- self.infer()
459
-
460
- def find_triple(
461
- self,
462
- triple_template: TripleTemplate,
463
- ) -> Triple | None:
464
- """Lightweight procedure to find a triple by template."""
465
- triples = self.graph.triples(
466
- (triple_template.subject, triple_template.predicate, triple_template.object),
467
- )
468
-
469
- raw_triple = funcy.first(triples)
470
- if raw_triple:
471
- return Triple(*raw_triple)
472
-
473
- return self.retrieve_triple(triple_template)
474
-
475
377
  def node_as_qname(self, node: Node):
476
378
  """
477
379
  Render node as a QName if possible.
iolanta/models.py CHANGED
@@ -1,4 +1,4 @@
1
- from dataclasses import dataclass
1
+ import re
2
2
  from enum import Enum
3
3
  from typing import Any, Dict, List, NamedTuple, Union
4
4
 
@@ -82,8 +82,14 @@ class TripleTemplate(NamedTuple):
82
82
  object: Node | None
83
83
 
84
84
 
85
- @dataclass
86
- class Quad:
85
+ def _normalize_term(term: Node):
86
+ if isinstance(term, URIRef) and term.startswith('http://'):
87
+ return URIRef(re.sub('^http', 'https', term))
88
+
89
+ return term
90
+
91
+
92
+ class Quad(NamedTuple):
87
93
  """Triple assigned to a named graph."""
88
94
 
89
95
  subject: Node
@@ -107,6 +113,19 @@ class Quad:
107
113
  f'{rendered_graph})' # noqa: WPS326
108
114
  )
109
115
 
110
- def as_tuple(self):
111
- """Represent quad as a tuple which `Graph.addN()` would understand."""
112
- return self.subject, self.predicate, self.object, self.graph
116
+ def replace(self, mapping: dict[Node, URIRef]):
117
+ """Replace variables in the quad."""
118
+ terms = [
119
+ mapping.get(term, term)
120
+ for term in self
121
+ ]
122
+
123
+ return Quad(*terms)
124
+
125
+ def normalize(self) -> 'Quad':
126
+ """Normalize the quad by applying normalization to all its terms."""
127
+ terms = [
128
+ _normalize_term(term)
129
+ for term in self
130
+ ]
131
+ return Quad(*terms)
iolanta/namespaces.py CHANGED
@@ -8,6 +8,8 @@ NP = rdflib.Namespace('https://www.nanopub.org/nschema#')
8
8
  RDFG = rdflib.Namespace('https://www.w3.org/2004/03/trix/rdfg-1/')
9
9
  SDO = rdflib.SDO
10
10
 
11
+ META = rdflib.URIRef('iolanta://_meta')
12
+
11
13
 
12
14
  class DC(rdflib.DC):
13
15
  _NS = rdflib.Namespace('https://purl.org/dc/elements/1.1/')
iolanta/parse_quads.py CHANGED
@@ -1,14 +1,21 @@
1
1
  import dataclasses
2
2
  import hashlib
3
- from typing import Iterable
3
+ from types import MappingProxyType
4
+ from typing import Iterable, Optional
5
+ from urllib.parse import unquote
4
6
 
7
+ from documented import DocumentedError
5
8
  from rdflib import BNode, Literal, URIRef
6
9
  from rdflib.term import Node
7
10
 
8
11
  from iolanta.errors import UnresolvedIRI
9
12
  from iolanta.models import Quad
10
- from iolanta.namespaces import IOLANTA, RDF
11
- from iolanta.parsers.errors import SpaceInProperty
13
+ from iolanta.namespaces import IOLANTA, META
14
+
15
+ NORMALIZE_TERMS_MAP = MappingProxyType({
16
+ URIRef(_url := 'https://www.w3.org/2002/07/owl'): URIRef(f'{_url}#'),
17
+ URIRef(_url := 'https://www.w3.org/2000/01/rdf-schema'): URIRef(f'{_url}#'),
18
+ })
12
19
 
13
20
 
14
21
  def parse_term( # noqa: C901
@@ -23,7 +30,7 @@ def parse_term( # noqa: C901
23
30
  term_value = term['value']
24
31
 
25
32
  if term_type == 'IRI':
26
- return URIRef(term_value)
33
+ return URIRef(unquote(term_value))
27
34
 
28
35
  if term_type == 'literal':
29
36
  language = term.get('language')
@@ -48,6 +55,40 @@ def parse_term( # noqa: C901
48
55
  raise ValueError(f'Unknown term: {term}')
49
56
 
50
57
 
58
+ def construct_subgraph_name(subgraph_name: str, graph: URIRef) -> URIRef:
59
+ """
60
+ Construct a proper subgraph name URI from a base name and graph.
61
+
62
+ If the subgraph name already starts with the graph URI, return it as is.
63
+ Otherwise, append the name as a fragment to the graph URI.
64
+ """
65
+ if subgraph_name.startswith(str(graph)):
66
+ return URIRef(subgraph_name)
67
+
68
+ return URIRef(f'{graph}#{subgraph_name}')
69
+
70
+
71
+ def _parse_quads_per_subgraph(
72
+ raw_quads,
73
+ blank_node_prefix: str,
74
+ graph: URIRef,
75
+ subgraph: URIRef,
76
+ ) -> Iterable[Quad]:
77
+ for quad in raw_quads:
78
+ try:
79
+ yield Quad(
80
+ subject=parse_term(quad['subject'], blank_node_prefix),
81
+ predicate=parse_term(quad['predicate'], blank_node_prefix),
82
+ object=parse_term(quad['object'], blank_node_prefix),
83
+ graph=subgraph,
84
+ )
85
+ except SpaceInProperty as err:
86
+ raise dataclasses.replace(
87
+ err,
88
+ iri=graph,
89
+ )
90
+
91
+
51
92
  def parse_quads(
52
93
  quads_document,
53
94
  graph: URIRef,
@@ -59,33 +100,44 @@ def parse_quads(
59
100
  ).hexdigest()
60
101
  blank_node_prefix = f'_:{blank_node_prefix}'
61
102
 
62
- for graph_name, quads in quads_document.items():
63
- if graph_name == '@default':
64
- graph_name = graph # noqa: WPS440
103
+ subgraph_names = {
104
+ URIRef(subgraph_name): construct_subgraph_name(
105
+ subgraph_name,
106
+ graph=graph,
107
+ )
108
+ for subgraph_name in quads_document.keys()
109
+ if subgraph_name != '@default'
110
+ }
111
+ subgraph_names[graph] = graph
112
+
113
+ for subgraph, quads in quads_document.items():
114
+ if subgraph == '@default':
115
+ subgraph = graph # noqa: WPS440
65
116
 
66
117
  else:
67
- graph_name = URIRef(graph_name)
118
+ subgraph = URIRef(subgraph)
68
119
 
69
120
  yield Quad(
70
121
  graph,
71
122
  IOLANTA['has-sub-graph'],
72
- graph_name,
73
- graph,
123
+ subgraph_names[subgraph],
124
+ META,
74
125
  )
75
126
 
76
- for quad in quads:
77
- try:
78
- yield Quad(
79
- subject=parse_term(quad['subject'], blank_node_prefix),
80
- predicate=parse_term(quad['predicate'], blank_node_prefix),
81
- object=parse_term(quad['object'], blank_node_prefix),
82
- graph=graph_name,
83
- )
84
- except SpaceInProperty as err:
85
- raise dataclasses.replace(
86
- err,
87
- iri=graph,
88
- )
127
+ quads = _parse_quads_per_subgraph(
128
+ quads,
129
+ blank_node_prefix=blank_node_prefix,
130
+ graph=subgraph,
131
+ subgraph=subgraph_names[subgraph],
132
+ )
133
+
134
+ for quad in quads: # noqa: WPS526
135
+ yield quad.replace(
136
+ subgraph_names | NORMALIZE_TERMS_MAP | {
137
+ # To enable nanopub rendering
138
+ URIRef('http://purl.org/nanopub/temp/np/'): graph,
139
+ },
140
+ ).normalize()
89
141
 
90
142
 
91
143
  def raise_if_term_is_qname(term_value: str):
@@ -102,3 +154,19 @@ def raise_if_term_is_qname(term_value: str):
102
154
  iri=term_value,
103
155
  prefix=prefix,
104
156
  )
157
+
158
+
159
+ @dataclasses.dataclass
160
+ class SpaceInProperty(DocumentedError):
161
+ """
162
+ Space in property.
163
+
164
+ That impedes JSON-LD parsing.
165
+
166
+ Please do not use spaces in property names in JSON or YAML data; use `title`
167
+ or other methods instead.
168
+
169
+ Document IRI: {self.iri}
170
+ """
171
+
172
+ iri: Optional[URIRef] = None
@@ -24,8 +24,7 @@ from rdflib.plugins.sparql.parserutils import CompValue
24
24
  from rdflib.plugins.sparql.sparql import Query
25
25
  from rdflib.query import Processor
26
26
  from rdflib.term import BNode, Literal, Node
27
- from requests import HTTPError
28
- from requests.exceptions import ConnectionError
27
+ from requests.exceptions import ConnectionError, InvalidSchema
29
28
  from yaml_ld.document_loaders.content_types import ParserNotFound
30
29
  from yaml_ld.errors import NotFound, YAMLLDError
31
30
  from yarl import URL
@@ -36,19 +35,14 @@ from iolanta.namespaces import ( # noqa: WPS235
36
35
  DCTERMS,
37
36
  FOAF,
38
37
  IOLANTA,
38
+ META,
39
39
  OWL,
40
40
  PROV,
41
41
  RDF,
42
42
  RDFS,
43
43
  VANN,
44
44
  )
45
- from iolanta.parse_quads import parse_quads
46
-
47
- NORMALIZE_TERMS_MAP = MappingProxyType({
48
- URIRef(_url := 'https://www.w3.org/2002/07/owl'): URIRef(f'{_url}#'),
49
- URIRef(_url := 'https://www.w3.org/2000/01/rdf-schema'): URIRef(f'{_url}#'),
50
- })
51
-
45
+ from iolanta.parse_quads import NORMALIZE_TERMS_MAP, parse_quads
52
46
 
53
47
  REASONING_ENABLED = True
54
48
  OWL_REASONING_ENABLED = False
@@ -78,6 +72,10 @@ REDIRECTS = MappingProxyType({
78
72
  'https://nanopub.net/nschema#',
79
73
  ),
80
74
  URIRef(PROV): URIRef('https://www.w3.org/ns/prov-o'),
75
+
76
+ # Convert lexvo.org/id URLs to lexvo.org/data URLs
77
+ r'https://lexvo\.org/id/(.+)': r'http://lexvo.org/data/\1',
78
+ r'https://www\.lexinfo\.net/(.+)': r'http://www.lexinfo.net/\1',
81
79
  })
82
80
 
83
81
 
@@ -94,9 +92,7 @@ def find_retractions_for(nanopublication: URIRef) -> set[URIRef]:
94
92
  # context of this dirty hack.
95
93
  use_server = 'http://grlc.nanopubs.lod.labs.vu.nl/api/local/local/'
96
94
 
97
- client = NanopubClient(
98
- use_server=use_server,
99
- )
95
+ client = NanopubClient(use_server=use_server)
100
96
  client.grlc_urls = [use_server]
101
97
 
102
98
  http_url = str(nanopublication).replace(
@@ -106,7 +102,7 @@ def find_retractions_for(nanopublication: URIRef) -> set[URIRef]:
106
102
 
107
103
  try:
108
104
  retractions = client.find_retractions_of(http_url)
109
- except HTTPError:
105
+ except (requests.HTTPError, InvalidSchema):
110
106
  return set()
111
107
 
112
108
  return {URIRef(retraction) for retraction in retractions}
@@ -289,11 +285,30 @@ def _extract_nanopublication_uris(
289
285
  )
290
286
 
291
287
 
292
- def apply_redirect(source: URIRef) -> URIRef:
293
- """Rewrite the URL."""
288
+ def apply_redirect(source: URIRef) -> URIRef: # noqa: WPS210
289
+ """
290
+ Rewrite the URL using regex patterns and group substitutions.
291
+
292
+ For each pattern in REDIRECTS:
293
+ - If the pattern matches the source URI
294
+ - Replace the source with the destination, substituting any regex groups
295
+ """
296
+ source_str = str(source)
297
+
294
298
  for pattern, destination in REDIRECTS.items():
295
- if source.startswith(pattern):
296
- return destination
299
+ pattern_str = str(pattern)
300
+ destination_str = str(destination)
301
+
302
+ match = re.match(pattern_str, source_str)
303
+ if match:
304
+ # Replace any group references in the destination
305
+ # (like \1, \2, etc.)
306
+ redirected_uri = re.sub(
307
+ pattern_str,
308
+ destination_str,
309
+ source_str,
310
+ )
311
+ return URIRef(redirected_uri)
297
312
 
298
313
  return source
299
314
 
@@ -331,7 +346,7 @@ class NanopubQueryPlugin:
331
346
  if potential_type == original_RDF.type:
332
347
  yield potential_class
333
348
 
334
- @funcy.retry(errors=HTTPError, tries=3, timeout=3)
349
+ @funcy.retry(errors=requests.HTTPError, tries=3, timeout=3)
335
350
  def _load_instances(self, class_uri: URIRef):
336
351
  """
337
352
  Load instances from Nanopub Registry.
@@ -537,7 +552,7 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
537
552
  uri,
538
553
  IOLANTA['last-loaded-time'],
539
554
  None,
540
- URIRef('iolanta://_meta'),
555
+ META,
541
556
  )),
542
557
  ) is not None
543
558
 
@@ -546,7 +561,7 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
546
561
  uri,
547
562
  IOLANTA['last-loaded-time'],
548
563
  Literal(datetime.datetime.now()),
549
- URIRef('iolanta://_meta'),
564
+ META,
550
565
  ))
551
566
 
552
567
  def _follow_is_visualized_with_links(self, uri: URIRef):
@@ -692,14 +707,7 @@ class GlobalSPARQLProcessor(Processor): # noqa: WPS338, WPS214
692
707
  self.logger.info('{source} | No data found', source=source)
693
708
  return Loaded()
694
709
 
695
- quad_tuples = [
696
- tuple([
697
- normalize_term(term) for term in quad.as_tuple()
698
- ])
699
- for quad in quads
700
- ]
701
-
702
- self.graph.addN(quad_tuples)
710
+ self.graph.addN(quads)
703
711
  self.graph.last_not_inferred_source = source
704
712
 
705
713
  into_graphs = ', '.join({
@@ -1,12 +1,31 @@
1
+ from rich.markdown import Markdown
2
+ from textual.containers import VerticalScroll
1
3
  from textual.widgets import Label
2
4
 
3
5
 
4
- class Description(Label):
6
+ class Description(VerticalScroll):
5
7
  """Free form textual description."""
6
8
 
7
9
  DEFAULT_CSS = """
8
10
  Description {
9
- padding: 1;
10
- padding-left: 4;
11
+ padding: 4;
12
+ padding-top: 1;
13
+ padding-bottom: 1;
14
+ max-height: 100%;
11
15
  }
12
16
  """ # noqa: WPS115
17
+
18
+ def __init__(self, renderable: str | Markdown):
19
+ """
20
+ Initialize a Description widget with a renderable content.
21
+
22
+ Args:
23
+ renderable: Content to display, either as a string or
24
+ Markdown object
25
+ """
26
+ self.renderable = renderable
27
+ super().__init__()
28
+
29
+ def compose(self):
30
+ """Build and return the widget's component structure."""
31
+ yield Label(self.renderable)