From 689b9d029e571cf456a673d550dee0e4841c4361 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 13 Aug 2024 13:53:04 +0100 Subject: [PATCH 01/21] chore: add logger to generatedssupersuper This allows us to use it in the various helper methods for better outputs. --- neuroml/nml/generatedssupersuper.py | 24 +++++++++++++++----- neuroml/nml/helper_methods.py | 23 ++++++++++--------- neuroml/nml/nml.py | 35 +++++++++++++++-------------- neuroml/utils.py | 2 +- 4 files changed, 50 insertions(+), 34 deletions(-) diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index 5973f97..c3d5eb8 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -13,9 +13,6 @@ from .generatedscollector import GdsCollector -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) - class GeneratedsSuperSuper(object): """Super class for GeneratedsSuper. @@ -23,6 +20,9 @@ class GeneratedsSuperSuper(object): Any bits that must go into every libNeuroML class should go here. """ + logger = logging.getLogger(__name__) + logger.setLevel(logging.INFO) + 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 @@ -97,7 +97,14 @@ def add(self, obj=None, hint=None, force=False, validate=True, **kwargs): if neuroml.build_time_validation.ENABLED and validate: self.validate() else: - logger.warning("Build time validation is disabled.") + if not neuroml.build_time_validation.ENABLED: + self.logger.warning( + f"Adding new {obj.__class__.__name__}: build time validation is globally disabled." + ) + else: + self.logger.warning( + f"Adding new {obj.__class__.__name__}: build time validation is locally disabled." + ) return obj @classmethod @@ -156,7 +163,14 @@ def component_factory(cls, component_type, validate=True, **kwargs): if neuroml.build_time_validation.ENABLED and validate: comp.validate() else: - logger.warning("Build time validation is disabled.") + if not neuroml.build_time_validation.ENABLED: + cls.logger.warning( + f"Creating new {comp_type_class.__name__}: build time validation is globally disabled." + ) + else: + cls.logger.warning( + f"Creating new {comp_type_class.__name__}: build time validation is locally disabled." + ) return comp def __add(self, obj, member, force=False): diff --git a/neuroml/nml/helper_methods.py b/neuroml/nml/helper_methods.py index 37b1a94..d5da6db 100644 --- a/neuroml/nml/helper_methods.py +++ b/neuroml/nml/helper_methods.py @@ -1551,13 +1551,13 @@ def add_segment( else: p = None except IndexError as e: - print("{}: prox must be a list of 4 elements".format(e)) + raise ValueError("{}: prox must be a list of 4 elements".format(e)) try: d = self.component_factory( "Point3DWithDiam", x=dist[0], y=dist[1], z=dist[2], diameter=dist[3] ) except IndexError as e: - print("{}: dist must be a list of 4 elements".format(e)) + raise ValueError("{}: dist must be a list of 4 elements".format(e)) segid = len(self.morphology.segments) if segid > 0 and parent is None: @@ -1578,7 +1578,7 @@ def add_segment( seg = None seg = self.get_segment(seg_id) if seg: - raise ValueError(f"A segment with provided id {seg_id} already exists") + raise ValueError(f"A segment with provided id '{seg_id}' already exists") except ValueError: # a segment with this ID does not already exist pass @@ -1596,8 +1596,8 @@ def add_segment( try: seg_group = self.get_segment_group(group_id) except ValueError as e: - print("Warning: {}".format(e)) - print(f"Warning: creating Segment Group with id {group_id}") + self.logger.warning("{}".format(e)) + self.logger.warning(f"Creating Segment Group with id '{group_id}'") seg_group = self.add_segment_group( group_id=group_id ) @@ -1685,7 +1685,7 @@ def add_segment_group(self, group_id, neuro_lex_id=None, notes=None): notes=notes, validate=False ) else: - print(f"Warning: Segment group {seg_group.id} already exists.") + self.logger.warning(f"Segment group '{seg_group.id}' already exists.") return seg_group @@ -2024,6 +2024,8 @@ def setup_default_segment_groups(self, use_convention=True, default_groups=["all :type default_groups: list of strings :returns: list of created segment groups (or empty list if none created) :rtype: list + + :raises ValueError: if a group other than the standard groups are provided """ new_groups = [] if use_convention: @@ -2044,8 +2046,7 @@ def setup_default_segment_groups(self, use_convention=True, default_groups=["all neuro_lex_id=None notes="Default segment group for all segments in the cell" else: - print(f"Error: only 'all', 'soma_group', 'dendrite_group', and 'axon_group' are supported. Received {grp}") - return [] + raise ValueError(f"Only 'all', 'soma_group', 'dendrite_group', and 'axon_group' are supported. Received {grp}") seg_group = self.add_segment_group(group_id=grp, neuro_lex_id=neuro_lex_id, notes=notes) new_groups.append(seg_group) @@ -2215,7 +2216,7 @@ def __sectionise(self, root_segment_id, seg_group, morph_tree): :returns: TODO """ - # print(f"Processing element: {root_segment_id}") + self.logger.debug(f"Processing element: {root_segment_id}") try: children = morph_tree[root_segment_id] @@ -2282,7 +2283,7 @@ def get_segment_adjacency_list(self): child_lists[parent] = [] child_lists[parent].append(segment.id) except AttributeError: - print(f"Warning: Segment: {segment} has no parent") + self.logger.warning(f"Segment: {segment} has no parent") self.adjacency_list = child_lists return child_lists @@ -2389,7 +2390,7 @@ def get_segments_at_distance(self, distance, src_seg = 0): frac_along = ((distance - dist) / self.get_segment_length(tgt)) except ZeroDivisionError: # ignore zero length segments - print(f"Warning: encountered zero length segment: {tgt}") + self.logger.warning(f"Encountered zero length segment: {tgt}") continue if frac_along > 1.0: diff --git a/neuroml/nml/nml.py b/neuroml/nml/nml.py index ed65129..316bf26 100644 --- a/neuroml/nml/nml.py +++ b/neuroml/nml/nml.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- # -# Generated Tue Jun 11 14:00:16 2024 by generateDS.py version 2.43.3. -# Python 3.10.9 (main, Jan 11 2023, 15:21:40) [GCC 11.2.0] +# Generated Tue Aug 13 13:52:09 2024 by generateDS.py version 2.44.1. +# Python 3.11.9 (main, Apr 17 2024, 00:00:00) [GCC 14.0.1 20240411 (Red Hat 14.0.1-0)] # # Command line options: # ('-o', 'nml.py') @@ -16,7 +16,7 @@ # NeuroML_v2.3.1.xsd # # Command line: -# /home/padraig/anaconda2/envs/py310//bin/generateDS -o "nml.py" --use-getter-setter="none" --user-methods="helper_methods.py" --export="write validate" --custom-imports-template="gds_imports-template.py" NeuroML_v2.3.1.xsd +# /home/asinha/.local/share/virtualenvs/neuroml-311-dev/bin/generateDS -o "nml.py" --use-getter-setter="none" --user-methods="helper_methods.py" --export="write validate" --custom-imports-template="gds_imports-template.py" NeuroML_v2.3.1.xsd # # Current working directory (os.getcwd()): # nml @@ -198,7 +198,7 @@ class GeneratedsSuperSuper(object): class GeneratedsSuper(GeneratedsSuperSuper): __hash__ = object.__hash__ - tzoff_pattern = re_.compile(r"(\+|-)((0\d|1[0-3]):[0-5]\d|14:00)$") + tzoff_pattern = re_.compile("(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)$") class _FixedOffsetTZ(datetime_.tzinfo): def __init__(self, offset, name): @@ -704,7 +704,7 @@ def get_path_(self, node): path = "/".join(path_list) return path - Tag_strip_pattern_ = re_.compile(r"\{.*\}") + Tag_strip_pattern_ = re_.compile(r"{.*}") def get_path_list_(self, node, path_list): if node is None: @@ -49111,13 +49111,13 @@ def add_segment( else: p = None except IndexError as e: - print("{}: prox must be a list of 4 elements".format(e)) + raise ValueError("{}: prox must be a list of 4 elements".format(e)) try: d = self.component_factory( "Point3DWithDiam", x=dist[0], y=dist[1], z=dist[2], diameter=dist[3] ) except IndexError as e: - print("{}: dist must be a list of 4 elements".format(e)) + raise ValueError("{}: dist must be a list of 4 elements".format(e)) segid = len(self.morphology.segments) if segid > 0 and parent is None: @@ -49139,7 +49139,7 @@ def add_segment( seg = self.get_segment(seg_id) if seg: raise ValueError( - f"A segment with provided id {seg_id} already exists" + f"A segment with provided id '{seg_id}' already exists" ) except ValueError: # a segment with this ID does not already exist @@ -49160,8 +49160,8 @@ def add_segment( try: seg_group = self.get_segment_group(group_id) except ValueError as e: - print("Warning: {}".format(e)) - print(f"Warning: creating Segment Group with id {group_id}") + self.logger.warning("{}".format(e)) + self.logger.warning(f"Creating Segment Group with id '{group_id}'") seg_group = self.add_segment_group(group_id=group_id) seg_group.members.append(Member(segments=segment.id)) @@ -49255,7 +49255,7 @@ def add_segment_group(self, group_id, neuro_lex_id=None, notes=None): validate=False, ) else: - print(f"Warning: Segment group {seg_group.id} already exists.") + self.logger.warning(f"Segment group '{seg_group.id}' already exists.") return seg_group @@ -49589,6 +49589,8 @@ def setup_default_segment_groups( :type default_groups: list of strings :returns: list of created segment groups (or empty list if none created) :rtype: list + + :raises ValueError: if a group other than the standard groups are provided """ new_groups = [] if use_convention: @@ -49609,10 +49611,9 @@ def setup_default_segment_groups( neuro_lex_id = None notes = "Default segment group for all segments in the cell" else: - print( - f"Error: only 'all', 'soma_group', 'dendrite_group', and 'axon_group' are supported. Received {grp}" + raise ValueError( + f"Only 'all', 'soma_group', 'dendrite_group', and 'axon_group' are supported. Received {grp}" ) - return [] seg_group = self.add_segment_group( group_id=grp, neuro_lex_id=neuro_lex_id, notes=notes @@ -49800,7 +49801,7 @@ def __sectionise(self, root_segment_id, seg_group, morph_tree): :returns: TODO """ - # print(f"Processing element: {root_segment_id}") + self.logger.debug(f"Processing element: {root_segment_id}") try: children = morph_tree[root_segment_id] @@ -49866,7 +49867,7 @@ def get_segment_adjacency_list(self): child_lists[parent] = [] child_lists[parent].append(segment.id) except AttributeError: - print(f"Warning: Segment: {segment} has no parent") + self.logger.warning(f"Segment: {segment} has no parent") self.adjacency_list = child_lists return child_lists @@ -49974,7 +49975,7 @@ def get_segments_at_distance(self, distance, src_seg=0): frac_along = (distance - dist) / self.get_segment_length(tgt) except ZeroDivisionError: # ignore zero length segments - print(f"Warning: encountered zero length segment: {tgt}") + self.logger.warning(f"Encountered zero length segment: {tgt}") continue if frac_along > 1.0: diff --git a/neuroml/utils.py b/neuroml/utils.py index 3033ff7..dc3c236 100644 --- a/neuroml/utils.py +++ b/neuroml/utils.py @@ -234,7 +234,7 @@ def component_factory( Please see `GeneratedsSuperSuper.component_factory` for more information. """ new_obj = schema.NeuroMLDocument().component_factory( - component_type, validate, **kwargs + component_type=component_type, validate=validate, **kwargs ) return new_obj From 89bd448a737ac8b36aa2ea18ea83c4dafe8376fc Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 13 Aug 2024 14:03:35 +0100 Subject: [PATCH 02/21] chore(generatedssupersuper): improve warnings Also, use `logger.warning` instead of `warnings.warn` --- neuroml/nml/generatedssupersuper.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index c3d5eb8..b0a3ec4 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -99,11 +99,11 @@ def add(self, obj=None, hint=None, force=False, validate=True, **kwargs): else: if not neuroml.build_time_validation.ENABLED: self.logger.warning( - f"Adding new {obj.__class__.__name__}: build time validation is globally disabled." + f"Build time validation is globally disabled: adding new {obj.__class__.__name__}." ) else: self.logger.warning( - f"Adding new {obj.__class__.__name__}: build time validation is locally disabled." + f"Build time validation is globally disabled: adding new {obj.__class__.__name__}." ) return obj @@ -165,11 +165,11 @@ def component_factory(cls, component_type, validate=True, **kwargs): else: if not neuroml.build_time_validation.ENABLED: cls.logger.warning( - f"Creating new {comp_type_class.__name__}: build time validation is globally disabled." + f"Build time validation is globally disabled: creating new {comp_type_class.__name__}." ) else: cls.logger.warning( - f"Creating new {comp_type_class.__name__}: build time validation is locally disabled." + f"Build time validation is globally disabled: creating new {comp_type_class.__name__}." ) return comp @@ -184,16 +184,14 @@ def __add(self, obj, member, force=False): :type force: bool """ - import warnings - # A single value, not a list: if member.get_container() == 0: if force: vars(self)[member.get_name()] = obj else: if vars(self)[member.get_name()]: - warnings.warn( - """{} has already been assigned. Use `force=True` to overwrite. Hint: you can make changes to the already added object as required without needing to re-add it because only references to the objects are added, not their values.""".format( + self.logger.warning( + """Member '{}' has already been assigned. Use `force=True` to overwrite. Hint: you can make changes to the already added object as required without needing to re-add it because only references to the objects are added, not their values.""".format( member.get_name() ) ) @@ -209,7 +207,7 @@ def __add(self, obj, member, force=False): # There is no use case where the same child would be added # twice to a component. if obj in vars(self)[member.get_name()]: - warnings.warn( + self.logger.warning( """{} already exists in {}. Use `force=True` to force readdition. Hint: you can make changes to the already added object as required without needing to re-add it because only references to the objects are added, not their values.""".format( obj, member.get_name() ) From 3e7329ec56ce7d7e56ab421efb3f0b95c469df76 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 13 Aug 2024 14:06:36 +0100 Subject: [PATCH 03/21] test: remove testing of user warning --- neuroml/test/test_nml.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/neuroml/test/test_nml.py b/neuroml/test/test_nml.py index f2ffd85..99134e2 100644 --- a/neuroml/test/test_nml.py +++ b/neuroml/test/test_nml.py @@ -73,10 +73,6 @@ def test_generic_add_single(self): # Success: returns object self.assertIsNotNone(doc.add(cell)) - # Already added, so throw exception - with self.assertWarns(UserWarning): - doc.add(cell) - # Success self.assertIsNotNone(doc.add(cell1)) @@ -164,9 +160,6 @@ def test_add_to_container(self): pop3 = neuroml.Population(id="unique") network.add(pop3) - # warning because this is already added - with self.assertWarns(UserWarning): - network.add(pop3) # Note that for Python, this is a new object # So we can add it again From 737839e0e346139be919da771baaa489685dce96 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 13 Aug 2024 14:50:36 +0100 Subject: [PATCH 04/21] feat(component-factory): handle addition of "__ANY__" Use cases: - `component_factory(sometype, __ANY__="something")` - `l = component_factory(sometype); l.add("something", hint="__ANY__")` In both, we require the user to give some notion that they are trying to add a `__ANY__` member. --- neuroml/nml/generatedssupersuper.py | 105 ++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 30 deletions(-) diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index b0a3ec4..c5ad619 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -58,6 +58,26 @@ def add(self, obj=None, hint=None, force=False, validate=True, **kwargs): if type(obj) is type or isinstance(obj, str): obj = self.component_factory(obj, validate=validate, **kwargs) + # if obj is still a string, it is a general string, but to confirm that + # this is what the user intends, ask them to provide a hint. + if isinstance(obj, str): + # no hint, no pass + if not hint: + self.logger.error("Received a text object to add.") + self.logger.error( + 'Please pass `hint="__ANY__"` to confirm that this is what you intend.' + ) + self.logger.error( + "I will then try to add this to an __ANY__ member in the object." + ) + return None + # hint confirmed, try to add it below + else: + self.logger.warning("Received a text object to add.") + self.logger.warning( + "Trying to add this to an __ANY__ member in the object." + ) + # getattr only returns the value of the provided member but one cannot # then use this to modify the member. Using `vars` also allows us to # modify the value @@ -65,13 +85,19 @@ def add(self, obj=None, hint=None, force=False, validate=True, **kwargs): all_members = self._get_members() for member in all_members: # get_data_type() returns the type as a string, e.g.: 'IncludeType' + + # handle "__ANY__" + if isinstance(obj, str) and member.get_data_type() == "__ANY__": + targets.append(member) + break + if member.get_data_type() == type(obj).__name__: targets.append(member) if len(targets) == 0: # no targets found e = Exception( - """A member object of {} type could not be found in NeuroML class {}.\n{} + """A member object of '{}' type could not be found in NeuroML class {}.\n{} """.format(type(obj).__name__, type(self).__name__, self.info()) ) raise e @@ -148,11 +174,26 @@ def component_factory(cls, component_type, validate=True, **kwargs): module_object = sys.modules[cls.__module__] if isinstance(component_type, str): - comp_type_class = getattr(module_object, component_type) + # class names do not have spaces, so if we find a space, we treat + # it as a general string and just return it. + if " " in component_type: + cls.logger.warning( + "Component Type appears to be a generic string: nothing to do." + ) + return component_type + else: + comp_type_class = getattr(module_object, component_type) else: comp_type_class = getattr(module_object, component_type.__name__) - comp = comp_type_class(**kwargs) + # handle component types that support __ANY__ + try: + anytypevalue = kwargs["__ANY__"] + # first value, so put it in a list, otherwise each element of the + # string is taken to be a new object + comp = comp_type_class(anytypeobjs_=[anytypevalue], **kwargs) + except KeyError: + comp = comp_type_class(**kwargs) # additional setups where required if comp_type_class.__name__ == "Cell": @@ -184,36 +225,40 @@ def __add(self, obj, member, force=False): :type force: bool """ - # A single value, not a list: - if member.get_container() == 0: - if force: - vars(self)[member.get_name()] = obj - else: - if vars(self)[member.get_name()]: - self.logger.warning( - """Member '{}' has already been assigned. Use `force=True` to overwrite. Hint: you can make changes to the already added object as required without needing to re-add it because only references to the objects are added, not their values.""".format( - member.get_name() - ) - ) - else: - vars(self)[member.get_name()] = obj - # List + # handle __ANY__ which is to be stored in anytypeobjs_ + if member.get_name() == "__ANY__": + vars(self)["anytypeobjs_"].append(obj) else: - if force: - vars(self)[member.get_name()].append(obj) - else: - # "obj in .." checks by identity and value. - # In XML, two children with same values are identical. - # There is no use case where the same child would be added - # twice to a component. - if obj in vars(self)[member.get_name()]: - self.logger.warning( - """{} already exists in {}. Use `force=True` to force readdition. Hint: you can make changes to the already added object as required without needing to re-add it because only references to the objects are added, not their values.""".format( - obj, member.get_name() - ) - ) + # A single value, not a list: + if member.get_container() == 0: + if force: + vars(self)[member.get_name()] = obj else: + if vars(self)[member.get_name()]: + self.logger.warning( + """Member '{}' has already been assigned. Use `force=True` to overwrite. Hint: you can make changes to the already added object as required without needing to re-add it because only references to the objects are added, not their values.""".format( + member.get_name() + ) + ) + else: + vars(self)[member.get_name()] = obj + # List + else: + if force: vars(self)[member.get_name()].append(obj) + else: + # "obj in .." checks by identity and value. + # In XML, two children with same values are identical. + # There is no use case where the same child would be added + # twice to a component. + if obj in vars(self)[member.get_name()]: + self.logger.warning( + """{} already exists in {}. Use `force=True` to force readdition. Hint: you can make changes to the already added object as required without needing to re-add it because only references to the objects are added, not their values.""".format( + obj, member.get_name() + ) + ) + else: + vars(self)[member.get_name()].append(obj) @classmethod def _get_members(cls): From c575dbebf9e85c1a915eda934440cdb1f9e4b240 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 13 Aug 2024 16:10:31 +0100 Subject: [PATCH 05/21] chore(component-factory): simplify code --- neuroml/nml/generatedssupersuper.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index c5ad619..a455325 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -186,14 +186,15 @@ def component_factory(cls, component_type, validate=True, **kwargs): else: comp_type_class = getattr(module_object, component_type.__name__) + comp = comp_type_class(**kwargs) + # handle component types that support __ANY__ try: anytypevalue = kwargs["__ANY__"] - # first value, so put it in a list, otherwise each element of the - # string is taken to be a new object - comp = comp_type_class(anytypeobjs_=[anytypevalue], **kwargs) + # append value to anytypeobjs_ list + comp.anytypeobjs_.append(anytypevalue) except KeyError: - comp = comp_type_class(**kwargs) + pass # additional setups where required if comp_type_class.__name__ == "Cell": From 09117fc34648398f33e32a4edd10d08f686631ef Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Tue, 13 Aug 2024 16:16:00 +0100 Subject: [PATCH 06/21] chore: revert to raising exception --- neuroml/nml/generatedssupersuper.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index a455325..c0655af 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -63,14 +63,12 @@ def add(self, obj=None, hint=None, force=False, validate=True, **kwargs): if isinstance(obj, str): # no hint, no pass if not hint: - self.logger.error("Received a text object to add.") - self.logger.error( - 'Please pass `hint="__ANY__"` to confirm that this is what you intend.' + e = Exception( + "Received a text object to add. " + + 'Please pass `hint="__ANY__"` to confirm that this is what you intend. ' + + "I will then try to add this to an __ANY__ member in the object." ) - self.logger.error( - "I will then try to add this to an __ANY__ member in the object." - ) - return None + raise e # hint confirmed, try to add it below else: self.logger.warning("Received a text object to add.") From 843d5df7ba32131d200b979e19f8ba0aa5887540 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 14 Aug 2024 18:15:15 +0100 Subject: [PATCH 07/21] chore: correct warning string --- neuroml/nml/generatedssupersuper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index c0655af..f577804 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -127,7 +127,7 @@ def add(self, obj=None, hint=None, force=False, validate=True, **kwargs): ) else: self.logger.warning( - f"Build time validation is globally disabled: adding new {obj.__class__.__name__}." + f"Build time validation is locally disabled: adding new {obj.__class__.__name__}." ) return obj @@ -209,7 +209,7 @@ def component_factory(cls, component_type, validate=True, **kwargs): ) else: cls.logger.warning( - f"Build time validation is globally disabled: creating new {comp_type_class.__name__}." + f"Build time validation is locally disabled: creating new {comp_type_class.__name__}." ) return comp From 0e1e62e0e217b91030e6098755dcac9bba23ad93 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 15 Aug 2024 10:31:21 +0100 Subject: [PATCH 08/21] chore: make validation disabled messages into debugs Otherwise the output is too verbose. --- neuroml/nml/generatedssupersuper.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index f577804..c26e38c 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -122,11 +122,11 @@ def add(self, obj=None, hint=None, force=False, validate=True, **kwargs): self.validate() else: if not neuroml.build_time_validation.ENABLED: - self.logger.warning( + self.logger.debug( f"Build time validation is globally disabled: adding new {obj.__class__.__name__}." ) else: - self.logger.warning( + self.logger.debug( f"Build time validation is locally disabled: adding new {obj.__class__.__name__}." ) return obj @@ -204,11 +204,11 @@ def component_factory(cls, component_type, validate=True, **kwargs): comp.validate() else: if not neuroml.build_time_validation.ENABLED: - cls.logger.warning( + cls.logger.debug( f"Build time validation is globally disabled: creating new {comp_type_class.__name__}." ) else: - cls.logger.warning( + cls.logger.debug( f"Build time validation is locally disabled: creating new {comp_type_class.__name__}." ) return comp From e8d4b8a65455df111ccb140cdc56faa5d9e0216d Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Thu, 15 Aug 2024 11:33:51 +0100 Subject: [PATCH 09/21] feat(generatedssuper): reduce warnings, improve when they're called --- neuroml/nml/generatedssupersuper.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index c26e38c..58b60b9 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -226,15 +226,20 @@ def __add(self, obj, member, force=False): """ # handle __ANY__ which is to be stored in anytypeobjs_ if member.get_name() == "__ANY__": + self.logger.debug("__ANY__ member, appending to anytypeobjs_.") vars(self)["anytypeobjs_"].append(obj) else: # A single value, not a list: + self.logger.debug("Not a container member, assigning.") if member.get_container() == 0: if force: vars(self)[member.get_name()] = obj + self.logger.warning( + """Overwriting member '{}'.""".format(member.get_name()) + ) else: if vars(self)[member.get_name()]: - self.logger.warning( + self.logger.debug( """Member '{}' has already been assigned. Use `force=True` to overwrite. Hint: you can make changes to the already added object as required without needing to re-add it because only references to the objects are added, not their values.""".format( member.get_name() ) @@ -243,8 +248,12 @@ def __add(self, obj, member, force=False): vars(self)[member.get_name()] = obj # List else: + self.logger.debug("Container member, appending.") if force: vars(self)[member.get_name()].append(obj) + self.logger.warning( + """Force appending to member '{}'""".format(member.get_name()) + ) else: # "obj in .." checks by identity and value. # In XML, two children with same values are identical. @@ -252,7 +261,7 @@ def __add(self, obj, member, force=False): # twice to a component. if obj in vars(self)[member.get_name()]: self.logger.warning( - """{} already exists in {}. Use `force=True` to force readdition. Hint: you can make changes to the already added object as required without needing to re-add it because only references to the objects are added, not their values.""".format( + """{} already exists in {}. Use `force=True` to force readdition.""".format( obj, member.get_name() ) ) From ebb2f8cae93faf858c66fd4f5222e4c4a112d524 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 16 Aug 2024 16:50:55 +0100 Subject: [PATCH 10/21] feat: generalise get_by_id --- neuroml/nml/generatedssupersuper.py | 28 ++++++++++++ neuroml/nml/helper_methods.py | 71 ++--------------------------- 2 files changed, 33 insertions(+), 66 deletions(-) diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index 58b60b9..ad3d099 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -681,3 +681,31 @@ def get_nml2_class_hierarchy(cls): schema = sys.modules[cls.__module__] cls.__nml_hier = schema.NeuroMLDocument.get_class_hierarchy() return cls.__nml_hier + + def get_by_id(self, id_): + """Get a component or attribute by its ID + + :param id_: ID of component or name of attribute to find + :type id_: str + :returns: component with specified ID, or attribute, or None if neither found + """ + all_ids = [] + all_members = self._get_members() + for member in all_members: + if member.get_container() == 0: + if member.get_name() == id_: + return member + else: + all_ids.append(member.get_name()) + else: + contents = getattr(self, member.get_name(), None) + for m in contents: + if hasattr(m, "id"): + if m.id == id_: + return m + else: + all_ids.append(m.id) + + self.logger.warning(f"Id '{id_}' not found in {self.__name__}") + self.logger.warning(f"All ids: {sorted(all_ids)}") + return None diff --git a/neuroml/nml/helper_methods.py b/neuroml/nml/helper_methods.py index d5da6db..83035c6 100644 --- a/neuroml/nml/helper_methods.py +++ b/neuroml/nml/helper_methods.py @@ -877,39 +877,6 @@ def summary(self, show_includes=True, show_non_network=True): return info - warn_count = 0 - - def get_by_id(self, id: str) -> typing.Optional[typing.Any]: - """Get a component by specifying its ID. - - :param id: id of Component to get - :type id: str - :returns: Component with given ID or None if no Component with provided ID was found - """ - if len(id)==0: - callframe = inspect.getouterframes(inspect.currentframe(), 2) - print('Method: '+ callframe[1][3] + ' is asking for an element with no id...') - - return None - all_ids = [] - for ms in self.member_data_items_: - mlist = getattr(self, ms.get_name()) - # TODO: debug why this is required - if mlist is None: - continue - for m in mlist: - if hasattr(m,"id"): - if m.id == id: - return m - else: - all_ids.append(m.id) - if self.warn_count<10: - neuroml.print_("Id "+id+" not found in element. All ids: "+str(sorted(all_ids))) - self.warn_count+=1 - elif self.warn_count==10: - neuroml.print_(" - Suppressing further warnings about id not found...") - return None - def append(self, element): """Append an element @@ -924,47 +891,19 @@ def append(self, element): METHOD_SPECS += (nml_doc_summary,) -network_get_by_id = MethodSpec( - name="get_by_id", - source='''\ - - warn_count = 0 - def get_by_id(self, id: str) -> typing.Optional[typing.Any]: - """Get a component by its ID - - :param id: ID of component to find - :type id: str - :returns: component with specified ID or None if no component with specified ID found - """ - all_ids = [] - for ms in self.member_data_items_: - mlist = getattr(self, ms.get_name()) - # TODO: debug why this is required - if mlist is None: - continue - for m in mlist: - if hasattr(m,"id"): - if m.id == id: - return m - else: - all_ids.append(m.id) - if self.warn_count<10: - neuroml.print_("Id "+id+" not found in element. All ids: "+str(sorted(all_ids))) - self.warn_count+=1 - elif self.warn_count==10: - neuroml.print_(" - Suppressing further warnings about id not found...") - return None - +network_str = MethodSpec( + name="str", + source="""\ def __str__(self): return "Network "+str(self.id)+" with "+str(len(self.populations))+" population(s)" - ''', + """, class_names=("Network"), ) -METHOD_SPECS += (network_get_by_id,) +METHOD_SPECS += (network_str,) cell_methods = MethodSpec( From fb7d15d1a3ff87dd624f4f9a959a310296a351d7 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 16 Aug 2024 16:57:47 +0100 Subject: [PATCH 11/21] fix(nml): return member value --- neuroml/nml/generatedssupersuper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index ad3d099..f7e7276 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -694,7 +694,7 @@ def get_by_id(self, id_): for member in all_members: if member.get_container() == 0: if member.get_name() == id_: - return member + return getattr(self, member.get_name()) else: all_ids.append(member.get_name()) else: From a440a28c13ce7bd29304f1525b48aee66431e026 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 16 Aug 2024 16:58:56 +0100 Subject: [PATCH 12/21] chore(nml): regenerate --- neuroml/nml/nml.py | 75 +--------------------------------------------- 1 file changed, 1 insertion(+), 74 deletions(-) diff --git a/neuroml/nml/nml.py b/neuroml/nml/nml.py index 316bf26..13b30b9 100644 --- a/neuroml/nml/nml.py +++ b/neuroml/nml/nml.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # -# Generated Tue Aug 13 13:52:09 2024 by generateDS.py version 2.44.1. +# Generated Fri Aug 16 16:50:58 2024 by generateDS.py version 2.44.1. # Python 3.11.9 (main, Apr 17 2024, 00:00:00) [GCC 14.0.1 20240411 (Red Hat 14.0.1-0)] # # Command line options: @@ -12468,39 +12468,6 @@ def _buildChildren( obj_.original_tagname_ = "inputList" super(Network, self)._buildChildren(child_, node, nodeName_, True) - warn_count = 0 - - def get_by_id(self, id: str) -> typing.Optional[typing.Any]: - """Get a component by its ID - - :param id: ID of component to find - :type id: str - :returns: component with specified ID or None if no component with specified ID found - """ - all_ids = [] - for ms in self.member_data_items_: - mlist = getattr(self, ms.get_name()) - # TODO: debug why this is required - if mlist is None: - continue - for m in mlist: - if hasattr(m, "id"): - if m.id == id: - return m - else: - all_ids.append(m.id) - if self.warn_count < 10: - neuroml.print_( - "Id " - + id - + " not found in element. All ids: " - + str(sorted(all_ids)) - ) - self.warn_count += 1 - elif self.warn_count == 10: - neuroml.print_(" - Suppressing further warnings about id not found...") - return None - def __str__(self): return ( "Network " @@ -42532,46 +42499,6 @@ def summary(self, show_includes=True, show_non_network=True): return info - warn_count = 0 - - def get_by_id(self, id: str) -> typing.Optional[typing.Any]: - """Get a component by specifying its ID. - - :param id: id of Component to get - :type id: str - :returns: Component with given ID or None if no Component with provided ID was found - """ - if len(id) == 0: - callframe = inspect.getouterframes(inspect.currentframe(), 2) - print( - "Method: " + callframe[1][3] + " is asking for an element with no id..." - ) - - return None - all_ids = [] - for ms in self.member_data_items_: - mlist = getattr(self, ms.get_name()) - # TODO: debug why this is required - if mlist is None: - continue - for m in mlist: - if hasattr(m, "id"): - if m.id == id: - return m - else: - all_ids.append(m.id) - if self.warn_count < 10: - neuroml.print_( - "Id " - + id - + " not found in element. All ids: " - + str(sorted(all_ids)) - ) - self.warn_count += 1 - elif self.warn_count == 10: - neuroml.print_(" - Suppressing further warnings about id not found...") - return None - def append(self, element): """Append an element From 3a9f3e5aaccb465c3f5b365ce73dea949f05cc5b Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 16 Aug 2024 16:59:17 +0100 Subject: [PATCH 13/21] test(nml): add another test for get_by_id --- neuroml/test/test_nml.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/neuroml/test/test_nml.py b/neuroml/test/test_nml.py index 99134e2..c4bad93 100644 --- a/neuroml/test/test_nml.py +++ b/neuroml/test/test_nml.py @@ -192,6 +192,18 @@ def test_get_by_id(self): test_pop = network.get_by_id("pop0") self.assertIs(test_pop, pop) + def test_get_by_id2(self): + """Test the get_by_id method, but for attribute""" + channel = neuroml.IonChannel(id="test", species="k", conductance="10pS") + species = channel.get_by_id("species") + self.assertEqual(species, "k") + conductance = channel.get_by_id("conductance") + self.assertEqual(conductance, "10pS") + + # can now be edited + conductance = "20pS" + self.assertEqual(conductance, "20pS") + def test_component_validate(self): """Test validate function""" network = neuroml.Network() From d2bba990ce1fb6eaf53a4ef7a4c2293fee0a258e Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 16 Aug 2024 17:31:24 +0100 Subject: [PATCH 14/21] fix(nml): correctly append to all ids --- neuroml/nml/generatedssupersuper.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index f7e7276..9776423 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -692,19 +692,20 @@ def get_by_id(self, id_): all_ids = [] all_members = self._get_members() for member in all_members: + # not a list if member.get_container() == 0: if member.get_name() == id_: return getattr(self, member.get_name()) else: all_ids.append(member.get_name()) else: - contents = getattr(self, member.get_name(), None) + contents = getattr(self, member.get_name(), []) for m in contents: if hasattr(m, "id"): if m.id == id_: return m - else: - all_ids.append(m.id) + else: + all_ids.append(m.id) self.logger.warning(f"Id '{id_}' not found in {self.__name__}") self.logger.warning(f"All ids: {sorted(all_ids)}") From a60ae01d4100211753e92c64b1378f8e7f9ac083 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Mon, 19 Aug 2024 16:48:28 +0100 Subject: [PATCH 15/21] feat(get_member): also return objects if type or attribute are given --- neuroml/nml/generatedssupersuper.py | 37 +++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index 9776423..dd61b9f 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -682,23 +682,43 @@ def get_nml2_class_hierarchy(cls): cls.__nml_hier = schema.NeuroMLDocument.get_class_hierarchy() return cls.__nml_hier - def get_by_id(self, id_): - """Get a component or attribute by its ID + def get_member(self, id_): + """Get a component or attribute by its ID, or type, or attribute name + + :param id_: id of component ("biophys"), or its type + ("BiophysicalProperties"), or its attribute name + ("biophysical_properties") + + + When the attribute name or type are given, this simply returns the + associated object, which could be a list. - :param id_: ID of component or name of attribute to find :type id_: str - :returns: component with specified ID, or attribute, or None if neither found + :returns: component if found, else None """ all_ids = [] all_members = self._get_members() for member in all_members: + # check name: biophysical_properties + if member.get_name() == id_: + return getattr(self, member.get_name()) + # check type: BiophysicalProperties + elif member.get_data_type() == id_: + return getattr(self, member.get_name()) + + # check contents + # not a list if member.get_container() == 0: - if member.get_name() == id_: - return getattr(self, member.get_name()) + # check contents for id: biophysical_properties.id + contents = getattr(self, member.get_name(), None) + if hasattr(contents, "id"): + if contents.id == id_: + return contents else: all_ids.append(member.get_name()) else: + # container: iterate over each item contents = getattr(self, member.get_name(), []) for m in contents: if hasattr(m, "id"): @@ -707,6 +727,9 @@ def get_by_id(self, id_): else: all_ids.append(m.id) - self.logger.warning(f"Id '{id_}' not found in {self.__name__}") + try: + self.logger.warning(f"Id '{id_}' not found in {self.__name__}") + except AttributeError: + self.logger.warning(f"Id '{id_}' not found in {self.id}") self.logger.warning(f"All ids: {sorted(all_ids)}") return None From 704483ae74afd8839cf3c95b9ccf064f50085492 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 21 Aug 2024 11:38:46 +0100 Subject: [PATCH 16/21] fix(fix_external_morphs/biophys): add checks for missing attributes --- neuroml/utils.py | 57 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/neuroml/utils.py b/neuroml/utils.py index dc3c236..5855882 100644 --- a/neuroml/utils.py +++ b/neuroml/utils.py @@ -316,7 +316,10 @@ def get_relative_component_path( def fix_external_morphs_biophys_in_cell( - nml2_doc: NeuroMLDocument, overwrite: bool = True + nml2_doc: NeuroMLDocument, + overwrite: bool = True, + load_morphology: bool = True, + load_biophysical_properties: bool = True, ) -> NeuroMLDocument: """Handle externally referenced morphologies and biophysics in cells. @@ -338,6 +341,10 @@ def fix_external_morphs_biophys_in_cell( :param overwrite: toggle whether the document is overwritten or a deep copy created :type overwrite: bool + :param load_morphology: whether morphologies should be loaded + :type load_morphology: bool + :param load_biophysical_properties: whether biophysical_properties should be loaded + :type load_biophysical_properties: bool :returns: neuroml document :raises KeyError: if referenced morphologies/biophysics cannot be found """ @@ -349,7 +356,7 @@ def fix_external_morphs_biophys_in_cell( # get a list of morph/biophys ids being referred to by cells referenced_ids = [] for cell in newdoc.cells: - if cell.morphology_attr is not None: + if load_morphology is True and cell.morphology_attr is not None: if cell.morphology is None: referenced_ids.append(cell.morphology_attr) else: @@ -357,7 +364,10 @@ def fix_external_morphs_biophys_in_cell( f"Cell ({cell}) already contains a Morphology, ignoring reference." ) logger.warning("Please check/correct your cell description") - if cell.biophysical_properties_attr is not None: + if ( + load_biophysical_properties is True + and cell.biophysical_properties_attr is not None + ): if cell.biophysical_properties is None: referenced_ids.append(cell.biophysical_properties_attr) else: @@ -369,27 +379,39 @@ def fix_external_morphs_biophys_in_cell( # load referenced ids from included files and store them in dicts ext_morphs = {} ext_biophys = {} - for inc in newdoc.includes: - incdoc = loaders.read_neuroml2_file(inc.href, verbose=False, optimized=True) - for morph in incdoc.morphology: + # includes/morphology/biophysical_properties should generally be empty + # lists and not None, but there may be cases where these have been removed + # after the document was loaded + if newdoc.includes: + for inc in newdoc.includes: + incdoc = loaders.read_neuroml2_file(inc.href, verbose=False, optimized=True) + if load_morphology is True and incdoc.morphology: + for morph in incdoc.morphology: + if morph.id in referenced_ids: + ext_morphs[morph.id] = morph + if load_biophysical_properties is True and incdoc.biophysical_properties: + for biophys in incdoc.biophysical_properties: + if biophys.id in referenced_ids: + ext_biophys[biophys.id] = biophys + + if load_morphology is True and newdoc.morphology: + for morph in newdoc.morphology: if morph.id in referenced_ids: ext_morphs[morph.id] = morph - for biophys in incdoc.biophysical_properties: + + if load_biophysical_properties is True and newdoc.biophysical_properties: + for biophys in newdoc.biophysical_properties: if biophys.id in referenced_ids: ext_biophys[biophys.id] = biophys - # also include morphs/biophys that are in the same document - for morph in newdoc.morphology: - if morph.id in referenced_ids: - ext_morphs[morph.id] = morph - for biophys in newdoc.biophysical_properties: - if biophys.id in referenced_ids: - ext_biophys[biophys.id] = biophys - # update cells by placing the morphology/biophys in them: # if referenced ids are not found, throw errors for cell in newdoc.cells: - if cell.morphology_attr is not None and cell.morphology is None: + if ( + load_morphology is True + and cell.morphology_attr is not None + and cell.morphology is None + ): try: # TODO: do we need a deepcopy here? cell.morphology = copy.deepcopy(ext_morphs[cell.morphology_attr]) @@ -401,7 +423,8 @@ def fix_external_morphs_biophys_in_cell( raise e if ( - cell.biophysical_properties_attr is not None + load_biophysical_properties is True + and cell.biophysical_properties_attr is not None and cell.biophysical_properties is None ): try: From 86e89150cae33fb1f8c8496f84e9cce5386e2e65 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 21 Aug 2024 15:30:06 +0100 Subject: [PATCH 17/21] feat(fix-morph-bio): only bother reading include files if anything is referenced --- neuroml/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/neuroml/utils.py b/neuroml/utils.py index 5855882..5e97600 100644 --- a/neuroml/utils.py +++ b/neuroml/utils.py @@ -376,6 +376,10 @@ def fix_external_morphs_biophys_in_cell( ) logger.warning("Please check/correct your cell description") + if len(referenced_ids) == 0: + logger.debug("No externally referenced morphologies or biophysics") + return newdoc + # load referenced ids from included files and store them in dicts ext_morphs = {} ext_biophys = {} From 124eefe1507c2a1b1040efe4a47ebcb3b5d966ab Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 21 Aug 2024 15:46:34 +0100 Subject: [PATCH 18/21] feat(fix-morph): optimise further to only load stuff if required --- neuroml/utils.py | 66 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/neuroml/utils.py b/neuroml/utils.py index 5e97600..af26761 100644 --- a/neuroml/utils.py +++ b/neuroml/utils.py @@ -15,7 +15,7 @@ import networkx import neuroml.nml.nml as schema -from neuroml import NeuroMLDocument +from neuroml import BiophysicalProperties, Morphology, NeuroMLDocument from . import loaders @@ -348,14 +348,9 @@ def fix_external_morphs_biophys_in_cell( :returns: neuroml document :raises KeyError: if referenced morphologies/biophysics cannot be found """ - if overwrite is False: - newdoc = copy.deepcopy(nml2_doc) - else: - newdoc = nml2_doc - # get a list of morph/biophys ids being referred to by cells referenced_ids = [] - for cell in newdoc.cells: + for cell in nml2_doc.cells: if load_morphology is True and cell.morphology_attr is not None: if cell.morphology is None: referenced_ids.append(cell.morphology_attr) @@ -378,35 +373,66 @@ def fix_external_morphs_biophys_in_cell( if len(referenced_ids) == 0: logger.debug("No externally referenced morphologies or biophysics") - return newdoc + return nml2_doc + + if overwrite is False: + newdoc = copy.deepcopy(nml2_doc) + else: + newdoc = nml2_doc # load referenced ids from included files and store them in dicts - ext_morphs = {} - ext_biophys = {} + found = False + ext_morphs: Dict[str, Morphology] = {} + ext_biophys: Dict[str, BiophysicalProperties] = {} + + # first check the same document + if not found and load_morphology is True and newdoc.morphology: + for morph in newdoc.morphology: + if morph.id in referenced_ids: + ext_morphs[morph.id] = morph + + if (len(ext_morphs) + len(ext_biophys)) == len(referenced_ids): + logger.debug("Found all references") + found = True + break + + if ( + not found + and load_biophysical_properties is True + and newdoc.biophysical_properties + ): + for biophys in newdoc.biophysical_properties: + if biophys.id in referenced_ids: + ext_biophys[biophys.id] = biophys + + if (len(ext_morphs) + len(ext_biophys)) == len(referenced_ids): + logger.debug("Found all references") + found = True + break + + # now check included files (they need to be loaded and parsed, so this can + # be computationally intensive) # includes/morphology/biophysical_properties should generally be empty # lists and not None, but there may be cases where these have been removed # after the document was loaded - if newdoc.includes: + if not found and newdoc.includes: for inc in newdoc.includes: incdoc = loaders.read_neuroml2_file(inc.href, verbose=False, optimized=True) + if load_morphology is True and incdoc.morphology: for morph in incdoc.morphology: if morph.id in referenced_ids: ext_morphs[morph.id] = morph + if load_biophysical_properties is True and incdoc.biophysical_properties: for biophys in incdoc.biophysical_properties: if biophys.id in referenced_ids: ext_biophys[biophys.id] = biophys - if load_morphology is True and newdoc.morphology: - for morph in newdoc.morphology: - if morph.id in referenced_ids: - ext_morphs[morph.id] = morph - - if load_biophysical_properties is True and newdoc.biophysical_properties: - for biophys in newdoc.biophysical_properties: - if biophys.id in referenced_ids: - ext_biophys[biophys.id] = biophys + if (len(ext_morphs) + len(ext_biophys)) == len(referenced_ids): + logger.debug("Found all references") + found = True + break # update cells by placing the morphology/biophys in them: # if referenced ids are not found, throw errors From db7e44258c2579e129490b3cade74583e4381f19 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Wed, 21 Aug 2024 16:27:39 +0100 Subject: [PATCH 19/21] chore: revert method name to `get_by_id` I was breaking API here! --- neuroml/nml/generatedssupersuper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index dd61b9f..5254024 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -682,7 +682,7 @@ def get_nml2_class_hierarchy(cls): cls.__nml_hier = schema.NeuroMLDocument.get_class_hierarchy() return cls.__nml_hier - def get_member(self, id_): + def get_by_id(self, id_): """Get a component or attribute by its ID, or type, or attribute name :param id_: id of component ("biophys"), or its type From 5b63e125c669ed1f0142fd9e43f501f9a236da12 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 23 Aug 2024 11:42:29 +0100 Subject: [PATCH 20/21] test(nml): add test for addition of "__ANY__" elements --- neuroml/nml/generatedssupersuper.py | 15 +++++++++++++-- neuroml/test/test_nml.py | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/neuroml/nml/generatedssupersuper.py b/neuroml/nml/generatedssupersuper.py index 5254024..5fa4e1b 100644 --- a/neuroml/nml/generatedssupersuper.py +++ b/neuroml/nml/generatedssupersuper.py @@ -37,7 +37,14 @@ def add(self, obj=None, hint=None, force=False, validate=True, **kwargs): calling (parent) component type object. :param obj: member object or class type (neuroml.NeuroMLDocument) or - name of class type ("NeuroMLDocument"), or None + name of class type ("NeuroMLDocument"), a string, or None + + If the string that is passed is not the name of a NeuroML + component type, it will be added as a string if the parent + component type allows "any" elements. This does not mean that one + can add anything, since it must still be valid XML/LEMS. The + only usecase for this currently is to add RDF strings to the + Annotation component type. :type obj: str or type or None :param hint: member name to add to when there are multiple members that `obj` can be added to :type hint: string @@ -159,12 +166,16 @@ def component_factory(cls, component_type, validate=True, **kwargs): Note that when providing the class type, one will need to import it, e.g.: `import NeuroMLDocument`, to ensure that it is defined, whereas this will not be required when using the string. + + If the value passed for component_type is a general string (with + spaces), it is simply returned :type component_type: str/type :param validate: toggle validation (default: True) :type validate: bool :param kwargs: named arguments to be passed to ComponentType constructor :type kwargs: named arguments - :returns: new Component (object) of provided ComponentType + :returns: new Component (object) of provided ComponentType, or + unprocessed string with spaces if given for component_type :rtype: object :raises ValueError: if validation/checks fail diff --git a/neuroml/test/test_nml.py b/neuroml/test/test_nml.py index c4bad93..29d12a1 100644 --- a/neuroml/test/test_nml.py +++ b/neuroml/test/test_nml.py @@ -841,6 +841,21 @@ def test_class_hierarchy(self): print() print_hierarchy(hier) + def test_adding_any(self): + """Test adding things to __ANY__ attributes""" + newdoc = neuroml.NeuroMLDocument(id="lol") + annotation = newdoc.add(neuroml.Annotation) + # valid NeuroML, but not valid LEMS + # space required to distinguish it from the name of a component type, + # which will not have spaces + annotation.add(" some_string", hint="__ANY__") + + # remove all spaces to test the string + annotation_text = str(annotation) + annotation_text = "".join(annotation_text.split()) + print(annotation_text) + self.assertEqual("some_string", annotation_text) + if __name__ == "__main__": ta = TestNML() From 5e36a9f31c8a769dfe9e0c164d0a2df24c88a6e0 Mon Sep 17 00:00:00 2001 From: "Ankur Sinha (Ankur Sinha Gmail)" Date: Fri, 23 Aug 2024 11:47:53 +0100 Subject: [PATCH 21/21] test(nml): test addition of __ANY__ --- neuroml/test/test_nml.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/neuroml/test/test_nml.py b/neuroml/test/test_nml.py index 29d12a1..6c71041 100644 --- a/neuroml/test/test_nml.py +++ b/neuroml/test/test_nml.py @@ -841,6 +841,20 @@ def test_class_hierarchy(self): print() print_hierarchy(hier) + def test_adding_any_exception(self): + """Test adding things to __ANY__ attributes exception raise""" + newdoc = neuroml.NeuroMLDocument(id="lol") + annotation = newdoc.add(neuroml.Annotation) + + # without hint="__ANY__", we raise an exception + with self.assertRaises(Exception) as cm: + annotation.add(" some_string") + + self.assertEqual( + """Received a text object to add. Please pass `hint="__ANY__"` to confirm that this is what you intend. I will then try to add this to an __ANY__ member in the object.""", + str(cm.exception), + ) + def test_adding_any(self): """Test adding things to __ANY__ attributes""" newdoc = neuroml.NeuroMLDocument(id="lol")