File: fypp_preprocessor.py

package info (click to toggle)
fypp 3.1-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 420 kB
  • sloc: python: 5,631; sh: 37; makefile: 16
file content (220 lines) | stat: -rw-r--r-- 6,442 bytes parent folder | download | duplicates (6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
#!/usr/bin/env python3
# encoding: utf-8
# Bálint Aradi, 2016-2021

'''General module for using Fypp as preprocessor.

This module implements the general framework for the Fypp preprocessor, but does
not bind it to any task generator. If you want to use it to preprocessor Fortran
source files (.fpp -> .f90), use the fypp_fortran module instead. Otherwise,
you can generate your own binding as ususal::

	def build(bld):
		bld(features='fypp', source=['test.fypp'])

	from waflib import TaskGen
	@TaskGen.extension('.fypp')
	def process(self, node):
    		tsk = self.create_task('fypp_preprocessor', [node], node.change_ext('.out'))

The preprocessor understands the following uselib attributes:

* ``includes``: Directory/directories to search for include files
* ``modules``: Python module(s) to import before preprocessing starts
* ``defines``: Definition(s) to apply before preprocessing starts
* ``inifiles``: Python file(s) to execute before preprosessing starts

The example below demonstrates this::

	def build(bld):
		bld(features='fypp',
			source=['trash.fypp'],
			includes='include',
			modules=['myfypp1', 'myfypp2'],
			defines='TEST=1 QUIET',
			inifiles='fyppini.py')
'''

import re
import os.path
from waflib import Configure, Logs, Task, TaskGen, Tools, Errors
try:
	import fypp
except ImportError:
	fypp = None


Tools.ccroot.USELIB_VARS['fypp'] = set([ 'DEFINES', 'INCLUDES', 'MODULES',
                                         'INIFILES' ])

FYPP_INCPATH_ST = '-I%s'
FYPP_DEFINES_ST = '-D%s'
FYPP_LINENUM_FLAG = '-n'
FYPP_MODULES_ST = '-m%s'
FYPP_INIFILES_ST = '-i%s'


class FyppPreprocError(Errors.WafError):
	pass


################################################################################
# Configure
################################################################################

def configure(conf):
	fypp_check(conf)
        fypp_add_user_flags(conf)


@Configure.conf
def fypp_add_user_flags(conf):
	'''Import user settings for Fypp.'''
	conf.add_os_flags('FYPP_FLAGS', dup=False)


@Configure.conf
def fypp_check(conf):
	'''Check for Fypp.'''
	conf.start_msg('Checking for fypp module')
	if fypp is None:
		conf.fatal('Python module \'fypp\' could not be imported.')
	version = fypp.VERSION
	version_regexp = re.compile(r'^(?P<major>\d+)\.(?P<minor>\d+)'\
		'(?:\.(?P<patch>\d+))?$')
	match = version_regexp.search(version)
	if not match:
		conf.fatal('cannot parse fypp version string')
	version = (match.group('major'), match.group('minor'))
	conf.env['FYPP_VERSION'] = version
	conf.end_msg('found (version %s.%s)' % version)


################################################################################
# Build
################################################################################

class fypp_preprocessor(Task.Task):

        def keyword(self):
                return 'Preprocessing'

	def run(self):
                argparser = fypp.get_option_parser()
                args = [FYPP_LINENUM_FLAG]
                args += self.env.FYPP_FLAGS
		args += [FYPP_DEFINES_ST % ss for ss in self.env['DEFINES']]
		args += [FYPP_INCPATH_ST % ss for ss in self.env['INCLUDES']]
                args += [FYPP_INIFILES_ST % ss for ss in self.env['INIFILES']]
                args += [FYPP_MODULES_ST % ss for ss in self.env['MODULES']]
                opts, leftover = argparser.parse_args(args)
                infile = self.inputs[0].abspath()
                outfile = self.outputs[0].abspath()
                if Logs.verbose:
                        Logs.debug('runner: fypp.Fypp %r %r %r'
                                   % (args, infile, outfile))

		tool = fypp.Fypp(opts)
                try:
		        tool.process_file(infile, outfile)
                except fypp.FyppError as err:
                        msg = ("%s [%s:%d]"
                               % (err.msg, err.fname, err.span[0] + 1))
                        raise FyppPreprocError(msg)
		return 0

	def scan(self):
		parser = FyppIncludeParser(self.generator.includes_nodes)
		nodes, names = parser.parse(self.inputs[0])
		if Logs.verbose:
			Logs.debug('deps: deps for %r: %r; unresolved: %r'
				% (self.inputs, nodes, names))
		return (nodes, names)


TaskGen.feature('fypp')(Tools.ccroot.propagate_uselib_vars)
TaskGen.feature('fypp')(Tools.ccroot.apply_incpaths)



################################################################################
# Helper routines
################################################################################

class FyppIncludeParser(object):

	'''Parser for include directives in files preprocessed by Fypp.

	It can not handle conditional includes.
	'''

	# Include file pattern, opening and closing quoute must be replaced inside.
	INCLUDE_PATTERN = re.compile(r'^\s*#:include\s*(["\'])(?P<incfile>.+?)\1',
		re.MULTILINE)


	def __init__(self, incpaths):
		'''Initializes the parser.

		:param quotes: Tuple containing the opening and closing quote sign.
		:type quotes: tuple
		'''
		# Nodes still to be processed
		self._waiting = []

		# Files we have already processed
		self._processed = set()

		# List of dependent nodes
		self._dependencies = []

		# List of unresolved dependencies
		self._unresolved = set()

		# Paths to consider when checking for includes
		self._incpaths = incpaths


	def parse(self, node):
		'''Parser the includes in a given node.

		:return: Tuple with two elements: list of dependent nodes and list of
			unresolved depencies.
		'''
		self._waiting = [ node, ]
		# self._waiting is eventually extended during _process() -> iterate
		while self._waiting:
			curnode = self._waiting.pop(0)
			self._process(curnode)
		return (self._dependencies, list(self._unresolved))


	def _process(self, node):
		incfiles = self._get_include_files(node)
		for incfile in incfiles:
			if incfile in self._processed:
				continue
			self._processed.add(incfile)
			incnode = self._find_include_node(node, incfile)
			if incnode:
				self._dependencies.append(incnode)
				self._waiting.append(incnode)
			else:
				self._unresolved.add(incfile)


	def _get_include_files(self, node):
		txt = node.read()
		matches = self.INCLUDE_PATTERN.finditer(txt)
		incs = [ match.group('incfile') for match in matches ]
		return incs


	def _find_include_node(self, node, filename):
		for incpath in self._incpaths:
			incnode = incpath.find_resource(filename)
			if incnode:
				break
		else:
			incnode = node.parent.find_resource(filename)
		return incnode