Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nml.py): add level 2 validation for SegmentGroup #156

Draft
wants to merge 16 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
aaddfa8
feat(nml.py): add level 2 validation for SegmentGroup
sanjayankur31 Dec 2, 2022
616be3d
Merge branch 'development' into feat/add-l2-validation-segmentgroup
sanjayankur31 Dec 2, 2022
089da1b
Merge branch 'development' into feat/add-l2-validation-segmentgroup
sanjayankur31 Dec 2, 2022
4e3bbb3
Merge branch 'development' into feat/add-l2-validation-segmentgroup
sanjayankur31 Dec 2, 2022
7415950
Merge branch 'development' into feat/add-l2-validation-segmentgroup
sanjayankur31 Jan 31, 2023
8b2eaa0
feat(l2testing): add framework for level 2 validation tests
sanjayankur31 Feb 3, 2023
87c2461
test(l2validators): add initial set of tests
sanjayankur31 Feb 3, 2023
c9feacb
feat(l2validator): remove duplicate SegmentGroup validation test
sanjayankur31 Feb 3, 2023
f42bdeb
feat(l2validator): allow validation of objects by arbitrary test classes
sanjayankur31 Feb 3, 2023
9260309
test(l2validator): add test for segment group including itself
sanjayankur31 Feb 3, 2023
46893e3
fix(l2validator): pass tests that are marked as warnings
sanjayankur31 Feb 3, 2023
e5088fd
feat(generatedssuper): print out remaining collected messsages
sanjayankur31 Feb 3, 2023
b19c94d
chore(cell-helper): add todo to deduplicate based on contents also
sanjayankur31 Feb 3, 2023
14c4554
test(l2validator): add more tests
sanjayankur31 Feb 3, 2023
cd28d71
fix(test-cell-optimise): correctly add duplicates of same object
sanjayankur31 Feb 3, 2023
518f294
WIP: performance drops need investigation
sanjayankur31 Feb 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions neuroml/l2validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#!/usr/bin/env python3
"""
"Level 2" validators: extra tests in addition to the validation tests included
in the standard/schema

File: neuroml/l2validators.py

Copyright 2023 NeuroML contributors
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""

import inspect
import importlib
import typing
from abc import ABC, abstractmethod
from enum import Enum
import logging
logger = logging.getLogger(__name__)


class TEST_LEVEL(Enum):
WARNING = logging.WARNING
ERROR = logging.ERROR


class StandardTestSuper(ABC):

"""Class implementing a standard test.

All tests should extend this class. `L2Validator` will automatically
register any classes in this module that extend this class.
"""

test_id = ""
target_class = ""
description = ""
level = TEST_LEVEL.ERROR

@abstractmethod
def run(self, obj):
"""Implementation of test to run.
:param obj: object to run test on
:returns: True if test passes, false if not
"""
pass


class L2Validator(object):

"""Main validator class."""
tests: typing.Dict[str, typing.Any] = {}

def __init__(self):
"""Register all classes that extend StandardTestSuper """
this_module = importlib.import_module(__name__)
for name, obj in inspect.getmembers(this_module):
if inspect.isclass(obj):
if StandardTestSuper in obj.__mro__:
if obj != StandardTestSuper:
self.register_test(obj)

@classmethod
def validate(cls, obj, class_name=None, collector=None):
"""Main validate method that should include calls to all tests that are
to be run on an object

