Skip to content

Commit

Permalink
Gen init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
sstucker committed Nov 17, 2021
1 parent ed4bcb6 commit 915ca63
Show file tree
Hide file tree
Showing 10 changed files with 744 additions and 729 deletions.
79 changes: 79 additions & 0 deletions gen/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from abc import ABC, abstractmethod
import h5py
import os
import sys

if sys.version_info[0] < 3:
raise ImportError('pysnirf2 requires Python > 3')


class SnirfFormatError(Exception):
pass


class _Group():

def __init__(self, gid: h5py.h5g.GroupID):
self._id = gid
if not isinstance(gid, h5py.h5g.GroupID):
raise TypeError('must initialize with a Group ID')
self._group = h5py.Group(self._id)

def __repr__(self):
return str(list(filter(lambda x: not x.startswith('_'), vars(self).keys())))



class _IndexedGroup(list, ABC):
"""
Represents the "indexed group" which is defined by v1.0 of the SNIRF
specification as:
If a data element is an HDF5 group and contains multiple sub-groups,
it is referred to as an indexed group. Each element of the sub-group
is uniquely identified by appending a string-formatted index (starting
from 1, with no preceding zeros) in the name, for example, /.../name1
denotes the first sub-group of data element name, and /.../name2
denotes the 2nd element, and so on.
"""

_name: str = ''
_element: _Group = None

def __init__(self, parent):
if isinstance(parent, (h5py.Group, h5py.File)):
# Because the indexed group is not a true HDF5 group but rather an
# iterable list of HDF5 groups, it takes a base group or file and
# searches its keys, appending the appropriate elements to itself
# in order
self._parent = parent
print('class name', self.__class__.__name__, 'signature', self._name)
i = 1
for key in self._parent.keys():
name = str(key).split('/')[-1]
print('Looking for keys starting with', self._name)
if key.startswith(self._name):
if key.endswith(str(i)):
print('adding numbered key', i, name)
self.append(self._element(self._parent[key].id))
i += 1
elif i == 1 and key.endswith(self._name):
print('adding non-numbered key', name)
self.append(self._element(self._parent[key].id))

else:
raise TypeError('must initialize _IndexedGroup with a Group or File')

def __new__(cls, *args, **kwargs):
if cls is _IndexedGroup:
raise NotImplementedError('_IndexedGroup is an abstract class')
return super().__new__(cls, *args, **kwargs)

@abstractmethod
def _append_group(self, gid: h5py.h5g.GroupID):
raise NotImplementedError('_append_group is an abstract method')

def __repr__(self):
prettylist = ''
for i in range(len(self)):
prettylist += (str(self[i]) + '\n')
return prettylist
12 changes: 12 additions & 0 deletions gen/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
SPEC_SRC = 'https://raw.github.com/fNIRS/snirf/master/snirf_specification.md'
SPEC_VERSION = '1.0' # Version of the spec linked above
TYPELUT = {
'GROUP': '{.}',
'INDEXED_GROUP': '{i}',
'REQUIRED': '*',
'1D_ARRAY': '...]',
'2D_ARRAY': '...]]',
'INT_VALUE': '<i>',
'FLOAT_VALUE': '<f>',
'VAR_STRING': '"s"'
}
126 changes: 126 additions & 0 deletions gen/gen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from jinja2 import Environment, FileSystemLoader, select_autoescape
import requests
from unidecode import unidecode
from pathlib import Path
from datetime import date
import getpass
import os


from data import TYPELUT, SPEC_SRC, SPEC_VERSION

"""
Generates SNIRF interface and validator from the summary table of the specification
hosted at SPEC_MD_RAW_SRC.
"""

env = Environment(
loader=FileSystemLoader(searchpath='./'),
autoescape=select_autoescape(),
trim_blocks=True,
lstrip_blocks=True,
)

template = env.get_template('pysnirf2.jinja')

# Retrieve the SNIRF specification from GitHub and parse it for the summary table
spec = requests.get(SPEC_SRC)
table = unidecode(spec.text).split('### SNIRF data format summary')[1].split('In the above table, the used notations are explained below')[0]

rows = table.split('\n')
while '' in rows:
rows.remove('')

type_codes = []

# Parse each row of the summary table for programmatic info
for row in rows[1:]: # Skip header row
delim = row.split('|')

# Number of leading spaces determines indent level, hierarchy
namews = delim[1].replace('`', '').replace('.','').replace('-', '')
name = namews.replace(' ', '').replace('/', '').replace('{i}', '')

# Get name: format pairs for each name
if len(name) > 1: # Skip the empty row
type_code = delim[-2].replace(' ', '').replace('`', '')
type_codes.append(type_code)

# Parse headings in spec for complete HDF style locations with which to build tree
definitions = unidecode(spec.text).split('### SNIRF data container definitions')[1].split('## Appendix')[0]
lines = definitions.split('\n')
while '' in lines:
lines.remove('')

locations = []
for line in lines:
line = line.replace(' ', '')
if line.startswith('####'):
locations.append(line.replace('`', '').replace('#', '').replace(',', ''))

