summaryrefslogtreecommitdiff
path: root/gr-utils/python
diff options
context:
space:
mode:
authorArpit Gupta <guptarpit1997@gmail.com>2019-12-19 22:48:00 +0530
committerMarcus Müller <mmueller@gnuradio.org>2019-12-19 18:18:00 +0100
commit116f0401f54e4c8483952118c013b8c668eb3682 (patch)
treec4362ba8274ff68d78f8179603d3173481134775 /gr-utils/python
parentf3dcc45afea4fafa84b0c0e861031105a67bbaf2 (diff)
Block header parsing tool: GSoC 2019 (#2750)
* Add base.py file in cli module to import override Click functions * Create cli and core base module for AST generation of header blocks * Create basic CLI for blocktool with minimal support * Add Sequence Completer to CLI and successful generation of AST * CLI structure complete with parseheader command * Basic core structure complete * Add test script gr_blocktool to run the tool * Add JSON schema and validation for parsed json output file * Change properties and methods key to list in JSON schema * Create an independent api from blocktool * Bug fix for abslute path of the header files * Create basic parser core api * Parse the block header documentation * Expose the core api, minor bug fixes * Create the code pylint compatible * Modify cli to accept file_path as an argument, parse default values of make function arguments * Fix: Namespace parsing of block header file * Parse the io_signature from the implementation file of the block header * Create json file generator * Add key-value io_signature and docstring in json schema, change sample generated json output * Fix: squash an I/O parsing bug * Change directory structure for blocktool tests * Add Blocktool unittest * Removed empty strings, make the code pylint compatible * Use str.format() to get output * Implement YAML generator * Add a new CLI argument to parse a complete header directory * Add Logger to log errors without raising exceptions * Create output schema file in blocktool core * Change directory structure of blocktool and cli commands * write unittests for Blocktool Excceptions * Add sample yaml files * Simplify blocktool cli structure * Refactor blocktool exception handling * Split long blocktool unit-tests * Parse message ports from the implementation file * Add tests for parsed message port id, update sample json files * Add blocktool subdirectory, files in CMakeLists.txt * Remove test files to run Blocktool * Fix: locates implementation file by traversing the module * Integrate blocktool with modtool as an external plugin * Create proper formatting of io_signature for yaml files * Extend modtool makeyaml command to extend support for blocktool * Remove external plugin for modtool support, add blocktool independent script * Minor formatiing, change function name due to conflict with modtool function * Add support to read and add blocktool special comments in header file * Fix: Key Errors, Modify Documentation Reader * Raise warning in case of conflict in the parsed information and blocktool comments * Remove all the blocktool boilerplate cli code and provide minimal support * Remove gr_blocktool script and use blocktool as a python module * Major refactoring of the modtool cli structure to support the blocktool API * Check for PyGCCXML dependency during build * Add README.md for gr-blocktool and remove modtool cli warnings
Diffstat (limited to 'gr-utils/python')
-rw-r--r--gr-utils/python/blocktool/CMakeLists.txt32
-rw-r--r--gr-utils/python/blocktool/README.blocktool28
-rw-r--r--gr-utils/python/blocktool/__init__.py23
-rw-r--r--gr-utils/python/blocktool/__main__.py29
-rw-r--r--gr-utils/python/blocktool/cli.py140
-rw-r--r--gr-utils/python/blocktool/core/CMakeLists.txt31
-rw-r--r--gr-utils/python/blocktool/core/Constants.py67
-rw-r--r--gr-utils/python/blocktool/core/__init__.py0
-rw-r--r--gr-utils/python/blocktool/core/base.py53
-rw-r--r--gr-utils/python/blocktool/core/comments.py270
-rw-r--r--gr-utils/python/blocktool/core/iosignature.py194
-rw-r--r--gr-utils/python/blocktool/core/outputschema.py169
-rw-r--r--gr-utils/python/blocktool/core/parseheader.py277
-rw-r--r--gr-utils/python/blocktool/tests/README.blocktool_test12
-rw-r--r--gr-utils/python/blocktool/tests/sample_json/analog_agc2_cc.json131
-rw-r--r--gr-utils/python/blocktool/tests/sample_json/digital_additive_scrambler_bb.json95
-rw-r--r--gr-utils/python/blocktool/tests/sample_yaml/analog_agc2_cc.yml67
-rw-r--r--gr-utils/python/blocktool/tests/sample_yaml/digital_additive_scrambler_bb.yml63
-rw-r--r--gr-utils/python/blocktool/tests/test_blocktool.py199
-rw-r--r--gr-utils/python/blocktool/tests/test_json_file.py53
-rw-r--r--gr-utils/python/modtool/cli/makeyaml.py37
-rw-r--r--gr-utils/python/modtool/core/__init__.py2
-rw-r--r--gr-utils/python/modtool/core/add.py6
-rw-r--r--gr-utils/python/modtool/core/base.py2
-rw-r--r--gr-utils/python/modtool/core/disable.py2
-rw-r--r--gr-utils/python/modtool/core/makeyaml.py218
-rw-r--r--gr-utils/python/modtool/templates/templates.py2
27 files changed, 2178 insertions, 24 deletions
diff --git a/gr-utils/python/blocktool/CMakeLists.txt b/gr-utils/python/blocktool/CMakeLists.txt
new file mode 100644
index 0000000000..f086800436
--- /dev/null
+++ b/gr-utils/python/blocktool/CMakeLists.txt
@@ -0,0 +1,32 @@
+# Copyright 2019 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+
+include(GrPython)
+
+GR_PYTHON_INSTALL(FILES
+ __init__.py
+ __main__.py
+ cli.py
+ DESTINATION ${GR_PYTHON_DIR}/gnuradio/blocktool
+)
+
+########################################################################
+# Add subdirectories
+########################################################################
+add_subdirectory(core)
diff --git a/gr-utils/python/blocktool/README.blocktool b/gr-utils/python/blocktool/README.blocktool
new file mode 100644
index 0000000000..0183b0093c
--- /dev/null
+++ b/gr-utils/python/blocktool/README.blocktool
@@ -0,0 +1,28 @@
+gr_blocktool: Also known as the block header parsing tool, this tool
+ automatically parses any GNU Radio or OOT block header.
+
+Block header tool from the Command Line Interface
+=================================================
+
+* Parse any GNU Radio or OOT header file with just the file path as an input.
+* Parse a complete header directory with directory-path as the input.
+* Get the output in the form of a YAML or a JSON file.
+* Add blocktool comments automatically in the header file from the implementation file.
+
+
+Integration of blocktool with modtool
+=====================================
+
+* Blocktool API can also be called from modtool.
+* Modtool makeyaml subcommand along with -b flag and a
+ file path as the input can be used to create YAML files for the GRC.
+* YAML output is much better using the blocktool API.
+
+
+Use of blocktool as an independent API
+======================================
+
+Blocktool can be also be used as an independent API which thus can be used to
+parse a block header file during runtime.
+* A single mandatory argument block header file path is required.
+
diff --git a/gr-utils/python/blocktool/__init__.py b/gr-utils/python/blocktool/__init__.py
new file mode 100644
index 0000000000..fd4fbe7cd1
--- /dev/null
+++ b/gr-utils/python/blocktool/__init__.py
@@ -0,0 +1,23 @@
+#
+# Copyright 2019 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+
+from .core.parseheader import BlockHeaderParser
+from .core.outputschema import RESULT_SCHEMA
diff --git a/gr-utils/python/blocktool/__main__.py b/gr-utils/python/blocktool/__main__.py
new file mode 100644
index 0000000000..9ef683d4ed
--- /dev/null
+++ b/gr-utils/python/blocktool/__main__.py
@@ -0,0 +1,29 @@
+#
+# Copyright 2019 Free Software Foundation, Inc.
+# This file is part of GNU Radio
+#
+# GNU Radio Companion is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# GNU Radio Companion is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
+#
+""" main function to run the blocktool api from the command line. """
+
+from __future__ import print_function
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import sys
+from .cli import cli
+
+
+sys.exit(cli())
diff --git a/gr-utils/python/blocktool/cli.py b/gr-utils/python/blocktool/cli.py
new file mode 100644
index 0000000000..dbb7b7cbc5
--- /dev/null
+++ b/gr-utils/python/blocktool/cli.py
@@ -0,0 +1,140 @@
+#
+# Copyright 2019 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+""" Module to generate parsed header output data """
+
+from __future__ import print_function
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import os
+import sys
+import json
+import logging
+
+import click
+from click import ClickException
+
+from gnuradio.modtool.core import yaml_generator
+
+from .core.parseheader import BlockHeaderParser
+
+LOGGER = logging.getLogger(__name__)
+
+
+class BlockToolException(ClickException):
+ """ Exception class for enhanced CLI interface """
+
+ def show(self, file=None):
+ """ displays the colored message """
+ click.secho('BlockToolException: {}'.format(
+ self.format_message()), fg='red')
+
+
+def run_blocktool(module):
+ """Call the run function of the core modules."""
+ try:
+ module.run_blocktool()
+ except BlockToolException as err:
+ click.echo(err, file=sys.stderr)
+ exit(1)
+
+
+@click.command('parseheader',
+ short_help='Generate the parsed output for the header file or directory in a specified format')
+@click.argument('file-path', nargs=1)
+@click.option('--yaml', is_flag=True,
+ help='If given, a YAML response will be printed, else default json will be printed')
+@click.option('-c', '--blocktool-comments', is_flag=True,
+ help='blocktool helper comments will be added in the header file')
+@click.option('-o', '--output', is_flag=True,
+ help='If given, a file with desired output format will be generated')
+def cli(**kwargs):
+ """
+ Block header parsing tool.
+ \b
+ A tool that can be used to automatically parse the headers in GNU Radio project
+ or the OOT modules
+ """
+ kwargs['modtool'] = False
+ if os.path.isfile(kwargs['file_path']):
+ parser = BlockHeaderParser(**kwargs)
+ run_blocktool(parser)
+ if kwargs['yaml']:
+ parser.yaml = True
+ yaml_generator(parser, **kwargs)
+ else:
+ parser.json_confirm = True
+ json_generator(parser, **kwargs)
+ elif os.path.isdir(kwargs['file_path']):
+ parse_directory(**kwargs)
+ else:
+ raise BlockToolException('Invalid file or directory path.')
+
+
+def json_generator(parser, **kwargs):
+ """
+ Generate JSON file for the block header
+ """
+ header = parser.filename.split('.')[0]
+ block = parser.modname.split('-')[-1]
+ if kwargs['output']:
+ json_file = os.path.join('.', block+'_'+header + '.json')
+ with open(json_file, 'w') as _file:
+ json.dump(parser.parsed_data, _file, indent=4)
+ else:
+ print(json.dumps(parser.parsed_data, indent=4))
+
+
+def parse_directory(**kwargs):
+ """
+ Get parsed json and yaml output for complete header directory
+ """
+ kwargs['output'] = True
+ dir_path = kwargs['file_path']
+ dir_path = os.path.abspath(dir_path)
+ list_header = []
+ dir_name = os.path.basename(dir_path)
+ for _header in os.listdir(dir_path):
+ if _header.endswith('.h') and os.path.isfile(os.path.join(dir_path, _header)):
+ list_header.append(os.path.join(dir_path, _header))
+ list_header = sorted(list_header)
+ if list_header:
+ for header_path in list_header:
+ kwargs['file_path'] = header_path
+ header = os.path.basename(header_path)
+ try:
+ parse_dir = BlockHeaderParser(**kwargs)
+ parse_dir.yaml = True
+ parse_dir.json = True
+ run_blocktool(parse_dir)
+ yaml_generator(parse_dir, **kwargs)
+ if not kwargs['modtool']:
+ json_generator(parse_dir, **kwargs)
+ except:
+ logging.basicConfig(level=logging.DEBUG,
+ filename=os.path.join('.', dir_name+'_log.out'))
+ logging.exception(
+ 'Log for Exception raised for the header: {}\n'.format(header))
+ click.secho('Parsing unsuccessful: {}'.format(
+ header), fg='yellow')
+ else:
+ raise BlockToolException(
+ 'Invalid directory! No header found to be parsed')
diff --git a/gr-utils/python/blocktool/core/CMakeLists.txt b/gr-utils/python/blocktool/core/CMakeLists.txt
new file mode 100644
index 0000000000..7b0a704847
--- /dev/null
+++ b/gr-utils/python/blocktool/core/CMakeLists.txt
@@ -0,0 +1,31 @@
+# Copyright 2019 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+
+include(GrPython)
+
+GR_PYTHON_INSTALL(FILES
+ __init__.py
+ base.py
+ comments.py
+ parseheader.py
+ iosignature.py
+ outputschema.py
+ Constants.py
+ DESTINATION ${GR_PYTHON_DIR}/gnuradio/blocktool/core
+)
diff --git a/gr-utils/python/blocktool/core/Constants.py b/gr-utils/python/blocktool/core/Constants.py
new file mode 100644
index 0000000000..694f1b146e
--- /dev/null
+++ b/gr-utils/python/blocktool/core/Constants.py
@@ -0,0 +1,67 @@
+#
+# Copyright 2019 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+""" constants file """
+
+# Kernel Namespace
+KERNEL = 'kernel'
+
+# I/O Signature (Symbols and constants)
+IO_SIGNATURE = 'io_signature::'
+SIGNATURE_LIST = ['makev', 'make3', 'make2', 'make']
+MAKE = 'make'
+MAKE2 = 'make2'
+MAKE3 = 'make3'
+MAKEV = 'makev'
+
+
+# message ports id
+MESSAGE_INPUT = 'message_port_register_in'
+MESSAGE_OUTPUT = 'message_port_register_out'
+
+# Symbols and constants required for parsing
+GR = 'gr-'
+UTILS = 'utils'
+OPEN_BRACKET = '('
+CLOSE_BRACKET = ')'
+STRIP_SYMBOLS = ' ,:)'
+EXCLAMATION = '!'
+
+# Blocktool special comments
+BLOCKTOOL = '! BlockTool'
+END_BLOCKTOOL = 'EndTool !'
+INPUT_SIG = 'input_signature'
+OUTPUT_SIG = 'output_signature'
+INPUT_MIN = 'input_min_streams'
+INPUT_MAX = 'input_max_streams'
+OUTPUT_MIN = 'output_min_streams'
+OUTPUT_MAX = 'output_max_streams'
+INPUT_MAKE_SIZE = 'input_sizeof_stream_item'
+INPUT_MAKEV_SIZE = 'input_sizeof_stream_items'
+INPUT_MAKE_SIZE1 = 'input_sizeof_stream_item1'
+INPUT_MAKE_SIZE2 = 'input_sizeof_stream_item2'
+INPUT_MAKE_SIZE3 = 'input_sizeof_stream_item3'
+OUTPUT_MAKE_SIZE = 'output_sizeof_stream_item'
+OUTPUT_MAKEV_SIZE = 'output_sizeof_stream_items'
+OUTPUT_MAKE_SIZE1 = 'output_sizeof_stream_item1'
+OUTPUT_MAKE_SIZE2 = 'output_sizeof_stream_item2'
+OUTPUT_MAKE_SIZE3 = 'output_sizeof_stream_item3'
+INPUT_PORT = 'message_input'
+OUTPUT_PORT = 'message_output'
diff --git a/gr-utils/python/blocktool/core/__init__.py b/gr-utils/python/blocktool/core/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/gr-utils/python/blocktool/core/__init__.py
diff --git a/gr-utils/python/blocktool/core/base.py b/gr-utils/python/blocktool/core/base.py
new file mode 100644
index 0000000000..5e62835e12
--- /dev/null
+++ b/gr-utils/python/blocktool/core/base.py
@@ -0,0 +1,53 @@
+#
+# Copyright 2019 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+""" Base class for the modules """
+
+from __future__ import print_function
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+from abc import ABC, abstractmethod
+
+
+class BlockToolException(Exception):
+ """ Standard exception for blocktool classes. """
+ pass
+
+
+class BlockTool(ABC):
+ """ Base class for all blocktool command classes. """
+ name = 'base'
+ description = None
+
+ def __init__(self, modname=None, filename=None, targetdir=None,
+ target_file=None, module=None, impldir=None, impl_file=None,
+ yaml=False, json=False, **kwargs):
+ """ __init__ """
+ pass
+
+ def _validate(self):
+ """ Validates the arguments """
+ pass
+
+ @abstractmethod
+ def run_blocktool(self):
+ """ Override this. """
+ pass
diff --git a/gr-utils/python/blocktool/core/comments.py b/gr-utils/python/blocktool/core/comments.py
new file mode 100644
index 0000000000..9938eca122
--- /dev/null
+++ b/gr-utils/python/blocktool/core/comments.py
@@ -0,0 +1,270 @@
+#
+# Copyright 2019 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+""" Module to read and add special blocktool comments in the public header """
+
+from __future__ import print_function
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import warnings
+
+from ..core import Constants
+
+
+def strip_symbols(line):
+ """
+ helper function to strip symbols
+ from blocktool comment syntax
+ """
+ return line.split(':')[-1].lstrip().rstrip()
+
+
+def exist_comments(self):
+ """
+ function to check if blocktool special comments
+ already exist in the public header
+ """
+ _comments = True
+ _index = None
+ lines = []
+ with open(self.target_file, 'r') as header:
+ lines = header.readlines()
+ for line in lines:
+ if Constants.BLOCKTOOL in line:
+ _index = lines.index(line)
+ return bool(_index)
+
+
+def validate_message_port(self, message_ports, suppress_input, suppress_output):
+ """
+ function to solve conflicts if any in the
+ *message_port* comments and the implementation information
+ """
+ if message_ports['input'] != self.parsed_data['message_port']['input']:
+ if not suppress_input:
+ warnings.warn(
+ 'Conflict in values input message port Id. Add ! at the start of the key-value line to mandatory use the comment value.')
+ self.parsed_data['message_port']['input'] = message_ports['input']
+ if message_ports['output'] != self.parsed_data['message_port']['output']:
+ if not suppress_output:
+ warnings.warn(
+ 'Conflict in values output message port Id. Add ! at the start of the key-value line to mandatory use the comment value.')
+ self.parsed_data['message_port']['output'] = message_ports['output']
+
+
+def read_comments(self):
+ """
+ function to read special blocktool comments
+ in the public header
+ """
+ temp_parsed_data = {}
+ if self.parsed_data['io_signature'] or self.parsed_data['message_port']:
+ temp_parsed_data['io_signature'] = self.parsed_data['io_signature']
+ temp_parsed_data['message_port'] = self.parsed_data['message_port']
+ self.parsed_data['io_signature'] = {
+ "input": {
+ "signature": None
+ },
+ "output": {
+ "signature": None
+ }
+ }
+ self.parsed_data['message_port'] = {
+ "input": [],
+ "output": []
+ }
+ _suppress_input = False
+ _suppress_output = False
+ parsed_io = self.parsed_data['io_signature']
+ message_port = self.parsed_data['message_port']
+ special_comments = []
+ _index = None
+ lines = []
+ with open(self.target_file, 'r') as header:
+ lines = header.readlines()
+ for line in lines:
+ if Constants.BLOCKTOOL in line:
+ _index = lines.index(line)
+
+ if _index is not None:
+ _index = _index+1
+ for num in range(_index, len(lines)):
+ if Constants.END_BLOCKTOOL in lines[num]:
+ break
+ special_comments.append(lines[num])
+ for comment in special_comments:
+ if Constants.INPUT_SIG in comment:
+ parsed_io['input']['signature'] = strip_symbols(comment)
+ if Constants.INPUT_MAX in comment:
+ parsed_io['input']['max_streams'] = strip_symbols(comment)
+ if Constants.INPUT_MIN in comment:
+ parsed_io['input']['min_streams'] = strip_symbols(comment)
+ if parsed_io['input']['signature'] is Constants.MAKE and not None:
+ if Constants.INPUT_MAKE_SIZE in comment:
+ parsed_io['input']['sizeof_stream_item'] = strip_symbols(
+ comment)
+ elif parsed_io['input']['signature'] is Constants.MAKE2 and not None:
+ if Constants.INPUT_MAKE_SIZE1 in comment:
+ parsed_io['input']['sizeof_stream_item1'] = strip_symbols(
+ comment)
+ if Constants.INPUT_MAKE_SIZE2 in comment:
+ parsed_io['input']['sizeof_stream_item2'] = strip_symbols(
+ comment)
+ elif parsed_io['input']['signature'] is Constants.MAKE3 and not None:
+ if Constants.INPUT_MAKE_SIZE1 in comment:
+ parsed_io['input']['sizeof_stream_item1'] = strip_symbols(
+ comment)
+ if Constants.INPUT_MAKE_SIZE2 in comment:
+ parsed_io['input']['sizeof_stream_item2'] = strip_symbols(
+ comment)
+ if Constants.INPUT_MAKE_SIZE3 in comment:
+ parsed_io['input']['sizeof_stream_item3'] = strip_symbols(
+ comment)
+ elif parsed_io['input']['signature'] is Constants.MAKEV and not None:
+ if Constants.INPUT_MAKEV_SIZE in comment:
+ parsed_io['input']['sizeof_stream_items'] = strip_symbols(
+ comment)
+
+ if Constants.OUTPUT_SIG in comment:
+ parsed_io['output']['signature'] = strip_symbols(comment)
+ if Constants.OUTPUT_MAX in comment:
+ parsed_io['output']['max_streams'] = strip_symbols(comment)
+ if Constants.OUTPUT_MIN in comment:
+ parsed_io['output']['min_streams'] = strip_symbols(comment)
+ if parsed_io['output']['signature'] is Constants.MAKE and not None:
+ if Constants.OUTPUT_MAKE_SIZE in comment:
+ parsed_io['output']['sizeof_stream_item'] = strip_symbols(
+ comment)
+ elif parsed_io['output']['signature'] is Constants.MAKE2:
+ if Constants.OUTPUT_MAKE_SIZE1 in comment:
+ parsed_io['output']['sizeof_stream_item1'] = strip_symbols(
+ comment)
+ if Constants.OUTPUT_MAKE_SIZE2 in comment:
+ parsed_io['output']['sizeof_stream_item2'] = strip_symbols(
+ comment)
+ elif parsed_io['output']['signature'] is Constants.MAKE3 and not None:
+ if Constants.OUTPUT_MAKE_SIZE1 in comment:
+ parsed_io['output']['sizeof_stream_item1'] = strip_symbols(
+ comment)
+ if Constants.OUTPUT_MAKE_SIZE2 in comment:
+ parsed_io['output']['sizeof_stream_item2'] = strip_symbols(
+ comment)
+ if Constants.OUTPUT_MAKE_SIZE3 in comment:
+ parsed_io['output']['sizeof_stream_item3'] = strip_symbols(
+ comment)
+ elif parsed_io['output']['signature'] is Constants.MAKEV and not None:
+ if Constants.OUTPUT_MAKEV_SIZE in comment:
+ parsed_io['output']['sizeof_stream_items'] = strip_symbols(
+ comment)
+
+ if Constants.INPUT_PORT in comment:
+ if Constants.EXCLAMATION in comment:
+ _suppress_input = True
+ if strip_symbols(comment):
+ message_port['input'] = strip_symbols(comment).split(', ')
+ if Constants.OUTPUT_PORT in comment:
+ if Constants.EXCLAMATION in comment:
+ _suppress_output = True
+ if strip_symbols(comment):
+ message_port['output'] = strip_symbols(comment).split(', ')
+ validate_message_port(
+ self, temp_parsed_data['message_port'], _suppress_input, _suppress_output)
+ self.parsed_data['io_signature'] = temp_parsed_data['io_signature']
+
+
+def add_comments(self):
+ """
+ function to add special blocktool comments
+ in the public header
+ """
+ _index = None
+ lines = []
+ parsed_io = self.parsed_data['io_signature']
+ message_port = self.parsed_data['message_port']
+ with open(self.target_file, 'r') as header:
+ lines = header.readlines()
+ for line in lines:
+ if Constants.BLOCKTOOL in line:
+ _index = lines.index(line)
+ if _index is None:
+ with open(self.target_file, 'a') as header:
+ header.write('\n')
+ header.write('/* '+Constants.BLOCKTOOL + '\n')
+ header.write('input_signature: ' +
+ parsed_io['input']['signature'] + '\n')
+ header.write('input_min_streams: ' +
+ parsed_io['input']['min_streams'] + '\n')
+ header.write('input_max_streams: ' +
+ parsed_io['input']['max_streams'] + '\n')
+ if parsed_io['input']['signature'] is Constants.MAKE:
+ header.write('input_sizeof_stream_item: ' +
+ parsed_io['input']['sizeof_stream_item'] + '\n')
+ elif parsed_io['input']['signature'] is Constants.MAKE2:
+ header.write('input_sizeof_stream_item1: ' +
+ parsed_io['input']['sizeof_stream_item1'] + '\n')
+ header.write('input_sizeof_stream_item2: ' +
+ parsed_io['input']['sizeof_stream_item2'] + '\n')
+ elif parsed_io['input']['signature'] is Constants.MAKE3:
+ header.write('input_sizeof_stream_item1: ' +
+ parsed_io['input']['sizeof_stream_item1'] + '\n')
+ header.write('input_sizeof_stream_item2: ' +
+ parsed_io['input']['sizeof_stream_item2'] + '\n')
+ header.write('input_sizeof_stream_item3: ' +
+ parsed_io['input']['sizeof_stream_item3'] + '\n')
+ elif parsed_io['input']['signature'] is Constants.MAKEV:
+ header.write('input_sizeof_stream_item: ' +
+ parsed_io['input']['sizeof_stream_items'] + '\n')
+ header.write('output_signature: ' +
+ parsed_io['output']['signature'] + '\n')
+ header.write('output_min_streams: ' +
+ parsed_io['output']['min_streams'] + '\n')
+ header.write('output_max_streams: ' +
+ parsed_io['output']['max_streams'] + '\n')
+ if parsed_io['output']['signature'] is Constants.MAKE:
+ header.write('output_sizeof_stream_item: ' +
+ parsed_io['output']['sizeof_stream_item'] + '\n')
+ elif parsed_io['output']['signature'] is Constants.MAKE2:
+ header.write('output_sizeof_stream_item1: ' +
+ parsed_io['output']['sizeof_stream_item1'] + '\n')
+ header.write('output_sizeof_stream_item2: ' +
+ parsed_io['output']['sizeof_stream_item2'] + '\n')
+ elif parsed_io['output']['signature'] is Constants.MAKE3:
+ header.write('output_sizeof_stream_item1: ' +
+ parsed_io['output']['sizeof_stream_item1'] + '\n')
+ header.write('output_sizeof_stream_item2: ' +
+ parsed_io['output']['sizeof_stream_item2'] + '\n')
+ header.write('output_sizeof_stream_item3: ' +
+ parsed_io['output']['sizeof_stream_item3'] + '\n')
+ elif parsed_io['output']['signature'] is Constants.MAKEV:
+ header.write('output_sizeof_stream_item: ' +
+ parsed_io['output']['sizeof_stream_items'] + '\n')
+
+ if message_port['input']:
+ header.write('message_input: ' +
+ ', '.join(message_port['input']) + '\n')
+ else:
+ header.write('message_input: ' + '\n')
+ if message_port['output']:
+ header.write('message_output: ' +
+ ', '.join(message_port['output']) + '\n')
+ else:
+ header.write('message_output: ' + '\n')
+ header.write(Constants.END_BLOCKTOOL + '*/' + '\n')
diff --git a/gr-utils/python/blocktool/core/iosignature.py b/gr-utils/python/blocktool/core/iosignature.py
new file mode 100644
index 0000000000..cd90eb0a86
--- /dev/null
+++ b/gr-utils/python/blocktool/core/iosignature.py
@@ -0,0 +1,194 @@
+#
+# Copyright 2019 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+""" Module to get io_signature of the header block """
+
+from __future__ import print_function
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import re
+import itertools
+import logging
+import string
+
+from ..core import Constants
+
+LOGGER = logging.getLogger(__name__)
+
+
+def io_signature(impl_file):
+ """
+ function to generate the io_signature of the block
+ : returns the io parmaters
+ """
+ parsed_io = {
+ "input": {
+ "signature": None
+ },
+ "output": {
+ "signature": None
+ }
+ }
+ with open(impl_file, 'r') as impl:
+ io_lines = []
+ for line in impl:
+ if Constants.IO_SIGNATURE in line:
+ io_lines.append(line)
+ if len(io_lines) > 2:
+ io_lines = io_lines[0:2]
+ _io_sig = []
+ for line in io_lines:
+ if Constants.IO_SIGNATURE in line:
+ line = line.lstrip().rstrip().split(Constants.IO_SIGNATURE)
+ _io_sig.append(line)
+ _io_sig = list(itertools.chain.from_iterable(_io_sig))
+ for index, _element in enumerate(_io_sig):
+ _io_sig[index] = _element.lstrip().rstrip()
+ if all(i in string.punctuation for i in _element):
+ _io_sig.remove(_element)
+ _io_sig = list(filter(None, _io_sig))
+ io_func = []
+ for _io in _io_sig:
+ if Constants.MAKE in _io:
+ io_func.append(_io.lstrip().rstrip(Constants.STRIP_SYMBOLS))
+ for signature in Constants.SIGNATURE_LIST:
+ if signature in io_func[0] and parsed_io['input']['signature'] is None:
+ parsed_io['input']['signature'] = signature
+ io_func[0] = io_func[0].lstrip(signature+' (')
+ if signature in io_func[1] and parsed_io['output']['signature'] is None:
+ parsed_io['output']['signature'] = signature
+ io_func[1] = io_func[1].lstrip(signature+' (')
+ io_elements = []
+ for _io in io_func:
+ _io = _io.split(',')
+ io_elements.append(_io)
+ io_elements = list(itertools.chain.from_iterable(io_elements))
+ for index, _io in enumerate(io_elements):
+ _io = _io.lstrip(' (').rstrip(' )')
+ if Constants.OPEN_BRACKET in _io:
+ _io = _io + Constants.CLOSE_BRACKET
+ io_elements[index] = _io
+
+ # Because of any possible combination of I/O signature and different number
+ # of arguments, manual if else loop is required
+ if parsed_io['input']['signature'] is Constants.MAKE:
+ parsed_io['input']['min_streams'] = io_elements[0]
+ parsed_io['input']['max_streams'] = io_elements[1]
+ parsed_io['input']['sizeof_stream_item'] = io_elements[2]
+ del io_elements[0:3]
+ elif parsed_io['input']['signature'] is Constants.MAKE2:
+ parsed_io['input']['min_streams'] = io_elements[0]
+ parsed_io['input']['max_streams'] = io_elements[1]
+ parsed_io['input']['sizeof_stream_item1'] = io_elements[2]
+ parsed_io['input']['sizeof_stream_item2'] = io_elements[3]
+ del io_elements[0:4]
+ elif parsed_io['input']['signature'] is Constants.MAKE3:
+ parsed_io['input']['min_streams'] = io_elements[0]
+ parsed_io['input']['max_streams'] = io_elements[1]
+ parsed_io['input']['sizeof_stream_item1'] = io_elements[2]
+ parsed_io['input']['sizeof_stream_item2'] = io_elements[3]
+ parsed_io['input']['sizeof_stream_item3'] = io_elements[4]
+ del io_elements[0:5]
+ elif parsed_io['input']['signature'] is Constants.MAKEV:
+ parsed_io['input']['min_streams'] = io_elements[0]
+ parsed_io['input']['max_streams'] = io_elements[1]
+ parsed_io['input']['sizeof_stream_items'] = io_elements[2]
+ del io_elements[0:3]
+
+ if parsed_io['output']['signature'] is Constants.MAKE:
+ parsed_io['output']['min_streams'] = io_elements[0]
+ parsed_io['output']['max_streams'] = io_elements[1]
+ parsed_io['output']['sizeof_stream_item'] = io_elements[2]
+ del io_elements[0:3]
+ elif parsed_io['output']['signature'] is Constants.MAKE2:
+ parsed_io['output']['min_streams'] = io_elements[0]
+ parsed_io['output']['max_streams'] = io_elements[1]
+ parsed_io['output']['sizeof_stream_item1'] = io_elements[2]
+ parsed_io['output']['sizeof_stream_item2'] = io_elements[3]
+ del io_elements[0:4]
+ elif parsed_io['output']['signature'] is Constants.MAKE3:
+ parsed_io['output']['min_streams'] = io_elements[0]
+ parsed_io['output']['max_streams'] = io_elements[1]
+ parsed_io['output']['sizeof_stream_item1'] = io_elements[2]
+ parsed_io['output']['sizeof_stream_item2'] = io_elements[3]
+ parsed_io['output']['sizeof_stream_item3'] = io_elements[4]
+ del io_elements[0:5]
+ elif parsed_io['output']['signature'] is Constants.MAKEV:
+ parsed_io['output']['min_streams'] = io_elements[0]
+ parsed_io['output']['max_streams'] = io_elements[1]
+ parsed_io['output']['sizeof_stream_items'] = io_elements[2]
+ del io_elements[0:3]
+ return parsed_io
+
+
+def message_port(impl_file):
+ """
+ parses message ports from implementation file
+ """
+ parsed_message_port = {
+ "input": [],
+ "output": []
+ }
+ with open(impl_file, 'r') as impl:
+ _input = []
+ _output = []
+ for line in impl:
+ if Constants.MESSAGE_INPUT in line:
+ _input.append(line)
+ if Constants.MESSAGE_OUTPUT in line:
+ _output.append(line)
+
+ input_port = []
+ output_port = []
+ if _input:
+ for port in _input:
+ port = port.lstrip().rstrip().strip(Constants.MESSAGE_INPUT)
+ pattern = port.find('\"')
+ if pattern != -1:
+ if re.findall(r'"([^"]*)"', port)[0]:
+ input_port.append(re.findall(r'"([^"]*)"', port)[0])
+ else:
+ input_port.append(port[port.find('(')+1:port.rfind(')')])
+ _temp_port = ''.join(map(str, input_port))
+ input_port.clear()
+ input_port.append(_temp_port)
+
+ if _output:
+ for port in _output:
+ port = port.lstrip().rstrip().strip(Constants.MESSAGE_OUTPUT)
+ pattern = port.find('\"')
+ if pattern != -1:
+ if re.findall(r'"([^"]*)"', port)[0]:
+ output_port.append(re.findall(r'"([^"]*)"', port)[0])
+ else:
+ output_port.append(port[port.find('(')+1:port.rfind(')')])
+ _temp_port = ''.join(map(str, output_port))
+ output_port.clear()
+ output_port.append(_temp_port)
+
+ if input_port:
+ for port in input_port:
+ parsed_message_port['input'].append(port)
+
+ if output_port:
+ for port in output_port:
+ parsed_message_port['output'].append(port)
+ return parsed_message_port
diff --git a/gr-utils/python/blocktool/core/outputschema.py b/gr-utils/python/blocktool/core/outputschema.py
new file mode 100644
index 0000000000..baa9b61042
--- /dev/null
+++ b/gr-utils/python/blocktool/core/outputschema.py
@@ -0,0 +1,169 @@
+#
+# Copyright 2019 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+""" Schema to be strictly followed be parsed header output """
+
+
+RESULT_SCHEMA = {
+ "title": "JSON SCHEMA TO BE FOLLOWED BY BLOCK HEADER PARSING TOOL",
+ "description": "Schema designed for the header file parsed python dict output",
+ "type": "object",
+ "properties": {
+ "namespace": {
+ "description": "List of nested namspace",
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": True,
+ "items": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "class": {
+ "description": "Class name",
+ "type": "string",
+ "minLength": 1
+ },
+ "io_signature": {
+ "description": "I/O signature",
+ "type": "object",
+ "properties": {
+ "input": {
+ "description": "Input ports",
+ "type": "object"
+ },
+ "output": {
+ "description": "Output ports",
+ "type": "object"
+ }
+ },
+ "required": ["input", "output"]
+ },
+ "make": {
+ "description": "Make function",
+ "type": "object",
+ "properties": {
+ "arguments": {
+ "description": "Arguments of make function",
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": True,
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "dtype": {
+ "type": "string",
+ "minLength": 1
+ },
+ "default": {
+ "type": "string"
+ }
+ },
+ "required": ["name"],
+ "dependencies": {
+ "name": [
+ "dtype",
+ "default"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "methods": {
+ "description": "Setters",
+ "type": "array",
+ "minItems": 0,
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "arguments_type": {
+ "type": "array",
+ "uniqueItems": True,
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "dtype": {
+ "type": "string",
+ "minLength": 1
+ }
+ },
+ "required": ["name"],
+ "dependencies": {
+ "name": ["dtype"]
+ }
+ }
+ },
+ "required": ["name"]
+ }
+ },
+ "properties": {
+ "description": "Getters",
+ "type": "array",
+ "uniqueItems": True,
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 1
+ },
+ "dtype": {
+ "type": "string",
+ "minLength": 1
+ },
+ "read_only": {
+ "type": "boolean"
+ }
+ },
+ "required": ["name"],
+ "dependencies": {
+ "name": [
+ "dtype",
+ "read_only"
+ ]
+ }
+ }
+ },
+ "docstring": {
+ "description": "documentation of the header file",
+ "type": "array"
+ }
+ },
+ "required": [
+ "namespace",
+ "class",
+ "io_signature",
+ "make",
+ "methods",
+ "properties",
+ "docstring"
+ ]
+}
diff --git a/gr-utils/python/blocktool/core/parseheader.py b/gr-utils/python/blocktool/core/parseheader.py
new file mode 100644
index 0000000000..7b1e743554
--- /dev/null
+++ b/gr-utils/python/blocktool/core/parseheader.py
@@ -0,0 +1,277 @@
+#
+# Copyright 2019 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+""" Module to generate AST for the headers and parse it """
+
+from __future__ import print_function
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import os
+import re
+import codecs
+import logging
+
+from pygccxml import parser, declarations, utils
+
+from ..core.base import BlockToolException, BlockTool
+from ..core.iosignature import io_signature, message_port
+from ..core.comments import read_comments, add_comments, exist_comments
+from ..core import Constants
+
+LOGGER = logging.getLogger(__name__)
+
+
+class BlockHeaderParser(BlockTool):
+ """
+ : Single argument required: file_path
+ file_path: enter path for the header block in any of GNU Radio module
+ : returns the parsed header data in python dict
+ : return dict keys: namespace, class, io_signature, make,
+ properties, methods
+ : Can be used as an CLI command or an extenal API
+ """
+ name = 'Block Parse Header'
+ description = 'Create a parsed output from a block header file'
+
+ def __init__(self, file_path=None, blocktool_comments=False, **kwargs):
+ """ __init__ """
+ BlockTool.__init__(self, **kwargs)
+ self.parsed_data = {}
+ self.addcomments = blocktool_comments
+ if not os.path.isfile(file_path):
+ raise BlockToolException('file does not exist')
+ file_path = os.path.abspath(file_path)
+ self.target_file = file_path
+ self.initialize()
+ self.validate()
+
+ def initialize(self):
+ """
+ initialize all the required API variables
+ """
+ self.module = self.target_file
+ for dirs in self.module:
+ if not os.path.basename(self.module).startswith(Constants.GR):
+ self.module = os.path.abspath(
+ os.path.join(self.module, os.pardir))
+ self.modname = os.path.basename(self.module)
+ self.filename = os.path.basename(self.target_file)
+ self.targetdir = os.path.dirname(self.target_file)
+ for dirs in os.scandir(self.module):
+ if dirs.is_dir():
+ if dirs.path.endswith('lib'):
+ self.impldir = dirs.path
+ self.impl_file = os.path.join(self.impldir,
+ self.filename.split('.')[0]+'_impl.cc')
+
+ def validate(self):
+ """ Override the Blocktool validate function """
+ BlockTool._validate(self)
+ if not self.filename.endswith('.h'):
+ raise BlockToolException(
+ 'Cannot parse a non-header file')
+
+ def get_header_info(self):
+ """
+ PyGCCXML header code parser
+ magic happens here!
+ : returns the parsed header data in python dict
+ : return dict keys: namespace, class, io_signature, make,
+ properties, methods
+ : Can be used as an CLI command or an extenal API
+ """
+ gr = self.modname.split('-')[0]
+ module = self.modname.split('-')[-1]
+ generator_path, generator_name = utils.find_xml_generator()
+ xml_generator_config = parser.xml_generator_configuration_t(
+ xml_generator_path=generator_path,
+ xml_generator=generator_name,
+ compiler='gcc')
+ decls = parser.parse(
+ [self.target_file], xml_generator_config)
+ global_namespace = declarations.get_global_namespace(decls)
+
+ # namespace
+ try:
+ self.parsed_data['namespace'] = []
+ ns = global_namespace.namespace(gr)
+ if ns is None:
+ raise BlockToolException
+ main_namespace = ns.namespace(module)
+ if main_namespace is None:
+ raise BlockToolException('namespace cannot be none')
+ self.parsed_data['namespace'] = [gr, module]
+ if main_namespace.declarations:
+ for _namespace in main_namespace.declarations:
+ if isinstance(_namespace, declarations.namespace_t):
+ if Constants.KERNEL not in str(_namespace):
+ main_namespace = _namespace
+ self.parsed_data['namespace'].append(
+ str(_namespace).split('::')[-1].split(' ')[0])
+ except RuntimeError:
+ raise BlockToolException(
+ 'Invalid namespace format in the block header file')
+
+ # class
+ try:
+ self.parsed_data['class'] = ''
+ for _class in main_namespace.declarations:
+ if isinstance(_class, declarations.class_t):
+ main_class = _class
+ self.parsed_data['class'] = str(_class).split('::')[
+ 2].split(' ')[0]
+ except RuntimeError:
+ raise BlockToolException(
+ 'Block header namespace {} must consist of a valid class instance'.format(module))
+
+ # io_signature, message_ports
+ self.parsed_data['io_signature'] = {}
+ self.parsed_data['message_port'] = {}
+ if os.path.isfile(self.impl_file) and exist_comments(self):
+ self.parsed_data['io_signature'] = io_signature(
+ self.impl_file)
+ self.parsed_data['message_port'] = message_port(
+ self.impl_file)
+ read_comments(self)
+ elif os.path.isfile(self.impl_file) and not exist_comments(self):
+ self.parsed_data['io_signature'] = io_signature(
+ self.impl_file)
+ self.parsed_data['message_port'] = message_port(
+ self.impl_file)
+ if self.addcomments:
+ add_comments(self)
+ elif not os.path.isfile(self.impl_file) and exist_comments(self):
+ read_comments(self)
+ else:
+ self.parsed_data['io_signature'] = {
+ "input": [],
+ "output": []
+ }
+ self.parsed_data['message_port'] = self.parsed_data['io_signature']
+
+ # make
+ try:
+ self.parsed_data['make'] = {}
+ self.parsed_data['make']['arguments'] = []
+ query_m = declarations.custom_matcher_t(
+ lambda mem_fun: mem_fun.name.startswith('make'))
+ query_make = query_m & declarations.access_type_matcher_t('public')
+ make_func = main_class.member_functions(function=query_make,
+ allow_empty=True,
+ header_file=self.target_file)
+ criteria = declarations.calldef_matcher(name='make')
+ _make_fun = declarations.matcher.get_single(criteria, main_class)
+ _make_fun = str(_make_fun).split(
+ 'make')[-1].split(')')[0].split('(')[1].lstrip().rstrip().split(',')
+ if make_func:
+ for arg in make_func[0].arguments:
+ for _arg in _make_fun:
+ if str(arg.name) in _arg:
+ make_arguments = {
+ "name": str(arg.name),
+ "dtype": str(arg.decl_type),
+ "default": ""
+ }
+ if re.findall(r'[-+]?\d*\.\d+|\d+', _arg):
+ make_arguments['default'] = re.findall(
+ r'[-+]?\d*\.\d+|\d+', _arg)[0]
+ elif re.findall(r'\"(.+?)\"', _arg):
+ make_arguments['default'] = re.findall(
+ r'\"(.+?)\"', _arg)[0]
+ elif "true" in _arg:
+ make_arguments['default'] = "True"
+ elif "false" in _arg:
+ make_arguments['default'] = "False"
+ self.parsed_data['make']['arguments'].append(
+ make_arguments.copy())
+ except RuntimeError:
+ self.parsed_data['make'] = {}
+ self.parsed_data['make']['arguments'] = []
+
+ # setters
+ try:
+ self.parsed_data['methods'] = []
+ query_methods = declarations.access_type_matcher_t('public')
+ setters = main_class.member_functions(function=query_methods,
+ allow_empty=True,
+ header_file=self.target_file)
+ getter_arguments = []
+ if setters:
+ for setter in setters:
+ if str(setter.name).startswith('set_') and setter.arguments:
+ setter_args = {
+ "name": str(setter.name),
+ "arguments_type": []
+ }
+ for argument in setter.arguments:
+ args = {
+ "name": str(argument.name),
+ "dtype": str(argument.decl_type)
+ }
+ getter_arguments.append(args['name'])
+ setter_args['arguments_type'].append(args.copy())
+ self.parsed_data['methods'].append(setter_args.copy())
+ except RuntimeError:
+ self.parsed_data['methods'] = []
+
+ # getters
+ try:
+ self.parsed_data['properties'] = []
+ query_properties = declarations.access_type_matcher_t('public')
+ getters = main_class.member_functions(function=query_properties,
+ allow_empty=True,
+ header_file=self.target_file)
+ if getters:
+ for getter in getters:
+ if not getter.arguments or getter.has_const:
+ getter_args = {
+ "name": str(getter.name),
+ "dtype": str(getter.return_type),
+ "read_only": True
+ }
+ if getter_args['name'] in getter_arguments:
+ getter_args["read_only"] = False
+ self.parsed_data['properties'].append(
+ getter_args.copy())
+ except RuntimeError:
+ self.parsed_data['properties'] = []
+
+ # documentation
+ try:
+ _index = None
+ header_file = codecs.open(self.target_file, 'r', 'cp932')
+ self.parsed_data['docstring'] = re.compile(
+ r'//.*?$|/\*.*?\*/', re.DOTALL | re.MULTILINE).findall(
+ header_file.read())[2:]
+ header_file.close()
+ for doc in self.parsed_data['docstring']:
+ if Constants.BLOCKTOOL in doc:
+ _index = self.parsed_data['docstring'].index(doc)
+ if _index is not None:
+ self.parsed_data['docstring'] = self.parsed_data['docstring'][: _index]
+ except:
+ self.parsed_data['docstring'] = []
+
+ return self.parsed_data
+
+ def run_blocktool(self):
+ """ Run, run, run. """
+ self.get_header_info()
diff --git a/gr-utils/python/blocktool/tests/README.blocktool_test b/gr-utils/python/blocktool/tests/README.blocktool_test
new file mode 100644
index 0000000000..1ca66e9c6e
--- /dev/null
+++ b/gr-utils/python/blocktool/tests/README.blocktool_test
@@ -0,0 +1,12 @@
+gr_blocktool: Block Header parsing tool.
+Parses GNU Radio header files to generate YAML or JSON output.
+
+This directory consists of test for the parsed header output and Blocktool Exceptions.
+============================================================================================
+- Schema defined in the test will be strictly followed for every parsed JSON output file.
+
+Two sample response for header files are available in this directory:
+=====================================================================
+
+- sample_agc2_cc.json for public header file agc2_cc.h from gr-analog directory.
+- sample_additive_scrambler_bb.json for public header file additive_scrambler_bb.h from gr-digital directory. \ No newline at end of file
diff --git a/gr-utils/python/blocktool/tests/sample_json/analog_agc2_cc.json b/gr-utils/python/blocktool/tests/sample_json/analog_agc2_cc.json
new file mode 100644
index 0000000000..fa9eae3551
--- /dev/null
+++ b/gr-utils/python/blocktool/tests/sample_json/analog_agc2_cc.json
@@ -0,0 +1,131 @@
+{
+ "namespace": [
+ "gr",
+ "analog"
+ ],
+ "class": "agc2_cc",
+ "io_signature": {
+ "input": {
+ "signature": "make",
+ "min_streams": "1",
+ "max_streams": "1",
+ "sizeof_stream_item": "sizeof(gr_complex)"
+ },
+ "output": {
+ "signature": "make",
+ "min_streams": "1",
+ "max_streams": "1",
+ "sizeof_stream_item": "sizeof(gr_complex)"
+ }
+ },
+ "message_port": {
+ "input": [],
+ "output": []
+ },
+ "make": {
+ "arguments": [
+ {
+ "name": "attack_rate",
+ "dtype": "float",
+ "default": "0.10000000000000001"
+ },
+ {
+ "name": "decay_rate",
+ "dtype": "float",
+ "default": "0.01"
+ },
+ {
+ "name": "reference",
+ "dtype": "float",
+ "default": "1"
+ },
+ {
+ "name": "gain",
+ "dtype": "float",
+ "default": "1"
+ }
+ ]
+ },
+ "methods": [
+ {
+ "name": "set_attack_rate",
+ "arguments_type": [
+ {
+ "name": "rate",
+ "dtype": "float"
+ }
+ ]
+ },
+ {
+ "name": "set_decay_rate",
+ "arguments_type": [
+ {
+ "name": "rate",
+ "dtype": "float"
+ }
+ ]
+ },
+ {
+ "name": "set_reference",
+ "arguments_type": [
+ {
+ "name": "reference",
+ "dtype": "float"
+ }
+ ]
+ },
+ {
+ "name": "set_gain",
+ "arguments_type": [
+ {
+ "name": "gain",
+ "dtype": "float"
+ }
+ ]
+ },
+ {
+ "name": "set_max_gain",
+ "arguments_type": [
+ {
+ "name": "max_gain",
+ "dtype": "float"
+ }
+ ]
+ }
+ ],
+ "properties": [
+ {
+ "name": "attack_rate",
+ "dtype": "float",
+ "read_only": true
+ },
+ {
+ "name": "decay_rate",
+ "dtype": "float",
+ "read_only": true
+ },
+ {
+ "name": "reference",
+ "dtype": "float",
+ "read_only": false
+ },
+ {
+ "name": "gain",
+ "dtype": "float",
+ "read_only": false
+ },
+ {
+ "name": "max_gain",
+ "dtype": "float",
+ "read_only": false
+ }
+ ],
+ "docstring": [
+ "/*!\n * \\brief high performance Automatic Gain Control class with\n * attack and decay rates.\n * \\ingroup level_controllers_blk\n *\n * \\details\n * For Power the absolute value of the complex number is used.\n */",
+ "// gr::analog::agc2_cc::sptr",
+ "/*!\n * Build a complex value AGC loop block with attack and decay rates.\n *\n * \\param attack_rate the update rate of the loop when in attack mode.\n * \\param decay_rate the update rate of the loop when in decay mode.\n * \\param reference reference value to adjust signal power to.\n * \\param gain initial gain value.\n */",
+ "/* namespace analog */",
+ "/* namespace gr */",
+ "/* INCLUDED_ANALOG_AGC2_CC_H */"
+ ]
+} \ No newline at end of file
diff --git a/gr-utils/python/blocktool/tests/sample_json/digital_additive_scrambler_bb.json b/gr-utils/python/blocktool/tests/sample_json/digital_additive_scrambler_bb.json
new file mode 100644
index 0000000000..265b5a6ddc
--- /dev/null
+++ b/gr-utils/python/blocktool/tests/sample_json/digital_additive_scrambler_bb.json
@@ -0,0 +1,95 @@
+{
+ "namespace": [
+ "gr",
+ "digital"
+ ],
+ "class": "additive_scrambler_bb",
+ "io_signature": {
+ "input": {
+ "signature": "make",
+ "min_streams": "1",
+ "max_streams": "1",
+ "sizeof_stream_item": "sizeof(unsigned char)"
+ },
+ "output": {
+ "signature": "make",
+ "min_streams": "1",
+ "max_streams": "1",
+ "sizeof_stream_item": "sizeof(unsigned char)"
+ }
+ },
+ "message_port": {
+ "input": [],
+ "output": []
+ },
+ "make": {
+ "arguments": [
+ {
+ "name": "mask",
+ "dtype": "int",
+ "default": ""
+ },
+ {
+ "name": "seed",
+ "dtype": "int",
+ "default": ""
+ },
+ {
+ "name": "len",
+ "dtype": "int",
+ "default": ""
+ },
+ {
+ "name": "count",
+ "dtype": "int",
+ "default": "0"
+ },
+ {
+ "name": "bits_per_byte",
+ "dtype": "int",
+ "default": "1"
+ },
+ {
+ "name": "reset_tag_key",
+ "dtype": "std::string const &",
+ "default": ""
+ }
+ ]
+ },
+ "methods": [],
+ "properties": [
+ {
+ "name": "mask",
+ "dtype": "int",
+ "read_only": true
+ },
+ {
+ "name": "seed",
+ "dtype": "int",
+ "read_only": true
+ },
+ {
+ "name": "len",
+ "dtype": "int",
+ "read_only": true
+ },
+ {
+ "name": "count",
+ "dtype": "int",
+ "read_only": true
+ },
+ {
+ "name": "bits_per_byte",
+ "dtype": "int",
+ "read_only": true
+ }
+ ],
+ "docstring": [
+ "/*!\n * \\ingroup coding_blk\n *\n * \\brief\n * Scramble an input stream using an LFSR.\n *\n * \\details\n * This block scrambles up to 8 bits per byte of the input\n * data stream, starting at the LSB.\n *\n * The scrambler works by XORing the incoming bit stream by the\n * output of the LFSR. Optionally, after \\p count bits have been\n * processed, the shift register is reset to the \\p seed value.\n * This allows processing fixed length vectors of samples.\n *\n * Alternatively, the LFSR can be reset using a reset tag to\n * scramble variable length vectors. However, it cannot be reset\n * between bytes.\n *\n * For details on configuring the LFSR, see gr::digital::lfsr.\n */",
+ "// gr::digital::additive_scrambler_bb::sptr",
+ "/*!\n * \\brief Create additive scrambler.\n *\n * \\param mask Polynomial mask for LFSR\n * \\param seed Initial shift register contents\n * \\param len Shift register length\n * \\param count Number of bytes after which shift register is reset, 0=never\n * \\param bits_per_byte Number of bits per byte\n * \\param reset_tag_key When a tag with this key is detected, the shift register is reset (when this is set, count is ignored!)\n */",
+ "/* namespace digital */",
+ "/* namespace gr */",
+ "/* INCLUDED_DIGITAL_ADDITIVE_SCRAMBLER_BB_H */"
+ ]
+} \ No newline at end of file
diff --git a/gr-utils/python/blocktool/tests/sample_yaml/analog_agc2_cc.yml b/gr-utils/python/blocktool/tests/sample_yaml/analog_agc2_cc.yml
new file mode 100644
index 0000000000..fef19eff95
--- /dev/null
+++ b/gr-utils/python/blocktool/tests/sample_yaml/analog_agc2_cc.yml
@@ -0,0 +1,67 @@
+id: analog_agc2_cc
+label: AGC2
+category: '[Analog]'
+flags: '[python, cpp]'
+templates:
+ imports: from gnuradio import analog
+ make: analog.agc2_cc(${attack_rate}, ${decay_rate}, ${reference}, ${gain}, ${max_gain})
+ callbacks: !!python/tuple
+ - set_attack_rate(${rate})
+ - set_decay_rate(${rate})
+ - set_reference(${reference})
+ - set_gain(${gain})
+ - set_max_gain(${max_gain})
+parameters:
+- id: attack_rate
+ label: Attack_rate
+ dtype: float
+ read_only: true
+- id: decay_rate
+ label: Decay_rate
+ dtype: float
+ read_only: true
+- id: reference
+ label: Reference
+ dtype: float
+ read_only: false
+- id: gain
+ label: Gain
+ dtype: float
+ read_only: false
+- id: max_gain
+ label: Max_gain
+ dtype: float
+ read_only: false
+inputs:
+- domain: stream
+ dtype: sizeof(gr_complex)
+outputs:
+- domain: stream
+ dtype: sizeof(gr_complex)
+cpp_templates:
+ includes: '#include <gnuradio/analog/agc2_cc.h>'
+ declartions: analog::agc2_cc::sptr ${id}
+ make: this->${id} = analog::agc2_cc::make(${attack_rate}, ${decay_rate}, ${reference},
+ ${gain}, ${max_gain})
+ callbacks: !!python/tuple
+ - set_attack_rate(${rate})
+ - set_decay_rate(${rate})
+ - set_reference(${reference})
+ - set_gain(${gain})
+ - set_max_gain(${max_gain})
+ link: gnuradio-analog
+documentation:
+- "/*!\n * \\brief high performance Automatic Gain Control class with\n *
+ attack and decay rates.\n * \\ingroup level_controllers_blk\n *\n *
+ \\details\n * For Power the absolute value of the complex number is used.\n
+ \ */"
+- // gr::analog::agc2_cc::sptr
+- "/*!\n * Build a complex value AGC loop block with attack and decay rates.\n
+ \ *\n * \\param attack_rate the update rate of the loop when in attack
+ mode.\n * \\param decay_rate the update rate of the loop when in decay mode.\n
+ \ * \\param reference reference value to adjust signal power to.\n *
+ \\param gain initial gain value.\n */"
+- /* namespace analog */
+- /* namespace gr */
+- /* INCLUDED_ANALOG_AGC2_CC_H */
+file_format: 1
diff --git a/gr-utils/python/blocktool/tests/sample_yaml/digital_additive_scrambler_bb.yml b/gr-utils/python/blocktool/tests/sample_yaml/digital_additive_scrambler_bb.yml
new file mode 100644
index 0000000000..0001653273
--- /dev/null
+++ b/gr-utils/python/blocktool/tests/sample_yaml/digital_additive_scrambler_bb.yml
@@ -0,0 +1,63 @@
+id: digital_additive_scrambler_bb
+label: ADDITIVE SCRAMBLER
+category: '[Digital]'
+flags: '[python, cpp]'
+templates:
+ imports: from gnuradio import digital
+ make: digital.additive_scrambler_bb(${mask}, ${seed}, ${len}, ${count}, ${bits_per_byte})
+parameters:
+- id: mask
+ label: Mask
+ dtype: int
+ read_only: true
+- id: seed
+ label: Seed
+ dtype: int
+ read_only: true
+- id: len
+ label: Len
+ dtype: int
+ read_only: true
+- id: count
+ label: Count
+ dtype: int
+ read_only: true
+- id: bits_per_byte
+ label: Bits_per_byte
+ dtype: int
+ read_only: true
+inputs:
+- domain: stream
+ dtype: sizeof(unsigned char)
+outputs:
+- domain: stream
+ dtype: sizeof(unsigned char)
+cpp_templates:
+ includes: '#include <gnuradio/digital/additive_scrambler_bb.h>'
+ declartions: digital::additive_scrambler_bb::sptr ${id}
+ make: this->${id} = digital::additive_scrambler_bb::make(${mask}, ${seed}, ${len},
+ ${count}, ${bits_per_byte})
+ link: gnuradio-digital
+documentation:
+- "/*!\n * \\ingroup coding_blk\n *\n * \\brief\n * Scramble an input
+ stream using an LFSR.\n *\n * \\details\n * This block scrambles up
+ to 8 bits per byte of the input\n * data stream, starting at the LSB.\n *\n
+ \ * The scrambler works by XORing the incoming bit stream by the\n * output
+ of the LFSR. Optionally, after \\p count bits have been\n * processed, the shift
+ register is reset to the \\p seed value.\n * This allows processing fixed length
+ vectors of samples.\n *\n * Alternatively, the LFSR can be reset using a
+ reset tag to\n * scramble variable length vectors. However, it cannot be reset\n
+ \ * between bytes.\n *\n * For details on configuring the LFSR, see gr::digital::lfsr.\n
+ \ */"
+- // gr::digital::additive_scrambler_bb::sptr
+- "/*!\n * \\brief Create additive scrambler.\n *\n * \\param mask
+ \ Polynomial mask for LFSR\n * \\param seed Initial shift register contents\n
+ \ * \\param len Shift register length\n * \\param count Number of
+ bytes after which shift register is reset, 0=never\n * \\param bits_per_byte
+ Number of bits per byte\n * \\param reset_tag_key When a tag with this key
+ is detected, the shift register is reset (when this is set, count is ignored!)\n
+ \ */"
+- /* namespace digital */
+- /* namespace gr */
+- /* INCLUDED_DIGITAL_ADDITIVE_SCRAMBLER_BB_H */
+file_format: 1
diff --git a/gr-utils/python/blocktool/tests/test_blocktool.py b/gr-utils/python/blocktool/tests/test_blocktool.py
new file mode 100644
index 0000000000..f8dd9798fd
--- /dev/null
+++ b/gr-utils/python/blocktool/tests/test_blocktool.py
@@ -0,0 +1,199 @@
+#
+# Copyright 2019 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+""" unittest for gr-blocktool api """
+
+from __future__ import print_function
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import os
+import unittest
+import warnings
+try:
+ import pygccxml
+ SKIP_BLOCK_TEST = False
+except:
+ SKIP_BLOCK_TEST = True
+
+try:
+ import apt
+ CACHE = apt.cache.Cache()
+ CACHE.open()
+
+ PKG = CACHE['castxml']
+ if PKG.is_installed:
+ SKIP_BLOCK_TEST = False
+ else:
+ SKIP_BLOCK_TEST = True
+except:
+ SKIP_BLOCK_TEST = True
+
+from jsonschema import validate
+
+from blocktool import BlockHeaderParser
+from blocktool.core.base import BlockToolException
+from blocktool.core import Constants
+from blocktool import RESULT_SCHEMA
+
+
+class TestBlocktoolCore(unittest.TestCase):
+ """ The Tests for blocktool core """
+
+ def __init__(self, *args, **kwargs):
+ super(TestBlocktoolCore, self).__init__(*args, **kwargs)
+ self.module = os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '../../../../gr-analog'))
+ self.test_dir = os.path.abspath(os.path.join(self.module,
+ 'include/gnuradio/analog'))
+
+ def is_int(self, number):
+ """
+ Check for int conversion
+ """
+ try:
+ int(number)
+ return True
+ except ValueError:
+ return False
+
+ @classmethod
+ def setUpClass(cls):
+ """ create a temporary Blocktool object """
+ try:
+ warnings.simplefilter("ignore", ResourceWarning)
+ except NameError:
+ pass
+ test_path = {}
+ target_file = os.path.abspath(os.path.join(os.path.dirname(
+ __file__), '../../../../gr-analog/include/gnuradio/analog', 'agc2_cc.h'))
+ test_path['file_path'] = target_file
+ cls.test_obj = BlockHeaderParser(**test_path).get_header_info()
+
+ @unittest.skipIf(SKIP_BLOCK_TEST, 'pygccxml not found, skipping this unittest')
+ def test_blocktool_exceptions(self):
+ """
+ tests for blocktool exceptions
+ """
+ # test for non-existent header or invalid headers
+ test_dict = {}
+ test_dict['file_path'] = os.path.abspath(
+ os.path.join(self.test_dir, 'sample.h'))
+ with self.assertRaises(BlockToolException):
+ BlockHeaderParser(**test_dict).run_blocktool()
+ # test for invalid header file
+ test_dict['file_path'] = os.path.abspath(
+ os.path.join(self.test_dir, 'CMakeLists.txt'))
+ if not os.path.basename(test_dict['file_path']).endswith('.h'):
+ with self.assertRaises(BlockToolException):
+ BlockHeaderParser(**test_dict).run_blocktool()
+
+ @unittest.skipIf(SKIP_BLOCK_TEST, 'pygccxml not found, skipping this unittest')
+ def test_namespace(self):
+ """ test for header namespace """
+ module_name = os.path.basename(self.module)
+ self.assertTrue(self.test_obj['namespace'][0] == 'gr')
+ self.assertTrue(self.test_obj['namespace']
+ [1] == module_name.split('-')[-1])
+
+ @unittest.skipIf(SKIP_BLOCK_TEST, 'pygccxml not found, skipping this unittest')
+ def test_io_signature(self):
+ """ test for io_signature """
+ input_signature = self.test_obj['io_signature']['input']['signature']
+ output_signature = self.test_obj['io_signature']['output']['signature']
+ valid_signature = False
+ if input_signature and output_signature in Constants.SIGNATURE_LIST:
+ valid_signature = True
+ self.assertTrue(valid_signature)
+ valid_io_stream = False
+ input_max = self.test_obj['io_signature']['input']['max_streams']
+ input_min = self.test_obj['io_signature']['input']['min_streams']
+ output_max = self.test_obj['io_signature']['output']['max_streams']
+ output_min = self.test_obj['io_signature']['output']['min_streams']
+ if self.is_int(input_max) and self.is_int(input_min) and self.is_int(output_max) and self.is_int(output_min):
+ valid_io_stream = True
+ self.assertTrue(valid_io_stream)
+
+ @unittest.skipIf(SKIP_BLOCK_TEST, 'pygccxml not found, skipping this unittest')
+ def test_message_port(self):
+ """ test for message ports """
+ input_port = self.test_obj['message_port']['input']
+ output_port = self.test_obj['message_port']['output']
+ valid_input_message_port = True
+ valid_output_message_port = True
+ if input_port:
+ for port in input_port:
+ if not port['id']:
+ valid_input_message_port = False
+ if output_port:
+ for port in output_port:
+ if not port['id']:
+ valid_output_message_port = False
+ self.assertTrue(valid_input_message_port)
+ self.assertTrue(valid_output_message_port)
+
+ @unittest.skipIf(SKIP_BLOCK_TEST, 'pygccxml not found, skipping this unittest')
+ def test_factory_signature(self):
+ """ test for factory signature in the header """
+ valid_factory_arg = True
+ if self.test_obj['make']['arguments']:
+ for arguments in self.test_obj['make']['arguments']:
+ if not arguments['name'] or not arguments['dtype']:
+ valid_factory_arg = False
+ self.assertTrue(valid_factory_arg)
+
+ @unittest.skipIf(SKIP_BLOCK_TEST, 'pygccxml not found, skipping this unittest')
+ def test_methods(self):
+ """ test for methods """
+ valid_method = True
+ if self.test_obj['methods']:
+ for arguments in self.test_obj['methods']:
+ if not arguments['name']:
+ valid_method = False
+ if arguments['arguments_type']:
+ for args in arguments['arguments_type']:
+ if not args['name'] or not args['dtype']:
+ valid_method = False
+ self.assertTrue(valid_method)
+
+ @unittest.skipIf(SKIP_BLOCK_TEST, 'pygccxml not found, skipping this unittest')
+ def test_properties(self):
+ """ test for properties """
+ valid_properties = True
+ if self.test_obj['properties']:
+ for arguments in self.test_obj['properties']:
+ if not arguments['name'] or not arguments['dtype']:
+ valid_properties = False
+ self.assertTrue(valid_properties)
+
+ @unittest.skipIf(SKIP_BLOCK_TEST, 'pygccxml not found, skipping this unittest')
+ def test_result_format(self):
+ """ test for parsed blocktool output format """
+ valid_schema = False
+ try:
+ validate(instance=self.test_obj, schema=RESULT_SCHEMA)
+ valid_schema = True
+ except BlockToolException:
+ raise BlockToolException
+ self.assertTrue(valid_schema)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/gr-utils/python/blocktool/tests/test_json_file.py b/gr-utils/python/blocktool/tests/test_json_file.py
new file mode 100644
index 0000000000..1d79aa78ca
--- /dev/null
+++ b/gr-utils/python/blocktool/tests/test_json_file.py
@@ -0,0 +1,53 @@
+#
+# Copyright 2019 Free Software Foundation, Inc.
+#
+# This file is part of GNU Radio
+#
+# GNU Radio is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GNU Radio is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GNU Radio; see the file COPYING. If not, write to
+# the Free Software Foundation, Inc., 51 Franklin Street,
+# Boston, MA 02110-1301, USA.
+#
+""" testing the JSON files generated by gr-blocktool """
+
+from __future__ import print_function
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import sys
+import json
+import jsonschema
+
+from blocktool import RESULT_SCHEMA
+
+
+def is_valid():
+ """ Validate json file """
+
+ with open(sys.argv[1], 'r') as json_file:
+ data = json.load(json_file)
+ try:
+ print("Validating...")
+ jsonschema.validate(data, RESULT_SCHEMA)
+ except jsonschema.ValidationError as exception:
+ print("Record JSON file # {}: NOT OK".format(sys.argv[1]))
+ raise Exception(exception)
+ else:
+ print("Record JSON file # {}: OK".format(sys.argv[1]))
+
+
+if __name__ == '__main__':
+ if len(sys.argv) == 2:
+ is_valid()
+ else:
+ raise Exception('Please input only one json file')
diff --git a/gr-utils/python/modtool/cli/makeyaml.py b/gr-utils/python/modtool/cli/makeyaml.py
index 621d444958..8e38f9a3a0 100644
--- a/gr-utils/python/modtool/cli/makeyaml.py
+++ b/gr-utils/python/modtool/cli/makeyaml.py
@@ -24,14 +24,27 @@ from __future__ import print_function
from __future__ import absolute_import
from __future__ import unicode_literals
+import os
import click
-from ..core import get_block_candidates, ModToolMakeYAML
+try:
+ from gnuradio.blocktool import BlockHeaderParser
+ from gnuradio.blocktool.core.base import BlockToolException
+except ImportError:
+ have_blocktool = False
+else:
+ have_blocktool = True
+
+from ..core import get_block_candidates, ModToolMakeYAML, yaml_generator
from ..tools import SequenceCompleter
from .base import common_params, block_name, run, cli_input
@click.command('makeyaml', short_help=ModToolMakeYAML.description)
+@click.option('-b', '--blocktool', is_flag=True,
+ help='Use blocktool support to print yaml output. FILE PATH mandatory if used.')
+@click.option('-o', '--output', is_flag=True,
+ help='If given, a file with desired output format will be generated')
@common_params
@block_name
def cli(**kwargs):
@@ -42,10 +55,24 @@ def cli(**kwargs):
Note: This does not work on python blocks
"""
kwargs['cli'] = True
- self = ModToolMakeYAML(**kwargs)
- click.secho("GNU Radio module name identified: " + self.info['modname'], fg='green')
- get_pattern(self)
- run(self)
+ if kwargs['blocktool']:
+ kwargs['modtool'] = True
+ if kwargs['blockname'] is None:
+ raise BlockToolException('Missing argument FILE PATH with blocktool flag')
+ kwargs['file_path'] = os.path.abspath(kwargs['blockname'])
+ if os.path.isfile(kwargs['file_path']):
+ parse_yml = BlockHeaderParser(**kwargs)
+ parse_yml.run_blocktool()
+ parse_yml.cli = True
+ parse_yml.yaml = True
+ yaml_generator(parse_yml, **kwargs)
+ else:
+ raise BlockToolException('Invalid file path.')
+ else:
+ self = ModToolMakeYAML(**kwargs)
+ click.secho("GNU Radio module name identified: " + self.info['modname'], fg='green')
+ get_pattern(self)
+ run(self)
def get_pattern(self):
""" Get the regex pattern for block(s) to be parsed """
diff --git a/gr-utils/python/modtool/core/__init__.py b/gr-utils/python/modtool/core/__init__.py
index eb9386cdf6..d895e88d12 100644
--- a/gr-utils/python/modtool/core/__init__.py
+++ b/gr-utils/python/modtool/core/__init__.py
@@ -27,7 +27,7 @@ from .base import ModTool, ModToolException, get_block_candidates
from .add import ModToolAdd
from .disable import ModToolDisable
from .info import ModToolInfo
-from .makeyaml import ModToolMakeYAML
+from .makeyaml import ModToolMakeYAML, yaml_generator
from .newmod import ModToolNewModule
from .rm import ModToolRemove
from .rename import ModToolRename
diff --git a/gr-utils/python/modtool/core/add.py b/gr-utils/python/modtool/core/add.py
index e74509f930..2ec57f4d2a 100644
--- a/gr-utils/python/modtool/core/add.py
+++ b/gr-utils/python/modtool/core/add.py
@@ -161,7 +161,7 @@ class ModToolAdd(ModTool):
return
try:
append_re_line_sequence(self._file['cmlib'],
- 'list\(APPEND test_{}_sources.*\n'.format(self.info['modname']),
+ r'list\(APPEND test_{}_sources.*\n'.format(self.info['modname']),
'qa_{}.cc'.format(self.info['blockname']))
append_re_line_sequence(self._file['qalib'],
'#include.*\n',
@@ -183,7 +183,7 @@ class ModToolAdd(ModTool):
return
try:
append_re_line_sequence(self._file['cmlib'],
- 'list\(APPEND test_{}_sources.*\n'.format(self.info['modname']),
+ r'list\(APPEND test_{}_sources.*\n'.format(self.info['modname']),
'qa_{}.cc'.format(self.info['blockname']))
self.scm.mark_files_updated((self._file['cmlib'],))
except IOError:
@@ -255,7 +255,7 @@ class ModToolAdd(ModTool):
if re.search('#include', oldfile):
append_re_line_sequence(self._file['swig'], '^#include.*\n', include_str)
else: # I.e., if the swig file is empty
- regexp = re.compile('^%\{\n', re.MULTILINE)
+ regexp = re.compile(r'^%\{\n', re.MULTILINE)
oldfile = regexp.sub('%%{\n%s\n' % include_str, oldfile, count=1)
with open(self._file['swig'], 'w') as f:
f.write(oldfile)
diff --git a/gr-utils/python/modtool/core/base.py b/gr-utils/python/modtool/core/base.py
index b9009f4891..55b6f48076 100644
--- a/gr-utils/python/modtool/core/base.py
+++ b/gr-utils/python/modtool/core/base.py
@@ -198,7 +198,7 @@ class ModTool(object):
for f in files:
if os.path.isfile(f) and f == 'CMakeLists.txt':
with open(f) as filetext:
- if re.search('find_package\(Gnuradio', filetext.read()) is not None:
+ if re.search(r'find_package\(Gnuradio', filetext.read()) is not None:
self.info['version'] = '36' # Might be 37, check that later
has_makefile = True
elif re.search('GR_REGISTER_COMPONENT', filetext.read()) is not None:
diff --git a/gr-utils/python/modtool/core/disable.py b/gr-utils/python/modtool/core/disable.py
index 1fb8d4a830..034e028724 100644
--- a/gr-utils/python/modtool/core/disable.py
+++ b/gr-utils/python/modtool/core/disable.py
@@ -95,7 +95,7 @@ class ModToolDisable(ModTool):
as well as the block magic """
with open(self._file['swig']) as f:
swigfile = f.read()
- (swigfile, nsubs) = re.subn('(.include\s+"({}/)?{}")'.format(
+ (swigfile, nsubs) = re.subn(r'(.include\s+"({}/)?{}")'.format(
self.info['modname'], fname),
r'//\1', swigfile)
if nsubs > 0:
diff --git a/gr-utils/python/modtool/core/makeyaml.py b/gr-utils/python/modtool/core/makeyaml.py
index 05903238b6..dc506e64ce 100644
--- a/gr-utils/python/modtool/core/makeyaml.py
+++ b/gr-utils/python/modtool/core/makeyaml.py
@@ -28,12 +28,45 @@ import os
import re
import glob
import logging
+import yaml
+
+from collections import OrderedDict
+
+try:
+ from yaml import CLoader as Loader, CDumper as Dumper
+except:
+ from yaml import Loader, Dumper
+
+try:
+ from gnuradio.blocktool.core import Constants
+except ImportError:
+ have_blocktool = False
+else:
+ have_blocktool = True
from ..tools import ParserCCBlock, CMakeFileEditor, ask_yes_no, GRCYAMLGenerator
from .base import ModTool, ModToolException
+
logger = logging.getLogger(__name__)
+## setup dumper for dumping OrderedDict ##
+_MAPPING_TAG = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG
+
+
+def dict_representer(dumper, data):
+ """ Representer to represent special OrderedDict """
+ return dumper.represent_dict(data.items())
+
+
+def dict_constructor(loader, node):
+ """ Construct an OrderedDict for dumping """
+ return OrderedDict(loader.construct_pairs(node))
+
+
+Dumper.add_representer(OrderedDict, dict_representer)
+Loader.add_constructor(_MAPPING_TAG, dict_constructor)
+
class ModToolMakeYAML(ModTool):
""" Make YAML file for GRC block bindings """
@@ -55,7 +88,8 @@ class ModToolMakeYAML(ModTool):
# This portion will be covered by the CLI
if not self.cli:
self.validate()
- logger.warning("Warning: This is an experimental feature. Don't expect any magic.")
+ logger.warning(
+ "Warning: This is an experimental feature. Don't expect any magic.")
# 1) Go through lib/
if not self.skip_subdirs['lib']:
if self.info['version'] in ('37', '38'):
@@ -106,10 +140,10 @@ class ModToolMakeYAML(ModTool):
file_exists = True
logger.warning("Warning: Overwriting existing GRC file.")
grc_generator = GRCYAMLGenerator(
- modname=self.info['modname'],
- blockname=blockname,
- params=params,
- iosig=iosig
+ modname=self.info['modname'],
+ blockname=blockname,
+ params=params,
+ iosig=iosig
)
grc_generator.save(path_to_yml)
if file_exists:
@@ -120,7 +154,8 @@ class ModToolMakeYAML(ModTool):
ed = CMakeFileEditor(self._file['cmgrc'])
if re.search(fname_yml, ed.cfile) is None and not ed.check_for_glob('*.yml'):
logger.info("Adding GRC bindings to grc/CMakeLists.txt...")
- ed.append_value('install', fname_yml, to_ignore_end='DESTINATION[^()]+')
+ ed.append_value('install', fname_yml,
+ to_ignore_end='DESTINATION[^()]+')
ed.write()
self.scm.mark_files_updated(self._file['cmgrc'])
@@ -138,18 +173,21 @@ class ModToolMakeYAML(ModTool):
'std::vector<int>': 'int_vector',
'std::vector<float>': 'real_vector',
'std::vector<gr_complex>': 'complex_vector',
- }
+ }
if p_type in ('int',) and default_v is not None and len(default_v) > 1 and default_v[:2].lower() == '0x':
return 'hex'
try:
return translate_dict[p_type]
except KeyError:
return 'raw'
+
def _get_blockdata(fname_cc):
""" Return the block name and the header file name from the .cc file name """
- blockname = os.path.splitext(os.path.basename(fname_cc.replace('_impl.', '.')))[0]
+ blockname = os.path.splitext(os.path.basename(
+ fname_cc.replace('_impl.', '.')))[0]
fname_h = (blockname + '.h').replace('_impl.', '.')
- contains_modulename = blockname.startswith(self.info['modname']+'_')
+ contains_modulename = blockname.startswith(
+ self.info['modname']+'_')
blockname = blockname.replace(self.info['modname']+'_', '', 1)
return (blockname, fname_h, contains_modulename)
# Go, go, go
@@ -157,15 +195,171 @@ class ModToolMakeYAML(ModTool):
(blockname, fname_h, contains_modulename) = _get_blockdata(fname_cc)
try:
parser = ParserCCBlock(fname_cc,
- os.path.join(self.info['includedir'], fname_h),
+ os.path.join(
+ self.info['includedir'], fname_h),
blockname,
self.info['version'],
_type_translate
- )
+ )
except IOError:
- raise ModToolException("Can't open some of the files necessary to parse {}.".format(fname_cc))
+ raise ModToolException(
+ "Can't open some of the files necessary to parse {}.".format(fname_cc))
if contains_modulename:
return (parser.read_params(), parser.read_io_signature(), self.info['modname']+'_'+blockname)
else:
return (parser.read_params(), parser.read_io_signature(), blockname)
+
+
+def yaml_generator(self, **kwargs):
+ """
+ Generate YAML file from the block header file using blocktool API
+ """
+ header = self.filename.split('.')[0]
+ block = self.modname.split('-')[-1]
+ label = header.split('_')
+ del label[-1]
+ yml_file = os.path.join('.', block+'_'+header+'.block.yml')
+ _header = (('id', '{}_{}'.format(block, header)),
+ ('label', ' '.join(label).upper()),
+ ('category', '[{}]'.format(block.capitalize())),
+ ('flags', '[python, cpp]')
+ )
+ params_list = [
+ '${'+s['name']+'}' for s in self.parsed_data['properties'] if self.parsed_data['properties']]
+ _templates = [('imports', 'from gnuradio import {}'.format(block)),
+ ('make', '{}.{}({})'.format(block, header, ', '.join(params_list)))
+ ]
+
+ if self.parsed_data['methods']:
+ list_callbacks = []
+ for param in self.parsed_data['methods']:
+ arguments = []
+ for args in param['arguments_type']:
+ arguments.append(args['name'])
+ arg_list = ['${'+s+'}' for s in arguments if arguments]
+ list_callbacks.append(
+ param['name']+'({})'.format(', '.join(arg_list)))
+ callback_key = ('callbacks')
+ callbacks = (callback_key, tuple(list_callbacks))
+ _templates.append(callbacks)
+ _templates = tuple(_templates)
+
+ data = OrderedDict()
+ for tag, value in _header:
+ data[tag] = value
+
+ templates = OrderedDict()
+ for tag, value in _templates:
+ templates[tag] = value
+ data['templates'] = templates
+
+ parameters = []
+ for param in self.parsed_data['properties']:
+ parameter = OrderedDict()
+ parameter['id'] = param['name']
+ parameter['label'] = param['name'].capitalize()
+ parameter['dtype'] = param['dtype']
+ parameter['read_only'] = param['read_only']
+ parameters.append(parameter)
+ if parameters:
+ data['parameters'] = parameters
+
+ input_signature = []
+ max_input_port = self.parsed_data['io_signature']['input']['max_streams']
+ i_sig = self.parsed_data['io_signature']['input']['signature']
+ for port in range(0, int(max_input_port)):
+ input_sig = OrderedDict()
+ if i_sig is Constants.MAKE:
+ input_sig['domain'] = 'stream'
+ input_sig['dtype'] = self.parsed_data['io_signature']['input']['sizeof_stream_item']
+ elif i_sig is Constants.MAKE2:
+ input_sig['domain'] = 'stream'
+ input_sig['dtype'] = self.parsed_data['io_signature']['input']['sizeof_stream_item' +
+ str(port+1)]
+ elif i_sig is Constants.MAKE3:
+ input_sig['domain'] = 'stream'
+ input_sig['dtype'] = self.parsed_data['io_signature']['input']['sizeof_stream_item' +
+ str(port+1)]
+ elif i_sig is Constants.MAKEV:
+ input_sig['domain'] = 'stream'
+ input_sig['dtype'] = self.parsed_data['io_signature']['input']['sizeof_stream_items']
+ input_signature.append(input_sig)
+
+ if self.parsed_data['message_port']['input']:
+ for _input in self.parsed_data['message_port']['input']:
+ m_input_sig = OrderedDict()
+ m_input_sig['domain'] = 'message'
+ m_input_sig['id'] = _input
+ input_signature.append(m_input_sig)
+ if input_signature:
+ data['inputs'] = input_signature
+
+ output_signature = []
+ max_output_port = self.parsed_data['io_signature']['output']['max_streams']
+ o_sig = self.parsed_data['io_signature']['output']['signature']
+ for port in range(0, int(max_output_port)):
+ output_sig = OrderedDict()
+ if o_sig is Constants.MAKE:
+ output_sig['domain'] = 'stream'
+ output_sig['dtype'] = self.parsed_data['io_signature']['output']['sizeof_stream_item']
+ elif o_sig is Constants.MAKE2:
+ output_sig['domain'] = 'stream'
+ output_sig['dtype'] = self.parsed_data['io_signature']['output']['sizeof_stream_item' +
+ str(port+1)]
+ elif o_sig is Constants.MAKE3:
+ output_sig['domain'] = 'stream'
+ output_sig['dtype'] = self.parsed_data['io_signature']['output']['sizeof_stream_item' +
+ str(port+1)]
+ elif o_sig is Constants.MAKEV:
+ output_sig['domain'] = 'stream'
+ output_sig['dtype'] = self.parsed_data['io_signature']['output']['sizeof_stream_items']
+ output_signature.append(output_sig)
+
+ if self.parsed_data['message_port']['output']:
+ for _output in self.parsed_data['message_port']['output']:
+ m_output_sig = OrderedDict()
+ m_output_sig['domain'] = 'message'
+ m_output_sig['id'] = _output
+ output_signature.append(m_output_sig)
+ if output_signature:
+ data['outputs'] = output_signature
+
+ _cpp_templates = [('includes', '#include <gnuradio/{}/{}>'.format(block, self.filename)),
+ ('declarations', '{}::{}::sptr ${{id}}'.format(block, header)),
+ ('make', 'this->${{id}} = {}::{}::make({})'.format(
+ block, header, ', '.join(params_list)))
+ ]
+
+ if self.parsed_data['methods']:
+ list_callbacks = []
+ for param in self.parsed_data['methods']:
+ arguments = []
+ for args in param['arguments_type']:
+ arguments.append(args['name'])
+ arg_list = ['${'+s+'}' for s in arguments if arguments]
+ list_callbacks.append(
+ param['name']+'({})'.format(', '.join(arg_list)))
+ callback_key = ('callbacks')
+ callbacks = (callback_key, tuple(list_callbacks))
+ _cpp_templates.append(callbacks)
+
+ link = ('link', 'gnuradio-{}'.format(block))
+ _cpp_templates.append(link)
+ _cpp_templates = tuple(_cpp_templates)
+
+ cpp_templates = OrderedDict()
+ for tag, value in _cpp_templates:
+ cpp_templates[tag] = value
+ data['cpp_templates'] = cpp_templates
+
+ if self.parsed_data['docstring'] is not None:
+ data['documentation'] = self.parsed_data['docstring']
+ data['file_format'] = 1
+
+ if kwargs['output']:
+ with open(yml_file, 'w') as yml:
+ yaml.dump(data, yml, Dumper=Dumper, default_flow_style=False)
+ else:
+ print(yaml.dump(data, Dumper=Dumper, allow_unicode=True,
+ default_flow_style=False, indent=4))
diff --git a/gr-utils/python/modtool/templates/templates.py b/gr-utils/python/modtool/templates/templates.py
index f811dc610a..f272791ae4 100644
--- a/gr-utils/python/modtool/templates/templates.py
+++ b/gr-utils/python/modtool/templates/templates.py
@@ -557,7 +557,7 @@ templates:
# Make one 'parameters' list entry for every parameter you want settable from the GUI.
# Keys include:
-# * id (makes the value accessible as \$keyname, e.g. in the make entry)
+# * id (makes the value accessible as keyname, e.g. in the make entry)
# * label (label shown in the GUI)
# * dtype (e.g. int, float, complex, byte, short, xxx_vector, ...)
parameters: