Skip to content

Commit 024ea24

Browse files
authored
Merge pull request #54 from mqnifestkelvin/feature_xml_3
Feature xml 3
2 parents 6339fd0 + 954f290 commit 024ea24

File tree

4 files changed

+141
-34
lines changed

4 files changed

+141
-34
lines changed

examples/neuroml2/TestNeuroML.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" ?>
2-
<neuroml xmlns="http://www.neuroml.org/schema/neuroml2" id="TestNeuroML">
2+
<neuroml xmlns="http://www.neuroml.org/schema/neuroml2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="TestNeuroML" xsi:schemaLocation="http://www.neuroml.org/schema/neuroml2https://raw.github.com/NeuroML/NeuroML2/development/Schemas/NeuroML2/NeuroML_v2.3.xsd">
33
<izhikevich2007Cell id="izh2007RS0" C="100pF" v0="-60mV" k="0.7nS_per_mV" vr="-60mV" vt="-40mV" vpeak="35mV" a="0.03per_ms" b="-2nS" c="-50.0mV" d="100pA"/>
44
<pulseGenerator id="pulseGen_0" delay="100ms" duration="800ms" amplitude="0.07 nA"/>
55
<network id="IzNet">

examples/neuroml2/neuroml2_spec.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ class neuroml(Base):
109109
xmlns: str = field(
110110
validator=instance_of(str), default="http://www.neuroml.org/schema/neuroml2"
111111
)
112+
xmlns_xsi: str = field(
113+
validator=instance_of(str), default="http://www.w3.org/2001/XMLSchema-instance"
114+
)
115+
xmlns_loc: str = field(
116+
validator=instance_of(str),
117+
default="http://www.neuroml.org/schema/neuroml2https://raw.github.com/NeuroML/NeuroML2/development/Schemas/NeuroML2/NeuroML_v2.3.xsd",
118+
)
112119

113120
izhikevich2007Cells: List[izhikevich2007Cell] = field(factory=list)
114121
pulseGenerators: List[pulseGenerator] = field(factory=list)
@@ -187,3 +194,8 @@ class neuroml(Base):
187194
yy = yaml.dump(doc_dict, indent=4, sort_keys=False)
188195
print(yy)
189196
d.write(yy)
197+
198+
from modelspec.utils import load_xml
199+
200+
new_neuroml = load_xml("hello_world_neuroml.net.nml")
201+
print(new_neuroml)

src/modelspec/base_types.py

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ def to_xml(self) -> str:
125125
)
126126
from modelspec.utils import build_xml_element
127127

128-
# root = ET.Element("modelspec")
129128
root = build_xml_element(self)
130129