# Create flat list of all nodes
flat = []

# Append root
flat.append({
'name': '',
'location': '/',
'parent': None,
'type': None,
'children': [],
'required': False
})

for i, location in enumerate(locations):
type_code = type_codes[i]
name = location.split('/')[-1].split('(')[0] # Remove (i), (j)
parent = location.split('/')[-2].split('(')[0] # Remove (i), (j)
# print(name, type_code)
if type_code is not None:
required = TYPELUT['REQUIRED'] in type_code
else:
required = None
# if name in type_codes.keys():
# type_code = type_codes[name]
# required = TYPELUT['REQUIRED'] in type_code
# else:
# warn('Found a name with no corresponding type: ' + name)
# type_code = None
# required = None
if not any([location == node['location'] for node in flat]):
flat.append({
'name': name,
'location': location,
'parent': parent,
'type': type_code,
'children': [],
'required': required
})

# Generate data for template
SNIRF = {
'BASE': '',
'TYPES': TYPELUT,
'USER': getpass.getuser(),
'DATE': str(date.today()),
'INDEXED_GROUPS': [],
'GROUPS': [],
}

# Build list of groups and indexed groups
for node in flat:
if node['type'] is not None:
for other_node in flat:
if other_node['parent'] == node['name']:
node['children'].append(other_node)
if TYPELUT['GROUP'] in node['type']:
SNIRF['GROUPS'].append(node)
elif TYPELUT['INDEXED_GROUP'] in node['type']:
SNIRF['INDEXED_GROUPS'].append(node)

# Generate the complete Snirf interface from base.py and the template + data
dst = os.path.abspath(os.path.join(os.getcwd(), os.pardir))
with open('base.py', 'r') as f_base:
with open(dst + '/src/' + 'pysnirf2.py', "w") as f_out:
SNIRF['BASE'] = f_base.read()
f_out.write(template.render(SNIRF))
89 changes: 89 additions & 0 deletions gen/pysnirf2.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{% macro sentencecase(text) %}
{{- text[0]|upper}}{{text[1:] -}}
{% endmacro %}
{{ BASE }}

# pysnirf2 dot jinja output
# generated by {{ USER }} on {{ DATE }}
{% for GROUP in GROUPS %}


class {{ sentencecase(GROUP.name) -}}(_Group):

{% for CHILD in GROUP.children %}
{{ CHILD.name }} = None
{% endfor %}

def __init__(self, gid: h5py.h5g.GroupID):
super().__init__(gid)
{% for CHILD in GROUP.children %}
{% if '{{TYPES.INDEXED_GROUP}}' in CHILD.type %}
self.{{- CHILD.name }} = {{ sentencecase(CHILD.name) }}(self._group) # Indexed group
{% elif '{{TYPES.GROUP}}' in CHILD.type %}
if '{{ CHILD.name }}' in self._group.keys():
self.{{- CHILD.name }} = {{ sentencecase(CHILD.name) }}(self._group['{{CHILD.name}}'].id) # Group
else:
print('{{CHILD.name}}', 'not found in', '{{GROUP.name}}')
{% else %}
if '{{ CHILD.name }}' in self._group.keys():
if
self.{{- CHILD.name }} = self._group['{{ CHILD.name }}'][0]
else:
print('{{CHILD.name}}', 'not found in', '{{GROUP.name}}')
{% endif %}
{% endfor %}
{% endfor %}
{% for INDEXED_GROUP in INDEXED_GROUPS %}


class {{ sentencecase(INDEXED_GROUP.name) -}}Element(_Group):

{% for CHILD in INDEXED_GROUP.children %}
{{ CHILD.name }} = None
{% endfor %}

def __init__(self, gid: h5py.h5g.GroupID):
super().__init__(gid)


class {{ sentencecase(INDEXED_GROUP.name) -}}(_IndexedGroup):

_name: str = '{{ INDEXED_GROUP.name }}'
_element: _Group = {{ sentencecase(INDEXED_GROUP.name) -}}Element

def __init__(self, f: h5py.File):
super().__init__(f)

def _append_group(self, gid):
self.append({{ sentencecase(INDEXED_GROUP.name) -}}Element(gid))
{% endfor %}


class Snirf():

_name = '/'
formatVersion: str = None
nirs: Nirs = ()

def __init__(self, argv):
if not argv.endswith('.snirf'):
path = argv.join('.snirf')
else:
path = argv
if os.path.exists(path):
self._f = h5py.File(path, 'r+')
print('Loading snirf', path)
keys = self._f.keys()
if 'formatVersion' in keys:
self.formatVersion = self._f['formatVersion'][0].decode('ascii')
# else:
# raise SnirfFormatError(path, 'does not have a valid formatVersion')
self.nirs = Nirs(self._f)

def __repr__(self):
return str(vars(self))

def __del__(self):
# TODO: this may be redundant
self._f.close()

6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta"
Loading

0 comments on commit 915ca63

Please sign in to comment.