diff --git a/docs/integration.rst b/docs/integration.rst index ebab1d4..a56c4aa 100644 --- a/docs/integration.rst +++ b/docs/integration.rst @@ -32,11 +32,15 @@ very first version of this example):: # Generate input file name set(infile "${CMAKE_CURRENT_SOURCE_DIR}/${infileName}") + # Create the dependency file + set(depfile "${CMAKE_CURRENT_BINARY_DIR}/${outfileName}.d") + # Custom command to do the processing add_custom_command( OUTPUT "${outfile}" - COMMAND fypp "${infile}" "${outfile}" + COMMAND fypp "${infile}" "${outfile}" --depfile "${depfile}" MAIN_DEPENDENCY "${infile}" + DEPFILE "${depfile}" VERBATIM) # Finally add output file to a list diff --git a/src/fypp.py b/src/fypp.py index 9118534..e15e7c8 100755 --- a/src/fypp.py +++ b/src/fypp.py @@ -237,6 +237,18 @@ def __init__(self, includedirs=None, encoding='utf-8'): # Directory of current file self._curdir = None + # All files that have been included + self._included_files = [] + + + def get_dependencies(self): + '''Returns the list of files included during parsing. + + Returns: + list of str: List of included file paths. + ''' + return self._included_files + def parsefile(self, fobj): '''Parses file or a file like object. @@ -244,6 +256,7 @@ def parsefile(self, fobj): Args: fobj (str or file): Name of a file or a file like object. ''' + self._included_files = [] if isinstance(fobj, pathlib.Path): fobj = str(fobj) @@ -259,6 +272,9 @@ def parsefile(self, fobj): def _includefile(self, span, fobj, fname, curdir): + # Don't add the root file, only later includes + if self._curfile: + self._included_files.append(fname) oldfile = self._curfile olddir = self._curdir self._curfile = fname @@ -274,6 +290,7 @@ def parse(self, txt): Args: txt (str): Text to parse. ''' + self._included_files = [] self._curfile = STRING self._curdir = '' self._parse_txt(None, self._curfile, txt) @@ -2420,6 +2437,31 @@ def process_text(self, txt): return self._render() + def get_dependencies(self): + '''Returns the list of included file dependencies. + + Returns: + list of str: List of included file paths. + ''' + return self._parser.get_dependencies() + + + def write_dependencies(self, outfile, depfile): + '''Writes a Make-compatible dependency file. + + Args: + outfile (str): Name of the output file (the target in the dep rule). + depfile (str): Path where the dependency file should be written. + ''' + def quote(text): + # Normalize to forward slashes for Make compatibility + text = text.replace('\\', '/') + return text.replace('$', '$$').replace(' ', '\\ ').replace('#', '\\#') + dependencies = [quote(d) for d in self.get_dependencies()] + with open(depfile, 'w', encoding='utf-8') as fobj: + fobj.write('{}: {}'.format(quote(outfile), ' '.join(dependencies))) + + def _render(self): output = self._renderer.render(self._builder.tree) self._builder.reset() @@ -2555,6 +2597,7 @@ def __init__(self, options=None, evaluator_factory=Evaluator, else: raise FyppFatalError('renderer_factory has incorrect signature') self._preprocessor = Processor(parser, builder, renderer) + self._depfile = getattr(options, 'depfile', None) def process_file(self, infile, outfile=None): @@ -2566,7 +2609,6 @@ def process_file(self, infile, outfile=None): outfile (str, optional): Name of the file to write the result to. If its value is '-', result is written to stdout. If not present, result will be returned as string. - env (dict, optional): Additional definitions for the evaluator. Returns: str: Result of processed input, if no outfile was specified. @@ -2576,13 +2618,15 @@ def process_file(self, infile, outfile=None): if outfile is None: return output if outfile == '-': - outfile = sys.stdout + outfile_handle = sys.stdout else: - outfile = _open_output_file(outfile, self._encoding, - self._create_parent_folder) - outfile.write(output) - if outfile != sys.stdout: - outfile.close() + outfile_handle = _open_output_file(outfile, self._encoding, + self._create_parent_folder) + outfile_handle.write(output) + if outfile_handle != sys.stdout: + outfile_handle.close() + if self._depfile and outfile and outfile != '-': + self.write_dependencies(outfile, self._depfile) return None @@ -2591,7 +2635,6 @@ def process_text(self, txt): Args: txt (str): String to process. - env (dict, optional): Additional definitions for the evaluator. Returns: str: Processed content. @@ -2599,6 +2642,25 @@ def process_text(self, txt): return self._preprocessor.process_text(txt) + def get_dependencies(self): + '''Returns the list of included file dependencies. + + Returns: + list of str: List of included file paths. + ''' + return self._preprocessor.get_dependencies() + + + def write_dependencies(self, outfile, depfile): + '''Writes a Make-compatible dependency file. + + Args: + outfile (str): Name of the output file (the target in the dep rule). + depfile (str): Path where the dependency file should be written. + ''' + self._preprocessor.write_dependencies(outfile, depfile) + + @staticmethod def _apply_definitions(defines, evaluator, evaluate): for define in defines: @@ -2681,6 +2743,8 @@ class FyppOptions(optparse.Values): setting. create_parent_folder (bool): Whether the parent folder for the output file should be created if it does not exist. Default: False. + depfile (str | None): If set, where to write a Makefile compatible + dependency file. Default: None. ''' def __init__(self): @@ -2703,6 +2767,7 @@ def __init__(self): self.encoding = 'utf-8' self.create_parent_folder = False self.file_var_root = None + self.depfile = None class FortranLineFolder: @@ -2946,6 +3011,10 @@ def get_option_parser(): parser.add_option('--file-var-root', metavar='DIR', dest='file_var_root', default=defs.file_var_root, help=msg) + msg = 'Write a Make-compatible dependency file to this location' + parser.add_option('--depfile', metavar='DEPFILE', dest='depfile', + default=defs.depfile, help=msg) + return parser @@ -2956,6 +3025,9 @@ def run_fypp(): opts, leftover = optparser.parse_args(values=options) infile = leftover[0] if len(leftover) > 0 else '-' outfile = leftover[1] if len(leftover) > 1 else '-' + if outfile == '-' and opts.depfile: + raise optparse.OptionValueError( + "--depfile cannot be used when writing to stdout") try: tool = Fypp(opts) tool.process_file(infile, outfile) diff --git a/test/include/escaped_includes.inc b/test/include/escaped_includes.inc new file mode 100644 index 0000000..7a2b63c --- /dev/null +++ b/test/include/escaped_includes.inc @@ -0,0 +1 @@ +#:include 'need$ #escape.inc' diff --git a/test/include/multi_includes.inc b/test/include/multi_includes.inc new file mode 100644 index 0000000..7ff8ea0 --- /dev/null +++ b/test/include/multi_includes.inc @@ -0,0 +1,2 @@ +#:include 'fypp1.inc' +#:include 'fypp2.inc' diff --git a/test/include/subfolder/need$ #escape.inc b/test/include/subfolder/need$ #escape.inc new file mode 100644 index 0000000..d0128e8 --- /dev/null +++ b/test/include/subfolder/need$ #escape.inc @@ -0,0 +1 @@ +#:include 'fypp2.inc' diff --git a/test/runtests.sh b/test/runtests.sh index 422e171..9f62c6a 100755 --- a/test/runtests.sh +++ b/test/runtests.sh @@ -6,11 +6,6 @@ else pythons="python3" fi root=".." -if [ -z "$PYTHONPATH" ]; then - export PYTHONPATH="$root/src" -else - export PYTHONPATH="$root/src:$PYTHONPATH" -fi cd $testdir failed="0" failing_pythons="" diff --git a/test/test_fypp.py b/test/test_fypp.py index 6e7a741..3986846 100644 --- a/test/test_fypp.py +++ b/test/test_fypp.py @@ -1,7 +1,14 @@ '''Unit tests for testing Fypp.''' from pathlib import Path +import os import platform +import sys +import tempfile import unittest + +# Allow for importing fypp +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + import fypp @@ -3056,6 +3063,27 @@ def _importmodule(module): ] +DEPFILE_TESTS = [ + ('basic', + ([_incdir('include')], + 'include/subfolder/include_fypp1.inc', + '{output}: include/fypp1.inc', + ) + ), + ('multiple_includes', + ([_incdir('include/subfolder')], + 'include/multi_includes.inc', + '{output}: include/fypp1.inc include/subfolder/fypp2.inc', + ) + ), + ('escapes', + ([_incdir('include'), _incdir('include/subfolder')], + 'include/escaped_includes.inc', + '{output}: include/subfolder/need$$\\ \\#escape.inc include/subfolder/fypp2.inc', + ) + ), +] + def _get_test_output_method(args, inp, out): '''Returns a test method for checking correctness of Fypp output. @@ -3102,6 +3130,33 @@ def test_output_from_file_input(self): return test_output_from_file_input +def _get_test_depfile_method(args, inputfile, expected): + '''Returns a test method for checking correctness of depfile. + + Args: + args (list of str): Command-line arguments to pass to Fypp. + inputfile (str): Input file with Fypp directives. + expected (str): Expected depfile content (with {output} placeholder). + + Returns: + method: Method to test equality of depfile with result delivered by Fypp. + ''' + + def test_depfile(self): + '''Tests whether Fypp result matches expected output when input is in a file.''' + output = self._get_tempfile() + depfile = self._get_tempfile() + optparser = fypp.get_option_parser() + options, leftover = optparser.parse_args(args + ['--depfile', depfile]) + self.assertEqual(len(leftover), 0) + tool = fypp.Fypp(options) + tool.process_file(inputfile, output) + with open(depfile, 'r', encoding='utf-8') as f: + got = f.read().strip() + self.assertEqual(got, expected.format(output=output.replace('\\', '/'))) + return test_depfile + + def _get_test_exception_method(args, inp, exceptions): '''Returns a test method for checking correctness of thrown exception. @@ -3199,6 +3254,14 @@ class ExceptionTest(_TestContainer): pass class ImportTest(_TestContainer): pass ImportTest.add_test_methods(IMPORT_TESTS, _get_test_output_method) +class DepfileTest(_TestContainer): + def _get_tempfile(self): + _fd, output = tempfile.mkstemp() + os.close(_fd) + self.addCleanup(os.unlink, output) + return output +DepfileTest.add_test_methods(DEPFILE_TESTS, _get_test_depfile_method) + if __name__ == '__main__': unittest.main()