mirror of
https://github.com/p2p-ld/nwb-linkml.git
synced 2025-01-09 13:44:27 +00:00
testing custom directive for doctests
This commit is contained in:
parent
0cbf1ac1b2
commit
b9ed7c0026
12 changed files with 3212 additions and 37 deletions
0
docs/__init__.py
Normal file
0
docs/__init__.py
Normal file
14
docs/conf.py
14
docs/conf.py
|
@ -13,6 +13,10 @@ author = 'Jonny Saunders'
|
||||||
release = 'v0.1.0'
|
release = 'v0.1.0'
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from directives import AdapterDirective
|
||||||
from sphinx.util.tags import Tags
|
from sphinx.util.tags import Tags
|
||||||
tags: Tags
|
tags: Tags
|
||||||
|
|
||||||
|
@ -35,12 +39,11 @@ extensions = [
|
||||||
|
|
||||||
|
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.venv']
|
||||||
if os.environ.get('SPHINX_MINIMAL', None) == 'True':
|
if os.environ.get('SPHINX_MINIMAL', None) == 'True':
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '**/models']
|
exclude_patterns.append('**/models')
|
||||||
tags.add('minimal')
|
tags.add('minimal')
|
||||||
else:
|
else:
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|
||||||
tags.add('full')
|
tags.add('full')
|
||||||
|
|
||||||
|
|
||||||
|
@ -124,3 +127,8 @@ from nwb_linkml.adapters import BuildResult
|
||||||
todo_include_todos = True
|
todo_include_todos = True
|
||||||
todo_link_only = True
|
todo_link_only = True
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
AdapterDirective.app = app
|
||||||
|
app.add_directive('adapter', AdapterDirective)
|
||||||
|
return {'parallel_read_safe': True, 'parallel_write_safe': True}
|
||||||
|
|
||||||
|
|
86
docs/directives.py
Normal file
86
docs/directives.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import codecs
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from docutils import nodes
|
||||||
|
from docutils.parsers.rst import Directive
|
||||||
|
from docutils.parsers.rst import directives
|
||||||
|
from docutils.statemachine import StringList
|
||||||
|
from jinja2 import FileSystemLoader, Environment
|
||||||
|
import sphinx.util
|
||||||
|
|
||||||
|
TEMPLATE = """
|
||||||
|
.. grid:: 2
|
||||||
|
:gutter: 1
|
||||||
|
:margin: 0
|
||||||
|
:padding: 0
|
||||||
|
|
||||||
|
.. grid-item-card::
|
||||||
|
:margin: 0
|
||||||
|
|
||||||
|
NWB Schema
|
||||||
|
^^^
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
{{ nwb }}
|
||||||
|
|
||||||
|
.. grid-item-card::
|
||||||
|
:margin: 0
|
||||||
|
|
||||||
|
LinkML
|
||||||
|
^^^
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
{{ linkml }}
|
||||||
|
"""
|
||||||
|
|
||||||
|
class AdapterDirective(Directive):
|
||||||
|
"""
|
||||||
|
Directive for writing inline adapter doctests with pretty rendering :)
|
||||||
|
|
||||||
|
Based on sphinx-jinja: https://pypi.org/project/sphinx-jinja/
|
||||||
|
"""
|
||||||
|
has_content = True
|
||||||
|
optional_arguments = 1
|
||||||
|
option_spec = {
|
||||||
|
"nwb": directives.unchanged,
|
||||||
|
"linkml": directives.unchanged,
|
||||||
|
}
|
||||||
|
app = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
node = nodes.Element()
|
||||||
|
node.document = self.state.document
|
||||||
|
|
||||||
|
cxt = {
|
||||||
|
'nwb': self.options.get("nwb"),
|
||||||
|
'linkml': self.options.get("linkml")
|
||||||
|
}
|
||||||
|
template = Environment(
|
||||||
|
#**conf.jinja_env_kwargs
|
||||||
|
).from_string(TEMPLATE)
|
||||||
|
new_content = template.render(**cxt)
|
||||||
|
|
||||||
|
new_content = StringList(new_content.splitlines(), source='')
|
||||||
|
sphinx.util.nested_parse_with_titles(self.state, new_content, node)
|
||||||
|
return node.children
|
||||||
|
|
||||||
|
|
||||||
|
def debug_print(title, content):
|
||||||
|
stars = '*' * 10
|
||||||
|
print('\n{1} Begin Debug Output: {0} {1}'.format(title, stars))
|
||||||
|
print(content)
|
||||||
|
print('\n{1} End Debug Output: {0} {1}'.format(title, stars))
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
AdapterDirective.app = app
|
||||||
|
app.add_directive('jinja', JinjaDirective)
|
||||||
|
app.add_config_value('jinja_contexts', {}, 'env')
|
||||||
|
app.add_config_value('jinja_base', app.srcdir, 'env')
|
||||||
|
app.add_config_value('jinja_env_kwargs', {}, 'env')
|
||||||
|
app.add_config_value('jinja_filters', {}, 'env')
|
||||||
|
app.add_config_value('jinja_tests', {}, 'env')
|
||||||
|
app.add_config_value('jinja_globals', {}, 'env')
|
||||||
|
app.add_config_value('jinja_policies', {}, 'env')
|
||||||
|
return {'parallel_read_safe': True, 'parallel_write_safe': True}
|
3034
docs/pdm.lock
Normal file
3034
docs/pdm.lock
Normal file
File diff suppressed because it is too large
Load diff
|
@ -21,6 +21,7 @@ dependencies = [
|
||||||
"myst-nb @ git+https://github.com/executablebooks/MyST-NB.git",
|
"myst-nb @ git+https://github.com/executablebooks/MyST-NB.git",
|
||||||
"ipykernel>=6.25.2",
|
"ipykernel>=6.25.2",
|
||||||
"ipywidgets>=8.1.1",
|
"ipywidgets>=8.1.1",
|
||||||
|
"sphinx-jinja>=2.0.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
@ -35,3 +36,5 @@ check-hidden = true
|
||||||
# ignore-words-list = ''
|
# ignore-words-list = ''
|
||||||
|
|
||||||
|
|
||||||
|
[tool.pdm]
|
||||||
|
distribution = false
|
|
@ -23,6 +23,7 @@ from linkml_runtime.linkml_model import (
|
||||||
SlotDefinition,
|
SlotDefinition,
|
||||||
TypeDefinition,
|
TypeDefinition,
|
||||||
)
|
)
|
||||||
|
from linkml_runtime.dumpers import yaml_dumper
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from nwb_schema_language import Attribute, Dataset, Group, Schema
|
from nwb_schema_language import Attribute, Dataset, Group, Schema
|
||||||
|
@ -90,6 +91,23 @@ class BuildResult:
|
||||||
|
|
||||||
return out_str
|
return out_str
|
||||||
|
|
||||||
|
def as_linkml(self) -> str:
|
||||||
|
"""
|
||||||
|
Print build results as linkml-style YAML.
|
||||||
|
|
||||||
|
Note that only non-schema results will be included, as a schema
|
||||||
|
usually contains all the other types.
|
||||||
|
"""
|
||||||
|
output = {}
|
||||||
|
for label, alist in (("classes", self.classes),
|
||||||
|
("slots", self.slots),
|
||||||
|
("types", self.types)):
|
||||||
|
if not alist:
|
||||||
|
continue
|
||||||
|
output[label] = {a.name: a for a in alist}
|
||||||
|
return yaml_dumper.dumps(output)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Adapter(BaseModel):
|
class Adapter(BaseModel):
|
||||||
"""Abstract base class for adapters"""
|
"""Abstract base class for adapters"""
|
||||||
|
|
|
@ -10,6 +10,7 @@ from linkml_runtime.linkml_model.meta import (
|
||||||
DimensionExpression,
|
DimensionExpression,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from nwb_linkml.maps.naming import snake_case
|
||||||
from nwb_linkml.types.nwb import DIMS_TYPE, SHAPE_TYPE
|
from nwb_linkml.types.nwb import DIMS_TYPE, SHAPE_TYPE
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,7 +88,7 @@ class ArrayAdapter:
|
||||||
"""
|
"""
|
||||||
Create the corresponding array specification from a shape
|
Create the corresponding array specification from a shape
|
||||||
"""
|
"""
|
||||||
dims = [DimensionExpression(alias=dim.dims, exact_cardinality=dim.shape) for dim in shape]
|
dims = [DimensionExpression(alias=snake_case(dim.dims), exact_cardinality=dim.shape) for dim in shape]
|
||||||
return ArrayExpression(dimensions=dims)
|
return ArrayExpression(dimensions=dims)
|
||||||
|
|
||||||
def make(self) -> List[ArrayExpression]:
|
def make(self) -> List[ArrayExpression]:
|
||||||
|
|
|
@ -3,25 +3,46 @@ Adapters to linkML classes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from typing import List, Optional
|
from typing import Type, TypeVar, List, Optional
|
||||||
|
|
||||||
from linkml_runtime.linkml_model import ClassDefinition, SlotDefinition
|
from linkml_runtime.linkml_model import ClassDefinition, SlotDefinition
|
||||||
|
from pydantic import field_validator
|
||||||
|
|
||||||
|
|
||||||
from nwb_linkml.adapters.adapter import Adapter, BuildResult
|
from nwb_linkml.adapters.adapter import Adapter, BuildResult
|
||||||
from nwb_linkml.maps import QUANTITY_MAP
|
from nwb_linkml.maps import QUANTITY_MAP
|
||||||
from nwb_linkml.maps.naming import camel_to_snake
|
from nwb_linkml.maps.naming import camel_to_snake
|
||||||
from nwb_schema_language import CompoundDtype, Dataset, DTypeType, Group, ReferenceDtype
|
from nwb_schema_language import CompoundDtype, Dataset, DTypeType, Group, ReferenceDtype
|
||||||
|
|
||||||
|
T = TypeVar('T', bound=Type[Dataset] | Type[Group])
|
||||||
|
TI = TypeVar('TI', bound=Dataset | Group)
|
||||||
|
|
||||||
class ClassAdapter(Adapter):
|
class ClassAdapter(Adapter):
|
||||||
"""
|
"""
|
||||||
Abstract adapter to class-like things in linkml, holds methods common to
|
Abstract adapter to class-like things in linkml, holds methods common to
|
||||||
both DatasetAdapter and GroupAdapter
|
both DatasetAdapter and GroupAdapter
|
||||||
"""
|
"""
|
||||||
|
TYPE: T
|
||||||
|
"""
|
||||||
|
The type that this adapter class handles
|
||||||
|
"""
|
||||||
|
|
||||||
cls: Dataset | Group
|
cls: TI
|
||||||
parent: Optional["ClassAdapter"] = None
|
parent: Optional["ClassAdapter"] = None
|
||||||
|
|
||||||
|
@field_validator('cls', mode='before')
|
||||||
|
@classmethod
|
||||||
|
def cast_from_string(cls, value: str | TI) -> TI:
|
||||||
|
"""
|
||||||
|
Cast from YAML string to desired class
|
||||||
|
"""
|
||||||
|
if isinstance(value, str):
|
||||||
|
from nwb_linkml.io.schema import load_yaml
|
||||||
|
value = load_yaml(value)
|
||||||
|
value = cls.TYPE(**value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def build(self) -> BuildResult:
|
def build(self) -> BuildResult:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
"""
|
"""
|
||||||
Adapter for NWB datasets to linkml Classes
|
Adapter for NWB datasets to linkml Classes
|
||||||
"""
|
"""
|
||||||
import pdb
|
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
from linkml_runtime.linkml_model.meta import (
|
from linkml_runtime.linkml_model.meta import (
|
||||||
ClassDefinition,
|
|
||||||
SlotDefinition,
|
SlotDefinition,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,37 +49,23 @@ class MapScalar(DatasetMap):
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
.. grid:: 2
|
.. adapter:: DatasetAdapter
|
||||||
:gutter: 1
|
:nwb:
|
||||||
:margin: 0
|
datasets:
|
||||||
:padding: 0
|
- name: MyScalar
|
||||||
|
doc: A scalar
|
||||||
|
dtype: int32
|
||||||
|
quantity: '?'
|
||||||
|
:linkml:
|
||||||
|
attributes:
|
||||||
|
- name: MyScalar
|
||||||
|
description: A scalar
|
||||||
|
multivalued: false
|
||||||
|
range: int32
|
||||||
|
required: false
|
||||||
|
|
||||||
.. grid-item-card::
|
|
||||||
:margin: 0
|
|
||||||
|
|
||||||
NWB Schema
|
|
||||||
^^^
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
datasets:
|
|
||||||
- name: MyScalar
|
|
||||||
doc: A scalar
|
|
||||||
dtype: int32
|
|
||||||
quantity: '?'
|
|
||||||
|
|
||||||
.. grid-item-card::
|
|
||||||
:margin: 0
|
|
||||||
|
|
||||||
LinkML
|
|
||||||
^^^
|
|
||||||
.. code-block:: yaml
|
|
||||||
|
|
||||||
attributes:
|
|
||||||
- name: MyScalar
|
|
||||||
description: A scalar
|
|
||||||
multivalued: false
|
|
||||||
range: int32
|
|
||||||
required: false
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -463,6 +447,7 @@ class DatasetAdapter(ClassAdapter):
|
||||||
"""
|
"""
|
||||||
Orchestrator class for datasets - calls the set of applicable mapping classes
|
Orchestrator class for datasets - calls the set of applicable mapping classes
|
||||||
"""
|
"""
|
||||||
|
TYPE = Dataset
|
||||||
|
|
||||||
cls: Dataset
|
cls: Dataset
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ class GroupAdapter(ClassAdapter):
|
||||||
"""
|
"""
|
||||||
Adapt NWB Groups to LinkML Classes
|
Adapt NWB Groups to LinkML Classes
|
||||||
"""
|
"""
|
||||||
|
TYPE = Group
|
||||||
|
|
||||||
cls: Group
|
cls: Group
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,29 @@ String manipulation methods for names
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
CAMEL_TO_SNAKE = re.compile("((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))")
|
CAMEL_TO_SNAKE = re.compile(r"((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))")
|
||||||
"""
|
"""
|
||||||
Convert camel case to snake case
|
Convert camel case to snake case
|
||||||
|
|
||||||
courtesy of: https://stackoverflow.com/a/12867228
|
courtesy of: https://stackoverflow.com/a/12867228
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def snake_case(name: str | None) -> str | None:
|
||||||
|
"""
|
||||||
|
Snake caser for replacing all non-word characters with single underscores
|
||||||
|
|
||||||
|
Primarily used when creating dimension labels in
|
||||||
|
:class:`~nwb_linkml.adapters.ArrayAdapter` , see also :func:`.camel_to_snake`
|
||||||
|
for converting camelcased names.
|
||||||
|
"""
|
||||||
|
if name is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
name = name.strip()
|
||||||
|
name = re.sub(r'\W+', '_', name)
|
||||||
|
name = name.lower()
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
def camel_to_snake(name: str) -> str:
|
def camel_to_snake(name: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -9,6 +9,8 @@ from sybil.parsers.rest import DocTestParser, PythonCodeBlockParser
|
||||||
|
|
||||||
from .fixtures import * # noqa: F403
|
from .fixtures import * # noqa: F403
|
||||||
|
|
||||||
|
# Test adapter generation examples
|
||||||
|
|
||||||
pytest_collect_file = Sybil(
|
pytest_collect_file = Sybil(
|
||||||
parsers=[
|
parsers=[
|
||||||
DocTestParser(optionflags=ELLIPSIS + NORMALIZE_WHITESPACE),
|
DocTestParser(optionflags=ELLIPSIS + NORMALIZE_WHITESPACE),
|
||||||
|
|
Loading…
Reference in a new issue