Skip to content

Commit ecd2f97

Browse files
committed
Reworked emitters and generation helpers.
Summary: There's no way to emit a partial line. - emit_line() is now just emit() - slightly modified @rpearl's block context manager - emit_raw() is used for multi-line strings - removed indent_to_cursor(). this functionality is now handled directly by generate_multiline_list() Test Plan: Added tests for this and more. (yay! the generator ABC had no tests before) Reviewed By: guido
1 parent 3b9fa6e commit ecd2f97

4 files changed

Lines changed: 507 additions & 275 deletions

File tree

babelapi/generator.py

Lines changed: 163 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class to be recognized as such.
1919
...
2020
2121
2. Use the family of emit*() functions to write to the output file.
22+
23+
The api attribute holds all information parsed from the specs, and should
24+
be used during generation.
2225
"""
2326

2427
__metaclass__ = ABCMeta
@@ -39,8 +42,10 @@ def __init__(self, api, target_folder_path):
3942

4043
@abstractmethod
4144
def generate(self):
42-
"""Subclasses should override this method. It's the entry point for
43-
all code generation given the api description."""
45+
"""
46+
Subclasses should override this method. It's the entry point that is
47+
invoked by the rest of the toolchain.
48+
"""
4449
raise NotImplemented
4550

4651
@contextmanager
@@ -49,7 +54,7 @@ def output_to_relative_path(self, relative_path):
4954
Sets up generator so that all emits are directed towards the new file
5055
created at :param:`relative_path`.
5156
52-
Clears output buffer on enter, and on exit.
57+
Clears the output buffer on enter and exit.
5358
"""
5459
full_path = os.path.join(self.target_folder_path, relative_path)
5560
self._logger.info('Generating %s', full_path)
@@ -59,14 +64,22 @@ def output_to_relative_path(self, relative_path):
5964
f.write(''.join(self.output))
6065
self.output = []
6166

67+
def output_buffer_to_string(self):
68+
"""Returns the contents of the output buffer as a string."""
69+
return ''.join(self.output)
70+
71+
def clear_output_buffer(self):
72+
self.output = []
73+
6274
@contextmanager
6375
def indent(self, dent=None):
6476
"""
6577
For the duration of the context manager, indentation will be increased
6678
by dent. Dent is in units of spaces or tabs depending on the value of
67-
the class variable tabs_for_indents.
79+
the class variable tabs_for_indents. If dent is None, indentation will
80+
increase by either four spaces or one tab.
6881
"""
69-
assert dent != 0, 'Cannot specify relative indent of 0'
82+
assert dent is None or dent > 0, 'dent must be a whole number.'
7083
if dent is None:
7184
if self.tabs_for_indents:
7285
dent = 1
@@ -76,100 +89,83 @@ def indent(self, dent=None):
7689
yield
7790
self.cur_indent -= dent
7891

79-
@contextmanager
80-
def block(self, header='', dent=None, delim=('{','}')):
81-
if header:
82-
self.emit_line('{} {}'.format(header, delim[0]))
83-
else:
84-
self.emit_line(delim[0])
85-
86-
with self.indent(dent):
87-
yield
88-
89-
self.emit_line(delim[1])
90-
91-
@contextmanager
92-
def indent_to_cur_col(self):
92+
def make_indent(self):
9393
"""
94-
For the duration of the context manager, indentation will be set to the
95-
current column marked by the "cursor". The cursor is what column of the
96-
current line the next emit call would begin writing at.
94+
Returns a string representing the current indentation. Indents can be
95+
either spaces or tabs, depending on the value of the class variable
96+
tabs_for_indents.
9797
"""
98-
dent = 0
99-
for s in self.output[::-1]:
100-
index = s.rfind('\n')
101-
if index == -1:
102-
dent += len(s)
103-
else:
104-
dent += len(s) - index - 1
105-
break
106-
dent_diff = dent - self.cur_indent
107-
self.cur_indent += dent_diff
108-
yield
109-
self.cur_indent -= dent_diff
110-
111-
def make_indent(self):
112-
"""Returns a string representing an indent. Indents can be either
113-
spaces or tabs, depending on the value of the class variable
114-
tabs_for_indents."""
11598
if self.tabs_for_indents:
11699
return '\t' * self.cur_indent
117100
else:
118101
return ' ' * self.cur_indent
119102