131130
xml_string = ET.tostring(
@@ -170,12 +169,38 @@ def from_bson(cls, bson_str: str) -> "Base":
170169
@classmethod
171170
def from_xml(cls, xml_str: str) -> "Base":
172171
"""Instantiate a Base object from an XML string"""
173-
from modelspec.utils import element_to_dict, handle_id, convert_values
172+
from modelspec.utils import (
173+
elementtree_element_to_dict,
174+
handle_xml_dict_id,
175+
convert_xml_dict_values,
176+
process_xml_namespace,
177+
)
178+
import re
179+
180+
# When the to_xml() method is used it messes up the string therefore,
181+
# it is necessary to convert it into an elementree object then decode into a string.
182+
xml_string_a = ET.fromstring(xml_str)
183+
xml_string_b = ET.tostring(xml_string_a).decode()
184+
185+
# while trying to obtain a useable xml structure, using the conversion above it acquires
186+
# some unusual string element that sometimes can be incremental from either :ns0 to :nsX or ns0: to nsX:.
187+
# Using the regex expression pattern catches it in any form and removes it from the xml string structure.
188+
ns_prefix_pattern = r"(ns\d+:|:ns\d+)"
189+
cleaned_xml = re.sub(ns_prefix_pattern, "", xml_string_b).strip()
190+
191+
# For the xml to be useable in modelspec unnecessary string elements which only serve as asthetics for the xml must
192+
# be removed when converting to a dict, the process_xml_namespaes function does just that.
193+
removed_namespaces = process_xml_namespace(cleaned_xml)
194+
195+
# process_xml_namespace function returns an elementtree object which can be directly worked upon by the elementtree_element_to_dict
196+
# function, this returns a python dictionary
197+
data_dict = elementtree_element_to_dict(removed_namespaces)
198+
199+
# This strips every instance of 'id' from the resulting dictionary structure
200+
removed_id = handle_xml_dict_id(data_dict)
174201

175-
root = ET.fromstring(xml_str)
176-
data_dict = element_to_dict(root)
177-
removed_id = handle_id(data_dict)
178-
converted_to_actual_val = convert_values(removed_id)
202+
# XML conversions do not returns exact values, instead all values are returned as a string, this reassigns their actual values
203+
converted_to_actual_val = convert_xml_dict_values(removed_id)
179204

180205
return cls.from_dict(converted_to_actual_val)
181206

@@ -377,15 +402,38 @@ def from_xml_file(cls, filename: str) -> "Base":
377402
Returns:
378403
A modelspec Base for this XML.
379404
"""
380-
from modelspec.utils import element_to_dict, handle_id, convert_values
405+
from modelspec.utils import (
406+
elementtree_element_to_dict,
407+
handle_xml_dict_id,
408+
convert_xml_dict_values,
409+
process_xml_namespace,
410+
)
411+
import re
381412

382413
with open(filename) as infile:
383-
tree = ET.parse(infile)
384-
root = tree.getroot()
414+
tree = ET.parse(infile) # Parse the XML file into an ElementTree object
415+
root = tree.getroot() # Get the root element
416+
417+
# This defines regular expressions to match the namespace patterns to be removed
418+
ns_prefix_pattern = r"(ns\d+:|:ns\d+)"
419+
420+
# Converts the loaded xml into a string and removes unwanted string values ':ns0' to :ns∞ and 'ns0:' to ns∞:
421+
# They prevent the xml from loading correctly
422+
xml_string = ET.tostring(root).decode()
423+
cleaned_xml = re.sub(ns_prefix_pattern, "", xml_string).strip()
424+
425+
# Removes xmlns, xmlns:xsi and xsi:schemaLocation from the xml structure for conversion
426+
# it passes an element tree object to the elementtree_element_to_dict function
427+
removed_namespaces = process_xml_namespace(cleaned_xml)
428+
429+
# Converts the resulting xml stripped of xmlns, xmlns:xsi and xsi:schemaLocation into a dict
430+
data_dict = elementtree_element_to_dict(removed_namespaces)
431+
432+
# Removes every key having 'id' and replaces it with it's value
433+
removed_id = handle_xml_dict_id(data_dict)
385434

386-
data_dict = element_to_dict(root)
387-
removed_id = handle_id(data_dict)
388-
converted_to_actual_val = convert_values(removed_id)
435+
# Values are returned as strings after conversion, this corrects them to their actual values
436+
converted_to_actual_val = convert_xml_dict_values(removed_id)
389437
return cls.from_dict(converted_to_actual_val)
390438

391439
def get_child(self, id: str, type_: str) -> Any:

src/modelspec/utils.py

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,35 @@ def load_xml(filename: str):
6767
Args:
6868
filename: The name of the XML file to load.
6969
"""
70+
import re
71+
7072
with open(filename, "rb") as infile:
7173
tree = ET.parse(infile) # Parse the XML file into an ElementTree object
7274
root = tree.getroot() # Get the root element
7375

74-
# Convert the ElementTree object to a dictionary
75-
data = element_to_dict(root)
76-
removed_id = handle_id(data)
77-
converted_to_actual_val = convert_values(removed_id)
76+
# This defines regular expressions to match the namespace patterns to be removed
77+
ns_prefix_pattern = r"(ns\d+:|:ns\d+)"
78+
79+
# Converts the loaded xml into a string and removes unwanted string values ':ns0' to :ns∞ and 'ns0:' to ns∞:
80+
# They prevent the xml from loading correctly
81+
xml_string = ET.tostring(root).decode()
82+
cleaned_xml = re.sub(ns_prefix_pattern, "", xml_string).strip()
83+
84+
# Removes xmlns, xmlns:xsi and xsi:schemaLocation from the xml structure for conversion
85+
# it passes an element tree object to the elementtree_element_to_dict function
86+
removed_namespaces = process_xml_namespace(cleaned_xml)
87+
88+
# Converts the resulting xml stripped of xmlns, xmlns:xsi and xsi:schemaLocation into a dict
89+
data = elementtree_element_to_dict(removed_namespaces)
7890

79-
return convert_values(converted_to_actual_val)
91+
# Removes every key having 'id' and replaces it with it's value
92+
removed_id = handle_xml_dict_id(data)
8093

94+
# Values are returned as strings after conversion, this corrects them to their actual values
95+
return convert_xml_dict_values(removed_id)
8196

82-
def element_to_dict(element):
97+
98+
def elementtree_element_to_dict(element):
8399
"""
84100
This convert an ElementTree element to a dictionary.
85101
@@ -94,35 +110,57 @@ def element_to_dict(element):
94110
if attrs:
95111
result.update(attrs)
96112

113+
children_by_tag = {}
97114
for child_element in element:
98-
child_key = child_element.tag
99-
child_value = element_to_dict(child_element)
115+
child_key = child_element.tag + "s"
116+
child_value = elementtree_element_to_dict(child_element)
100117

101-
if child_key in result:
102-
if not isinstance(result[child_key], list):
103-
result[child_key] = [result[child_key]]
104-
result[child_key].append(child_value)
105-
else:
118+
# Check if the child element has an 'id' attribute
119+
if "id" in child_element.attrib:
120+
# If the child element has an 'id', add it to the result dictionary directly
106121
result[child_key] = child_value
122+
else:
123+
# If the child element does not have an 'id', represent it as a list
124+
children_by_tag.setdefault(child_key, []).append(child_value)
125+
126+
# Append the lists to the result dictionary
127+
result.update(children_by_tag)
107128

108129
return result
109130

110131

111-
def handle_id(dictionary):
132+
def process_xml_namespace(xml_string):
133+
# Remove ignored elements from the XML string
134+
ignored_elements = [
135+
'xmlns="http://www.neuroml.org/schema/neuroml2"',
136+
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
137+
'xsi:schemaLocation="http://www.neuroml.org/schema/neuroml2 https://raw.github.com/NeuroML/NeuroML2/development/Schemas/NeuroML2/NeuroML_v2.3.xsd"',
138+
]
139+
140+
# Loops through the xml string and removes every instance of the elements in the list named ignored_elements
141+
for ignored_element in ignored_elements:
142+
xml_string = xml_string.replace(ignored_element, "").strip()
143+
144+
# Parse the XML string into an ElementTree
145+
root = ET.fromstring(xml_string)
146+
return root
147+
148+
149+
def handle_xml_dict_id(dictionary):
112150
if isinstance(dictionary, dict):
113151
if "id" in dictionary:
114152
nested_dict = {dictionary["id"]: dictionary.copy()}
115153
del nested_dict[dictionary["id"]]["id"]
116-
return {k: handle_id(v) for k, v in nested_dict.items()}
154+
return {k: handle_xml_dict_id(v) for k, v in nested_dict.items()}
117155
else:
118-
return {k: handle_id(v) for k, v in dictionary.items()}
156+
return {k: handle_xml_dict_id(v) for k, v in dictionary.items()}
119157
elif isinstance(dictionary, list):
120-
return [handle_id(item) for item in dictionary]
158+
return [handle_xml_dict_id(item) for item in dictionary]
121159
else:
122160
return dictionary
123161

124162

125-
def convert_values(value):
163+
def convert_xml_dict_values(value):
126164
"""
127165
This recursively converts values to their actual types.
128166
@@ -146,9 +184,9 @@ def convert_values(value):
146184
elif value.lower() == "none":
147185
return None
148186
elif isinstance(value, dict):
149-
return {key: convert_values(val) for key, val in value.items()}
187+
return {key: convert_xml_dict_values(val) for key, val in value.items()}
150188
elif isinstance(value, list):
151-
return [convert_values(item) for item in value]
189+
return [convert_xml_dict_values(item) for item in value]
152190

153191
return value
154192

@@ -219,11 +257,20 @@ def build_xml_element(data, parent=None):
219257
for child in children:
220258
child_element = build_xml_element(child)
221259
parent.append(child_element)
222-
else:
260+
261+
# Filters name space and schemaLoacation attributes, only allows non name space attributes to be added as attributes
262+
elif not isinstance(aattr.default, str):
223263
attribute_name = aattr.name
224264
attribute_value = data.__getattribute__(aattr.name)
225265
parent.set(attribute_name, str(attribute_value))
226266

267+
# This defines the various namespaces and schemaLocation of the generated xml
268+
if hasattr(data, "xmlns"):
269+
parent.set("xmlns", data.xmlns)
270+
if hasattr(data, "xmlns_xsi"):
271+
parent.set("xmlns:xsi", data.xmlns_xsi)
272+
if hasattr(data, "xmlns_loc"):
273+
parent.set("xsi:schemaLocation", str(data.xmlns_loc))
227274
return parent
228275

229276

0 commit comments

Comments
 (0)