Skip to content

Commit

Permalink
rewrite emitter
Browse files Browse the repository at this point in the history
Split emitter into separate object holding just the prefix and
indent_by information while everything else is handled by Section.
Section buffers the output while the Emitter.emit method writes
out a section to file.

Note that this is meant to fix:
* corner-cases with padding where padding is either too much or
  too little - now padding is merged when emitting to file such
  that the maximum padding value is used.
* can now define Sections stand-alone and fill them out. This permits
  reusing sections and to begin filling them out when convenient.
  • Loading branch information
jwdevantier committed Aug 19, 2022
1 parent 8de1cc4 commit 32f6622
Show file tree
Hide file tree
Showing 38 changed files with 588 additions and 376 deletions.
16 changes: 8 additions & 8 deletions docs/codeblocks/helloworld.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
from gcgen.api import Emitter, Scope, Json, snippet
from gcgen.api import Section, Scope, Json, snippet


@snippet("test")
def gen_arithmetic_ops(e: Emitter, s: Scope, val: Json):
def gen_arithmetic_ops(sec: Section, s: Scope, val: Json):
ops = {
"add": "+",
"sub": "-",
"div": "/",
"mul": "*",
}
for name, op in ops.items():
e.emitln(f"def {name}(x, y):")
e.indent()
e.emitln(f"return x {op} y")
e.dedent()
e.newline()
e.newline()
sec.emitln(f"def {name}(x, y):")
sec.indent()
sec.emitln(f"return x {op} y")
sec.dedent()
sec.newline()
sec.newline()


def gcgen_parse_files():
Expand Down
2 changes: 1 addition & 1 deletion docs/design.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This project does not try to radically redefine code-generation or
introduce a novel hybrid template/code DSL.
Instead it uses plain Python, introducing only the concept of a
snippet, a region of code within a source file, whose contents is
managed by a corresponding Python function which uses a simple emitter
managed by a corresponding Python function which uses a simple section
class to write plain lines of text and manage indentation.

It's very tempting to think a problem requires a new (bad) DSL, and
Expand Down
16 changes: 8 additions & 8 deletions docs/examples/01_intro/gcgen_conf.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
from gcgen.api import Emitter, Scope, Json, snippet
from gcgen.api import Section, Scope, Json, snippet


# Snippets are annotated `@snippet("<name>")`
@snippet("common_math_funcs")
def gen_arithmetic_ops(e: Emitter, s: Scope, val: Json):
def gen_arithmetic_ops(sec: Section, s: Scope, val: Json):
first = True
for lbl, op in val:
if not first:
e.newline()
e.newline()
sec.newline()
sec.newline()
else:
first = False
e.emitln(f"def {lbl}(x, y):")
e.indent()
e.emitln(f"return x {op} y")
e.dedent()
sec.emitln(f"def {lbl}(x, y):")
sec.indent()
sec.emitln(f"return x {op} y")
sec.dedent()


def gcgen_parse_files():
Expand Down
6 changes: 3 additions & 3 deletions docs/getting_started/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ A high-level introduction to gcg
Snippets & Generators
~~~~~~~~~~~~~~~~~~~~~
gcg supports two forms of code generation, snippets and generators, both
generate code using a :ref:`Emitter <sec-ref-emitter>`, which provides a
generate code using a :ref:`section <sec-ref-section>`, which provides a
convenient API for writing output, lines and handling indentation.

:ref:`Snippets <sec-ref-snippets>`, as shown above, mark regions within a
Expand All @@ -85,8 +85,8 @@ file (see next paragraph), which are mostly useful when generating files,
where the number and naming of the files is dynamic and depends on some input
data or model.

Each snippet and generator are passed two objects, an
:ref:`emitter <sec-ref-emitter>` as mentioned before, and a
Each snippet and generator are passed two objects, a
:ref:`section <sec-ref-section>` as mentioned before, and a
:ref:`scope <sec-ref-scope>`.

Scope
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Contents
reference/scope/index.rst
reference/snippets/index.rst
reference/generators/index.rst
reference/emitter/index.rst
reference/section/index.rst
reference/api/index.rst


Expand Down
2 changes: 1 addition & 1 deletion docs/reference/gcgen_file/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Configure indentation
.. _sec-ref-conf-indent-by:

Each :ref:`snippet <sec-ref-snippets>` and :ref:`generator <sec-ref-generators>`
is passed an :ref:`emitter <sec-ref-emitter>` object which is used to produce
is passed an :ref:`section <sec-ref-section>` object which is used to produce
the generated output and to indent & dedent lines.

In the ``gcgen_conf.py`` file, you can define the characters used to indent on a
Expand Down
16 changes: 8 additions & 8 deletions docs/reference/generators/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Snippets should be preferred to generators in most cases because:

* snippets allow mixing hand-written and generated code
* snippets allow using multiple snippets to build the final file (compositional)
* snippets provide a properly initialized :ref:`Emitter <sec-ref-emitter>`
* snippets provide a properly initialized :ref:`Section <sec-ref-section>`
object
* ``gcgen`` ensures that either the full file is processed successfully or
no changes are made to the file
Expand All @@ -27,17 +27,17 @@ The following example shows a simple generator:
@generator
def do_something(scope: Scope):
with write_file("foo.txt") as e:
e.emitln("line 1")
e.emitln("line 2")
with write_file("bar.txt") as e:
e.emitln("line 1")
e.emitln("line 2")
with write_file("foo.txt") as section:
section.emitln("line 1")
section.emitln("line 2")
with write_file("bar.txt") as section:
section.emitln("line 1")
section.emitln("line 2")
...
This particular generator uses the ``write_file`` helper, which provides an
``Emitter`` and which, if the code exits the context without raising an
``Section`` and which, if the code exits the context without raising an
exception, writes to the file given by the filename.
Note, just like for snippets, ``write_file`` operates on a temporary file first,
thereby preventing any files whose output is only half-way generated.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
.. _sec-ref-emitter:
.. _sec-ref-section:

The code emitter
Section
################

All :ref:`snippets <sec-ref-snippets>` and :ref:`generators <sec-ref-generators>`
are provided an emitter, which they must use to generate their output.
are provided an section, which they must use to generate their output.

The emitter provides a convenient if simple abstraction for output generation.
The section provides a convenient if simple abstraction for output generation.
The main objective of the API is to control lines and indentation.
This permits gcgen to ensure that output generated from a snippet is always
properly indented, such that each line of the snippet are indented relative
Expand All @@ -18,19 +18,19 @@ Example

.. code-block:: python3
from gcgen.api import Scope, Emitter, Json, snippet
from gcgen.api import Scope, Section, Json, snippet
@snippet("my-snippet")
def include_file_output(e: Emitter, s: Scope, v: Json):
def include_file_output(sec: Section, scope: Scope, val: Json):
if isinstance(v, str):
filename = v
else:
raise RuntimeError("expected filename passed as string argument")
e.emitln(f"""with open({filename!r}) as fh:""")
e.indent()
e.emitln("for lines in fh.readlines():")
e.indent()
e.emitln("...")
sec.emitln(f"""with open({filename!r}) as fh:""")
sec.indent()
sec.emitln("for lines in fh.readlines():")
sec.indent()
sec.emitln("...")
Given the following file:
Expand All @@ -56,13 +56,13 @@ The expanded output would become:
# ?>>
**Note**: While the emitter controls the level of indentation, the characters
**Note**: While the section controls the level of indentation, the characters
used to indent the line is actually determined by
:ref:`gcgen_indent_by <sec-ref-conf-indent-by>`, which can be used to define
how indentation should be handled on a file-type basis.

Emitter API
Section API
===========
.. autoclass:: gcgen.api.Emitter
.. autoclass:: gcgen.api.Section
:members:
:noindex:
61 changes: 27 additions & 34 deletions docs/reference/snippets/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Defining a snippet

Snippets are functions taking three arguments:

#. :ref:`emitter <sec-ref-emitter>` - this is used to generate output in the file calling the snippet
#. :ref:`section <sec-ref-section>` - this is used to generate output in the file calling the snippet
#. a :ref:`scope <sec-ref-scope>` - this is populated both by :ref:`gcgen <sec-ref-gcgen-file>` files and preceding snippets in the same file.
#. a ``Json`` value - snippets may receive an argument, which must be a valid Json value. The value is ``None`` if no argument was given or ``null`` was passed.

Expand All @@ -99,11 +99,11 @@ defines the name to give it.
:emphasize-lines: 5, 6
# (inside a gcgen_conf.py file)
from gcgen.api import Emitter, Scope, Json, snippet
from gcgen.api import Section, Scope, Json, snippet
@snippet("my-snippet")
def my_snippet(e: Emitter, s: Scope, v: Json):
def my_snippet(sec: Section, s: Scope, v: Json):
pass
Expand Down Expand Up @@ -134,30 +134,23 @@ How to use snippets effectively

.. _sec-ref-snippets-params:

Why snippets cannot take parameters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Why can snippets only take one parameter?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Snippets are intentionally limited to take at *most* one JSON argument.
Gcgen is inspired by tools like `Cog <https://nedbatchelder.com/code/cog>`_, but
disagrees with inlining code-generation code into source files.
Inlining code both clutters the source file and introduces code, for which
the user gets no ide/linting/type-checking support.