120-
def emit(self, s):
121-
"""Adds the input string to the output buffer."""
103+
def emit_raw(self, s):
104+
"""
105+
Adds the input string to the output buffer. The string must end in a
106+
newline. It may contain any number of newline characters. No
107+
indentation is generated.
108+
"""
122109
self.lineno += s.count('\n')
123110
self.output.append(s)
111+
if len(s) > 0 and s[-1] != '\n':
112+
raise AssertionError(
113+
'Input string to emit_raw must end with a newline.')
124114

125-
def emit_indent(self):
126-
"""Adds an indent into the output buffer."""
127-
self.emit(self.make_indent())
128-
129-
def emit_line(self, s, trailing_newline=True):
130-
"""Adds an indent, then the input string, and lastly a newline to the
131-
output buffer. If you want the input string to potentially span across
132-
multiple lines, see :func:`emit_string_wrap`."""
133-
self.emit_indent()
134-
self.emit(s)
135-
if trailing_newline:
136-
self.emit('\n')
137-
138-
def emit_empty_line(self):
139-
"""Adds a newline to the output buffer."""
140-
self.emit('\n')
115+
def emit(self, s=''):
116+
"""
117+
Adds indentation, then the input string, and lastly a newline to the
118+
output buffer. If s is an empty string (default) then an empty line is
119+
created with no indentation.
120+
"""
121+
assert isinstance(s, basestring), 's must be a string type'
122+
assert '\n' not in s, \
123+
'String to emit cannot contain newline strings.'
124+
if s:
125+
self.emit_raw('%s%s\n' % (self.make_indent(), s))
126+
else:
127+
self.emit_raw('\n')
141128