:param obj: object to be validated
:type obj: an object to be validated
:param class_name: name of class for which tests are to be run
In most cases, this will be None, and the class name will be
obtained from the object. However, in cases where a tests has been
defined in an ancestor (BaseCell, which a Cell would inherit), one
can pass the class name of the ancestor. This can be used to run L2
test defined for ancestors for all descendents. In fact, tests for
arbitrary classes can be run on any objects. It is for the
developer to ensure that the appropriate tests are run.
:type class_name: str
:param collector: a GdsCollector instance for messages
:type collector: neuroml.GdsCollector
:returns: True if all validation tests pass, false if not
"""
test_result = True
class_name_ = class_name if class_name else obj.__class__.__name__
# The collector looks for a local with name "self" in the stack frame
# to figure out what the "caller" class is.
# So, set "self" to the object that is being validated here.
# self = obj

try:
for test in cls.tests[class_name_]:
test_result = test.run(obj)

if test_result is False:
if obj.collector:
obj.collector.add_message(f"Validation failed: {test.test_id}: {test.description}")
if test.level == logging.WARNING:
# a warning, don't mark as invalid
test_result = True
logger.warning(f"Validation failed: {obj}: {test.test_id}: {test.description}")
else:
logger.error(f"Validation failed: {obj}: {test.test_id}: {test.description}")
else:
logger.debug(f"PASSED: {obj}: {test.test_id}: {test.description}")

except KeyError:
pass # no L2 tests have been defined

return test_result

@classmethod
def register_test(cls, test):
"""Register a test class

:param test: test class to register
:returns: None

"""
try:
if test not in cls.tests[test.target_class]:
cls.tests[test.target_class].append(test)
except KeyError:
cls.tests[test.target_class] = [test]

@classmethod
def list_tests(cls):
"""List all registered tests."""
print("Registered tests:")
for key, val in cls.tests.items():
print(f"* {key}")
for t in val:
print(f"\t* {t.test_id}: {t.description}")
print()


class SegmentGroupSelfIncludes(StandardTestSuper):

"""Segment groups should not include themselves"""
test_id = "0001"
target_class = "SegmentGroup"
description = "Segment group includes itself"
level = TEST_LEVEL.ERROR

@classmethod
def run(self, obj):
"""Test runner method.

:param obj: object to run tests on
:type object: any neuroml.* object
:returns: True if test passes, false if not.

"""
for sginc in obj.includes:
if sginc.segment_groups == obj.id:
return False
return True
24 changes: 21 additions & 3 deletions neuroml/nml/generatedssupersuper.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import sys

from .generatedscollector import GdsCollector
from ..l2validators import L2Validator


class GeneratedsSuperSuper(object):
Expand All @@ -20,6 +21,9 @@ class GeneratedsSuperSuper(object):
Any bits that must go into every libNeuroML class should go here.
"""

l2_validator = L2Validator()
collector = GdsCollector() # noqa

def add(self, obj=None, hint=None, force=False, validate=True, **kwargs):
"""Generic function to allow easy addition of a new member to a NeuroML object.
Without arguments, when `obj=None`, it simply calls the `info()` method
Expand Down Expand Up @@ -387,19 +391,33 @@ def validate(self, recursive=False):
:rtype: None
:raises ValueError: if component is invalid
"""
collector = GdsCollector() # noqa
self.collector.clear_messages()
valid = True
for c in type(self).__mro__:
if getattr(c, "validate_", None):
v = c.validate_(self, collector, recursive)
v = c.validate_(self, self.collector, recursive)
valid = valid and v

# l2 tests for specific classes
v1 = self.l2_validator.validate(obj=self,
class_name=c.__name__,
collector=self.collector)
valid = valid and v1

if valid is False:
err = "Validation failed:\n"
for msg in collector.get_messages():
for msg in self.collector.get_messages():
err += f"- {msg}\n"
raise ValueError(err)

# Other validation warnings
msgs = self.collector.get_messages()
if len(msgs) > 0:
err = "Validation warnings:\n"
for msg in self.collector.get_messages():
err += f"- {msg}\n"
print(err)

def parentinfo(self, return_format="string"):
"""Show the list of possible parents.

Expand Down
2 changes: 2 additions & 0 deletions neuroml/nml/helper_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -1762,6 +1762,8 @@ def optimise_segment_group(self, seg_group_id):
members = new_members
seg_group.members = list(members)

# TODO: only deduplicates by identical objects, also deduplicate by
# contents
includes = seg_group.includes
new_includes = []
for i in includes:
Expand Down
Loading