If you consider that each function argument can be an arbitrarily complex
Python expression, you will see why implementing parameter support in effect
means allowing in-line code.
By limiting input to a single JSON value, we allow input arguments without
supporting inline code. By limiting arguments to the snippet opening line,
we further push code-generation logic out of the source file and into the
:ref:`gcgen-file <sec-ref-gcgen-file>`.

However, by limiting input to Json, we allow input arguments without
supporting in-line code. By not supporting multi-line values, we furthermore
push complexity out into the :ref:`link <sec-ref-gcgen-file>` files, but
allow some parametrization of snippets.

Limited parametrization avoids cluttering the source file, but ensures we
can write snippets which must collaborate in some way, e.g. by one snippet
identifying a variable which other snippets or surrounding code can use.
Consider ``[[ start open_file {"file": "/etc/issue", "var": "fh"}}``, this
makes it apparent that we can pass ``fh`` to other snippets to operate on
the opened file or reference the ``fh`` handle directly on hand-written
code.
In practice, most parametrization needs are simple enough that a short
JSON value will suffice, if not, consider writing multiple snippets which
calls out to other functions for the common logic.
In practice, most snippets can be sufficiently parametrized

Tip: keep snippets small!
~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -189,21 +182,21 @@ function:
:linenos:
:emphasize-lines: 15
from gcgen.api import snippet, Emitter, Scope, Json
from gcgen.api import snippet, Section, Scope, Json
# These two snippets simply call the generalized function
# with the specific parameters
@snippet("foo")
def s_foo(e: Emitter, s: Scope, v: Json):
def s_foo(sec: Section, s: Scope, v: Json):
if v:
e.emitln(f"foo> hello {v}!")
sec.emitln(f"foo> hello {v}!")
else:
e.emitln("foo> hello!")
sec.emitln("foo> hello!")
@snippet("bar")
def s_bar(e: Emitter, s: Scope, v: Json):
e.emitln("bar> hello!")
s_foo(e, s, "Bar")
def s_bar(sec: Section, s: Scope, v: Json):
sec.emitln("bar> hello!")
s_foo(sec, s, "Bar")
However, now ``s_bar`` will *always* call ``s_foo``, even if ``foo`` is
Expand All @@ -214,21 +207,21 @@ We can instead dynamically resolve the snippet to call using ``get_snippet``:
:linenos:
:emphasize-lines: 15
from gcgen.api import snippet, Emitter, Scope, Json, get_snippet
from gcgen.api import snippet, Section, Scope, Json, get_snippet
# These two snippets simply call the generalized function
# with the specific parameters
@snippet("foo")
def s_foo(e: Emitter, s: Scope, v: Json):
def s_foo(sec: Section, s: Scope, v: Json):
if v:
e.emitln(f"foo> hello {v}!")
sec.emitln(f"foo> hello {v}!")
else:
e.emitln("foo> hello!")
sec.emitln("foo> hello!")
@snippet("bar")
def s_bar(e: Emitter, s: Scope, v: Json):
e.emitln("bar> hello!")
get_snippet(s, "foo", "Bar")(e, s)
def s_bar(sec: Section, s: Scope, v: Json):
sec.emitln("bar> hello!")
get_snippet(s, "foo", "Bar")(sec, s)
Using ``get_snippet``, we thus call whatever the ``foo`` snippet is in the
Expand Down
4 changes: 2 additions & 2 deletions gcgen/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from gcgen.scope import Scope
from gcgen.emitter import Emitter
from gcgen.emitter import Section
from gcgen.decorators import snippet, generator
from gcgen.api.snippets_helpers import get_snippet, SnippetFn
from gcgen.api.write_file import write_file
Expand All @@ -10,7 +10,7 @@
__all__ = [
"Scope",
"SnippetFn",
"Emitter",
"Section",
"snippet",
"generator",
"get_snippet",
Expand Down
9 changes: 7 additions & 2 deletions gcgen/api/snippets_helpers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from gcgen.scope import Scope
from gcgen.emitter import Emitter
from gcgen.decorators import is_snippet
from gcgen.emitter import Section
from gcgen.api.types import Json
from typing import Callable


SnippetFn = Callable[[Emitter, Scope, Json], None]
SnippetFn = Callable[[Section, Scope, Json], None]


unset = object()
Expand All @@ -25,4 +26,8 @@ def get_snippet(scope: Scope, snippet: str, default=unset) -> SnippetFn:
snippet = scope["$snippets"].get(snippet, default=default)
if snippet is unset:
raise KeyError(snippet)
elif not callable(snippet):
raise RuntimeError(f"expected callable, got {type(snippet)} / {repr(snippet)}")
elif not is_snippet(snippet):
raise RuntimeError("callable is not marked as a snippet")
return snippet
Loading

0 comments on commit 32f6622

Please sign in to comment.