142-
def emit_wrapped_lines(self, s, prefix='', width=80, trailing_newline=True, first_line_prefix=True):
129+
def emit_wrapped_text(self, s, initial_prefix='', subsequent_prefix='',
130+
width=80, break_long_words=False, break_on_hyphens=False):
143131
"""
144-
Adds the input string to the output buffer with wrapping.
132+
Adds the input string to the output buffer with indentation and
133+
wrapping. The wrapping is performed by the :func:`textwrap.fill` Python
134+
library function.
145135
146136
Args:
147-
s: The input string to wrap.
148-
prefix: The string to prepend to every line of the wrapped string.
149-
Does not include indenting in the prefix as those are injected
150-
automatically on every line.
151-
width: The target width of each line including indentation and text.
152-
"""
153-
indent = self.make_indent() + prefix
154-
if first_line_prefix:
155-
initial_indent = indent
156-
else:
157-
initial_indent = self.make_indent()
158-
159-
self.emit(textwrap.fill(s,
160-
initial_indent=initial_indent,
161-
subsequent_indent=indent,
162-
width=80))
163-
if trailing_newline:
164-
self.emit('\n')
137+
s (str): The input string to wrap.
138+
initial_prefix (str): The string to prepend to the first line of
139+
the wrapped string. Note that the current indentation is
140+
already added to each line.
141+
subsequent_prefix (str): The string to prepend to every line after
142+
the first. Note that the current indentation is already added
143+
to each line.
144+
width (int): The target width of each line including indentation
145+
and text.
146+
break_long_words (bool): Break words longer than width. If false,
147+
those words will not be broken, and some lines might be longer
148+
than width.
149+
break_on_hyphens (bool): Allow breaking hyphenated words. If true,
150+
wrapping will occur preferably on whitespaces and right after
151+
hyphens part of compound words.
152+
"""
153+
indent = self.make_indent()
154+
self.emit_raw(textwrap.fill(s,
155+
initial_indent=indent+initial_prefix,
156+
subsequent_indent=indent+subsequent_prefix,
157+
width=width,
158+
break_long_words=break_long_words,
159+
break_on_hyphens=break_on_hyphens,
160+
) + '\n')
165161

166162
class CodeGenerator(Generator):
167163
"""
168164
Extend this instead of :class:`Generator` when generating source code.
169165
Contains helper functions specific to code generation.
170166
"""
171167

172-
def _filter_out_none_valued_keys(self, d):
168+
def filter_out_none_valued_keys(self, d):
173169
"""Given a dict, returns a new dict with all the same key/values except
174170
for keys that had values of None."""
175171
new_d = {}
@@ -178,45 +174,101 @@ def _filter_out_none_valued_keys(self, d):
178174
new_d[k] = v
179175
return new_d
180176

181-
def _generate_func_arg_list(self, args, compact=True):
177+
def generate_multiline_list(self, items, before='', after='',
178+
delim=('(', ')'), compact=True, sep=',', skip_last_sep=False):
182179
"""
183-
Given a list of arguments to a function, emits the args, one per line
184-
with a trailing comma. The arguments are enclosed in parentheses making
185-
this convenient way to create argument lists in function prototypes and
186-
calls.
180+
Given a list of items, emits one item per line.
181+
182+
This is convenient for function prototypes and invocations, as well as
183+
for instantiating arrays, sets, and maps in some languages.
184+
185+
TODO(kelkabany): A generator that uses tabs cannot be used with this
186+
if compact is false.
187187
188188
Args:
189-
args: List of strings where each string is an argument.
190-
compact: In compact mode, the enclosing parentheses are on the same
191-
lines as the first and last argument.
189+
items (list[str]): Should contain the items to generate a list of.
190+
before (str): The string to come before the list of items.
191+
after (str): The string to follow the list of items.
192+
delim (str, str): The first element is added immediately following
193+
`before`. The second element is added prior to `after`.
194+
compact (bool): In compact mode, the enclosing parentheses are on
195+
the same lines as the first and last list item.
196+
sep (str): The string that follows each list item when compact is
197+
true. If compact is false, the separator is omitted for the
198+
last item.
199+
skip_last_sep (bool): When compact is false, whether the last line
200+
should have a trailing separator. Ignored when compact is true.
192201
"""
193-
self.emit('(')
194-
if len(args) == 0:
195-
self.emit(')')
202+
assert len(delim) == 2 and isinstance(delim[0], str) and \
203+
isinstance(delim[1], str), 'delim must be a tuple of two strings.'
204+
205+
if len(items) == 0:
206+
self.emit(before + delim[0] + delim[1] + after)
196207
return
197-
elif len(args) == 1:
198-
self.emit(args[0])
199-
self.emit(')')
200-
else:
201-
if compact:
202-
with self.indent_to_cur_col():
203-
args = args[:]
204-
self.emit(args.pop(0))
205-
self.emit(',')
206-
self.emit_empty_line()
207-
for (i, arg) in enumerate(args):
208-
if i == len(args) - 1:
209-
self.emit_line(arg, trailing_newline=False)
210-
else:
211-
self.emit_line(arg + ',')
212-
self.emit(')')
208+
if len(items) == 1:
209+
self.emit(before + delim[0] + items[0] + delim[1] + after)
210+
return
211+
212+
if compact:
213+
self.emit(before + delim[0] + items[0] + sep)
214+
def emit_list(items):
215+
items = items[1:]
216+
for (i, item) in enumerate(items):
217+
if i == len(items) - 1:
218+
self.emit(item + delim[1] + after)
219+
else:
220+
self.emit(item + sep)
221+
if before or delim[0]:
222+
with self.indent(len(before) + len(delim[0])):
223+
emit_list(items)
213224
else:
214-
self.emit_empty_line()
215-
with self.indent():
216-
for arg in args:
217-
self.emit_line(arg + ',')
218-
self.emit_indent()
219-
self.emit(')')
225+
emit_list(items)
226+
else:
227+
if before or delim[0]:
228+
self.emit(before + delim[0])
229+
with self.indent():
230+
for (i, item) in enumerate(items):
231+
if i == len(items) - 1 and skip_last_sep:
232+
self.emit(item)
233+
else:
234+
self.emit(item + sep)
235+
if delim[1] or after:
236+
self.emit(delim[1] + after)
237+
elif delim[1]:
238+
self.emit(delim[1])
239+
240+
@contextmanager
241+
def block(self, before='', after='', delim=('{','}'), dent=None):
242+
"""
243+
A context manager that emits configurable lines before and after an
244+
indented block of text.
245+
246+
This is convenient for class and function definitions in some
247+
languages.
248+
249+
Args:
250+
before (str): The string to be output in the first line which is
251+
not indented..
252+
after (str): The string to be output in the last line which is
253+
not indented.
254+
delim (str, str): The first element is added immediately following
255+
`before` and a space. The second element is added prior to a
256+
space and then `after`.
257+
dent (int): The amount to indent the block. If none, the default
258+
indentation increment is used (four spaces or one tab).
259+
"""
260+
assert len(delim) == 2 and isinstance(delim[0], str) and \
261+
isinstance(delim[1], str), 'delim must be a tuple of two strings.'
262+
263+
if before:
264+
self.emit('{} {}'.format(before, delim[0]))
265+
else:
266+
self.emit(delim[0])
267+
268+
with self.indent(dent):
269+
yield
270+
271+
self.emit(delim[1] + after)
220272

221273
class CodeGeneratorMonolingual(CodeGenerator):
222274
"""Identical to CodeGenerator, except that an additional attribute `lang`

0 commit comments

Comments
 (0)