diff options
Diffstat (limited to '')
-rw-r--r-- | src/boost/tools/litre/cplusplus.py | 810 |
1 files changed, 810 insertions, 0 deletions
diff --git a/src/boost/tools/litre/cplusplus.py b/src/boost/tools/litre/cplusplus.py new file mode 100644 index 000000000..c48e0678e --- /dev/null +++ b/src/boost/tools/litre/cplusplus.py @@ -0,0 +1,810 @@ +# Copyright David Abrahams 2004. +# Copyright Daniel Wallin 2006. +# Distributed under the Boost +# Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +import os +import tempfile +import litre +import re +import sys +import traceback + +# Thanks to Jean Brouwers for this snippet +def _caller(up=0): + '''Get file name, line number, function name and + source text of the caller's caller as 4-tuple: + (file, line, func, text). + + The optional argument 'up' allows retrieval of + a caller further back up into the call stack. + + Note, the source text may be None and function + name may be '?' in the returned result. In + Python 2.3+ the file name may be an absolute + path. + ''' + try: # just get a few frames' + f = traceback.extract_stack(limit=up+2) + if f: + return f[0] + except: + pass + # running with psyco? + return ('', 0, '', None) + +class Example: + closed = False + in_emph = None + + def __init__(self, node, section, line_offset, line_hash = '#'): + # A list of text fragments comprising the Example. Start with a #line + # directive + self.section = section + self.line_hash = line_hash + self.node = node + self.body = [] + self.line_offset = line_offset + self._number_of_prefixes = 0 + + self.emphasized = [] # indices of text strings that have been + # emphasized. These are generally expected to be + # invalid C++ and will need special treatment + + def begin_emphasis(self): + self.in_emph = len(self.body) + + def end_emphasis(self): + self.emphasized.append( (self.in_emph, len(self.body)) ) + + def append(self, s): + self.append_raw(self._make_line(s)) + + def prepend(self, s): + self.prepend_raw(self._make_line(s)) + + def append_raw(self, s): + self.body.append(s) + + def prepend_raw(self, s): + self.body.insert(0,s) + self.emphasized = [ (x[0]+1,x[1]+1) for x in self.emphasized ] + self._number_of_prefixes += 1 + + def replace(self, s1, s2): + self.body = [x.replace(s1,s2) for x in self.body] + + def sub(self, pattern, repl, count = 1, flags = re.MULTILINE): + pat = re.compile(pattern, flags) + for i,txt in enumerate(self.body): + if count > 0: + x, subs = pat.subn(repl, txt, count) + self.body[i] = x + count -= subs + + def wrap(self, s1, s2): + self.append_raw(self._make_line(s2)) + self.prepend_raw(self._make_line(s1, offset = -s1.count('\n'))) + + def replace_emphasis(self, s, index = 0): + """replace the index'th emphasized text with s""" + e = self.emphasized[index] + self.body[e[0]:e[1]] = [s] + del self.emphasized[index] + + elipsis = re.compile('^([ \t]*)([.][.][.][ \t]*)$', re.MULTILINE) + + def __str__(self): + # Comment out any remaining emphasized sections + b = [self.elipsis.sub(r'\1// \2', s) for s in self.body] + emph = self.emphasized + emph.reverse() + for e in emph: + b.insert(e[1], ' */') + b.insert(e[0], '/* ') + emph.reverse() + + # Add initial #line + b.insert( + self._number_of_prefixes, + self._line_directive(self.node.line, self.node.source) + ) + + # Add trailing newline to avoid warnings + b.append('\n') + return ''.join(b) + + def __repr__(self): + return "Example: " + repr(str(self)) + + def raw(self): + return ''.join(self.body) + + def _make_line(self, s, offset = 0): + c = _caller(2)[1::-1] + offset -= s.count('\n') + return '\n%s%s\n' % (self._line_directive(offset = offset, *c), s.strip('\n')) + + def _line_directive(self, line, source, offset = None): + if self.line_hash is None: + return '\n' + + if offset is None: + offset = self.line_offset + + if line is None or line <= -offset: + line = 1 + else: + line += offset + + if source is None: + return '%sline %d\n' % (self.line_hash, line) + else: + return '%sline %d "%s"\n' % (self.line_hash, line, source) + + +def syscmd( + cmd + , expect_error = False + , input = None + , max_output_lines = None + ): + + # On windows close() returns the exit code, on *nix it doesn't so + # we need to use popen2.Popen4 instead. + if sys.platform == 'win32': + stdin, stdout_stderr = os.popen4(cmd) + if input: stdin.write(input) + stdin.close() + + out = stdout_stderr.read() + status = stdout_stderr.close() + else: + import popen2 + process = popen2.Popen4(cmd) + if input: process.tochild.write(input) + out = process.fromchild.read() + status = process.wait() + + if max_output_lines is not None: + out = '\n'.join(out.split('\n')[:max_output_lines]) + + if expect_error: + status = not status + + if status: + print + print '========== offending command ===========' + print cmd + print '------------ stdout/stderr -------------' + print expect_error and 'Error expected, but none seen' or out + elif expect_error > 1: + print + print '------ Output of Expected Error --------' + print out + print '----------------------------------------' + + sys.stdout.flush() + + return (status,out) + + +def expand_vars(path): + if os.name == 'nt': + re_env = re.compile(r'%\w+%') + return re_env.sub( + lambda m: os.environ.get( m.group(0)[1:-1] ) + , path + ) + else: + return os.path.expandvars(path) + +def remove_directory_and_contents(path): + for root, dirs, files in os.walk(path, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + os.rmdir(path) + +class BuildResult: + def __init__(self, path): + self.path = path + + def __repr__(self): + return self.path + + def __del__(self): + remove_directory_and_contents(self.path) + +class CPlusPlusTranslator(litre.LitreTranslator): + + _exposed_attrs = ['compile', 'test', 'ignore', 'match_stdout', 'stack', 'config' + , 'example', 'prefix', 'preprocessors', 'litre_directory', + 'litre_translator', 'includes', 'build', 'jam_prefix', + 'run_python'] + + last_run_output = '' + + """Attributes that will be made available to litre code""" + + def __init__(self, document, config): + litre.LitreTranslator.__init__(self, document, config) + self.in_literal = False + self.in_table = True + self.preprocessors = [] + self.stack = [] + self.example = None + self.prefix = [] + self.includes = config.includes + self.litre_directory = os.path.split(__file__)[0] + self.config = config + self.litre_translator = self + self.line_offset = 0 + self.last_source = None + self.jam_prefix = [] + + self.globals = { 'test_literals_in_tables' : False } + for m in self._exposed_attrs: + self.globals[m] = getattr(self, m) + + self.examples = {} + self.current_section = None + + # + # Stuff for use by docutils writer framework + # + def visit_emphasis(self, node): + if self.in_literal: + self.example.begin_emphasis() + + def depart_emphasis(self, node): + if self.in_literal: + self.example.end_emphasis() + + def visit_section(self, node): + self.current_section = node['ids'][0] + + def visit_literal_block(self, node): + if node.source is None: + node.source = self.last_source + self.last_source = node.source + + # create a new example + self.example = Example(node, self.current_section, line_offset = self.line_offset, line_hash = self.config.line_hash) + + self.stack.append(self.example) + + self.in_literal = True + + def depart_literal_block(self, node): + self.in_literal = False + + def visit_literal(self, node): + if self.in_table and self.globals['test_literals_in_tables']: + self.visit_literal_block(node) + else: + litre.LitreTranslator.visit_literal(self,node) + + def depart_literal(self, node): + if self.in_table and self.globals['test_literals_in_tables']: + self.depart_literal_block(node) + else: + litre.LitreTranslator.depart_literal(self,node) + + def visit_table(self,node): + self.in_table = True + litre.LitreTranslator.visit_table(self,node) + + def depart_table(self,node): + self.in_table = False + litre.LitreTranslator.depart_table(self,node) + + def visit_Text(self, node): + if self.in_literal: + self.example.append_raw(node.astext()) + + def depart_document(self, node): + self.write_examples() + + # + # Private stuff + # + + def handled(self, n = 1): + r = self.stack[-n:] + del self.stack[-n:] + return r + + def _execute(self, code): + """Override of litre._execute; sets up variable context before + evaluating code + """ + self.globals['example'] = self.example + eval(code, self.globals) + + # + # Stuff for use by embedded python code + # + + def match_stdout(self, expected = None): + + if expected is None: + expected = self.example.raw() + self.handled() + + if not re.search(expected, self.last_run_output, re.MULTILINE): + #if self.last_run_output.strip('\n') != expected.strip('\n'): + print 'output failed to match example' + print '-------- Actual Output -------------' + print repr(self.last_run_output) + print '-------- Expected Output -----------' + print repr(expected) + print '------------------------------------' + sys.stdout.flush() + + def ignore(self, n = 1): + if n == 'all': + n = len(self.stack) + return self.handled(n) + + def wrap(self, n, s1, s2): + self.stack[-1].append(s2) + self.stack[-n].prepend(s1) + + + def compile( + self + , howmany = 1 + , pop = -1 + , expect_error = False + , extension = '.o' + , options = ['-c'] + , built_handler = lambda built_file: None + , source_file = None + , source_suffix = '.cpp' + # C-style comments by default; handles C++ and YACC + , make_comment = lambda text: '/*\n%s\n*/' % text + , built_file = None + , command = None + ): + """ + Compile examples on the stack, whose topmost item is the last example + seen but not yet handled so far. + + :howmany: How many of the topmost examples on the stack to compile. + You can pass a number, or 'all' to indicate that all examples should + be compiled. + + :pop: How many of the topmost examples to discard. By default, all of + the examples that are compiled are discarded. + + :expect_error: Whether a compilation error is to be expected. Any value + > 1 will cause the expected diagnostic's text to be dumped for + diagnostic purposes. It's common to expect an error but see a + completely unrelated one because of bugs in the example (you can get + this behavior for all examples by setting show_expected_error_output + in your config). + + :extension: The extension of the file to build (set to .exe for + run) + + :options: Compiler flags + + :built_file: A path to use for the built file. By default, a temp + filename is conjured up + + :built_handler: A function that's called with the name of the built file + upon success. + + :source_file: The full name of the source file to write + + :source_suffix: If source_file is None, the suffix to use for the source file + + :make_comment: A function that transforms text into an appropriate comment. + + :command: A function that is passed (includes, opts, target, source), where + opts is a string representing compiler options, target is the name of + the file to build, and source is the name of the file into which the + example code is written. By default, the function formats + litre.config.compiler with its argument tuple. + """ + + # Grab one example by default + if howmany == 'all': + howmany = len(self.stack) + + source = '\n'.join( + self.prefix + + [str(x) for x in self.stack[-howmany:]] + ) + + source = reduce(lambda s, f: f(s), self.preprocessors, source) + + if pop: + if pop < 0: + pop = howmany + del self.stack[-pop:] + + if len(self.stack): + self.example = self.stack[-1] + + cpp = self._source_file_path(source_file, source_suffix) + + if built_file is None: + built_file = self._output_file_path(source_file, extension) + + opts = ' '.join(options) + + includes = ' '.join(['-I%s' % d for d in self.includes]) + if not command: + command = self.config.compiler + + if type(command) == str: + command = lambda i, o, t, s, c = command: c % (i, o, t, s) + + cmd = command(includes, opts, expand_vars(built_file), expand_vars(cpp)) + + if expect_error and self.config.show_expected_error_output: + expect_error += 1 + + + comment_cmd = command(includes, opts, built_file, os.path.basename(cpp)) + comment = make_comment(config.comment_text(comment_cmd, expect_error)) + + self._write_source(cpp, '\n'.join([comment, source])) + + #print 'wrote in', cpp + #print 'trying command', cmd + + status, output = syscmd(cmd, expect_error) + + if status or expect_error > 1: + print + if expect_error and expect_error < 2: + print 'Compilation failure expected, but none seen' + print '------------ begin offending source ------------' + print open(cpp).read() + print '------------ end offending source ------------' + + if self.config.save_cpp: + print 'saved in', repr(cpp) + else: + self._remove_source(cpp) + + sys.stdout.flush() + else: + print '.', + sys.stdout.flush() + built_handler(built_file) + + self._remove_source(cpp) + + try: + self._unlink(built_file) + except: + if not expect_error: + print 'failed to unlink', built_file + + return status + + def test( + self + , rule = 'run' + , howmany = 1 + , pop = -1 + , expect_error = False + , requirements = '' + , input = '' + ): + + # Grab one example by default + if howmany == 'all': + howmany = len(self.stack) + + source = '\n'.join( + self.prefix + + [str(x) for x in self.stack[-howmany:]] + ) + + source = reduce(lambda s, f: f(s), self.preprocessors, source) + + id = self.example.section + if not id: + id = 'top-level' + + if not self.examples.has_key(self.example.section): + self.examples[id] = [(rule, source)] + else: + self.examples[id].append((rule, source)) + + if pop: + if pop < 0: + pop = howmany + del self.stack[-pop:] + + if len(self.stack): + self.example = self.stack[-1] + + def write_examples(self): + jam = open(os.path.join(self.config.dump_dir, 'Jamfile.v2'), 'w') + + jam.write(''' +import testing ; + +''') + + for id,examples in self.examples.items(): + for i in range(len(examples)): + cpp = '%s%d.cpp' % (id, i) + + jam.write('%s %s ;\n' % (examples[i][0], cpp)) + + outfile = os.path.join(self.config.dump_dir, cpp) + print cpp, + try: + if open(outfile, 'r').read() == examples[i][1]: + print ' .. skip' + continue + except: + pass + + open(outfile, 'w').write(examples[i][1]) + print ' .. written' + + jam.close() + + def build( + self + , howmany = 1 + , pop = -1 + , source_file = 'example.cpp' + , expect_error = False + , target_rule = 'obj' + , requirements = '' + , input = '' + , output = 'example_output' + ): + + # Grab one example by default + if howmany == 'all': + howmany = len(self.stack) + + source = '\n'.join( + self.prefix + + [str(x) for x in self.stack[-howmany:]] + ) + + source = reduce(lambda s, f: f(s), self.preprocessors, source) + + if pop: + if pop < 0: + pop = howmany + del self.stack[-pop:] + + if len(self.stack): + self.example = self.stack[-1] + + dir = tempfile.mkdtemp() + cpp = os.path.join(dir, source_file) + self._write_source(cpp, source) + self._write_jamfile( + dir + , target_rule = target_rule + , requirements = requirements + , input = input + , output = output + ) + + cmd = 'bjam' + if self.config.bjam_options: + cmd += ' %s' % self.config.bjam_options + + os.chdir(dir) + status, output = syscmd(cmd, expect_error) + + if status or expect_error > 1: + print + if expect_error and expect_error < 2: + print 'Compilation failure expected, but none seen' + print '------------ begin offending source ------------' + print open(cpp).read() + print '------------ begin offending Jamfile -----------' + print open(os.path.join(dir, 'Jamroot')).read() + print '------------ end offending Jamfile -------------' + + sys.stdout.flush() + else: + print '.', + sys.stdout.flush() + + if status: return None + else: return BuildResult(dir) + + def _write_jamfile(self, path, target_rule, requirements, input, output): + jamfile = open(os.path.join(path, 'Jamroot'), 'w') + contents = r""" +import modules ; + +BOOST_ROOT = [ modules.peek : BOOST_ROOT ] ; +use-project /boost : $(BOOST_ROOT) ; + +%s + +%s %s + : example.cpp %s + : <include>. + %s + %s + ; + """ % ( + '\n'.join(self.jam_prefix) + , target_rule + , output + , input + , ' '.join(['<include>%s' % d for d in self.includes]) + , requirements + ) + + jamfile.write(contents) + + def run_python( + self + , howmany = 1 + , pop = -1 + , module_path = [] + , expect_error = False + ): + # Grab one example by default + if howmany == 'all': + howmany = len(self.stack) + + if module_path == None: module_path = [] + + if isinstance(module_path, BuildResult) or type(module_path) == str: + module_path = [module_path] + + module_path = map(lambda p: str(p), module_path) + + source = '\n'.join( + self.prefix + + [str(x) for x in self.stack[-howmany:]] + ) + + if pop: + if pop < 0: + pop = howmany + del self.stack[-pop:] + + if len(self.stack): + self.example = self.stack[-1] + + r = re.compile(r'^(>>>|\.\.\.) (.*)$', re.MULTILINE) + source = r.sub(r'\2', source) + py = self._source_file_path(source_file = None, source_suffix = 'py') + open(py, 'w').write(source) + + old_path = os.getenv('PYTHONPATH') + if old_path == None: + pythonpath = ':'.join(module_path) + old_path = '' + else: + pythonpath = old_path + ':%s' % ':'.join(module_path) + + os.putenv('PYTHONPATH', pythonpath) + status, output = syscmd('python %s' % py) + + if status or expect_error > 1: + print + if expect_error and expect_error < 2: + print 'Compilation failure expected, but none seen' + print '------------ begin offending source ------------' + print open(py).read() + print '------------ end offending Jamfile -------------' + + sys.stdout.flush() + else: + print '.', + sys.stdout.flush() + + self.last_run_output = output + os.putenv('PYTHONPATH', old_path) + self._unlink(py) + + def _write_source(self, filename, contents): + open(filename,'w').write(contents) + + def _remove_source(self, source_path): + os.unlink(source_path) + + def _source_file_path(self, source_file, source_suffix): + if source_file is None: + cpp = tempfile.mktemp(suffix=source_suffix) + else: + cpp = os.path.join(tempfile.gettempdir(), source_file) + return cpp + + def _output_file_path(self, source_file, extension): + return tempfile.mktemp(suffix=extension) + + def _unlink(self, file): + file = expand_vars(file) + if os.path.exists(file): + os.unlink(file) + + def _launch(self, exe, stdin = None): + status, output = syscmd(exe, input = stdin) + self.last_run_output = output + + def run_(self, howmany = 1, stdin = None, **kw): + new_kw = { 'options':[], 'extension':'.exe' } + new_kw.update(kw) + + self.compile( + howmany + , built_handler = lambda exe: self._launch(exe, stdin = stdin) + , **new_kw + ) + + def astext(self): + return "" + return '\n\n ---------------- Unhandled Fragment ------------ \n\n'.join( + [''] # generates a leading announcement + + [ unicode(s) for s in self.stack] + ) + +class DumpTranslator(CPlusPlusTranslator): + example_index = 1 + + def _source_file_path(self, source_file, source_suffix): + if source_file is None: + source_file = 'example%s%s' % (self.example_index, source_suffix) + self.example_index += 1 + + cpp = os.path.join(config.dump_dir, source_file) + return cpp + + def _output_file_path(self, source_file, extension): + chapter = os.path.basename(config.dump_dir) + return '%%TEMP%%\metaprogram-%s-example%s%s' \ + % ( chapter, self.example_index - 1, extension) + + def _remove_source(self, source_path): + pass + + +class WorkaroundTranslator(DumpTranslator): + """Translator used to test/dump workaround examples for vc6 and vc7. Just + like a DumpTranslator except that we leave existing files alone. + + Warning: not sensitive to changes in .rst source!! If you change the actual + examples in source files you will have to move the example files out of the + way and regenerate them, then re-incorporate the workarounds. + """ + def _write_source(self, filename, contents): + if not os.path.exists(filename): + DumpTranslator._write_source(self, filename, contents) + +class Config: + save_cpp = False + line_hash = '#' + show_expected_error_output = False + max_output_lines = None + +class Writer(litre.Writer): + translator = CPlusPlusTranslator + + def __init__( + self + , config + ): + litre.Writer.__init__(self) + self._config = Config() + defaults = Config.__dict__ + + # update config elements + self._config.__dict__.update(config.__dict__) +# dict([i for i in config.__dict__.items() +# if i[0] in config.__all__])) + |