Skip to main content

typeset-py

An embedded DSL for defining source code pretty printers.

typeset-py is the Python binding for the typeset Rust crate.

Installation

pip install typeset-soren-n

The package installs the module typeset:

import typeset

layout = typeset.text('hello') + typeset.text('world')
print(typeset.render(typeset.compile(layout), 2, 80))
# hello world

Concept

The layout language is designed such that it fits well over a structurally recursive pass of some inductive data-structure; an abstract representation of the thing you wish to pretty print.

A layout is a tree of text literals composed together with either padded, unpadded compositions or with a line-break. The layout solver will select compositions in a layout and convert them into line-breaks, in order to make the layout fit within a given layout buffer width. It will do this in a greedy way, fitting as many literals on a line as possible. While doing so it will respect the annotated properties that the compositions are constructed under.

The solver being an abstract concept, is concretely implemented via two accompanying functions, a compiler implemented as compile, and a renderer implemented as render. Where the compiler takes a Layout and produces an immutable optimized layout called a Document. The renderer takes a Document along with arguments for indentation and buffer width, and produces the final text output.

Composition

Layouts compose through four families of binary constructors:

pad(left, right)        # "left right"  - separated by a space
unpad(left, right)      # "leftright"   - no separation
line(left, right)       # forced line-break between left and right
fix_pad(left, right)    # padded, and never broken at the seam
fix_unpad(left, right)  # unpadded, and never broken at the seam

The three most common compositions are also available as Python operators on Layout, mirroring the DSL syntax:

a + b   # pad(a, b)
a & b   # unpad(a, b)
a @ b   # line(a, b)

Null constructor

Sometimes in a data-structure there can be optional data (e.g. of type 'string option'), which when omitted should not have a layout. To make this case easy to handle, the null element of layout composition is available.

def layout_option(maybe_string: Optional[str]) -> Layout:
  match maybe_string:
    case None: return null()
    case data: return text(data)

The null will be eliminated from the layout by the compiler, and will not be rendered, e.g:

foobar = text('foo') & null() & text('bar')

When rendering foobar, when the layout fits in the layout buffer, the result will be:

       7
       |
foobar |
       |

Word literal constructor

These are the visible terminals that we are typesetting.

foo = text('foo')

When rendering foo, when the layout fits in the layout buffer, the result will be:

    4
    |
foo |
    |

It will simply overflow the buffer when it does not:

  2
  |
fo|o
  |

Fix constructor

Sometimes you need to render a part of some layout as inline, i.e. that its compositions should not be broken; this is what the fix constructor is for. In other words a fixed layout is treated as a literal.

foobar = fix(text('foo') + text('bar'))

When rendering the fixed layout foobar, when the layout fits in the layout buffer, the result will be:

        8
        |
foo bar |
        |

It will overflow the buffer when it does not:

  2
  |
fo|o bar
  |

Grp constructor

The grp constructor prevents the solver from breaking its compositions, as long as there are compositions to the left of the group which could still be broken. This is useful when you need part of the layout to be treated as an item.

foobarbaz = text('foo') & grp(text('bar') & text('baz'))

When rendering foobarbaz, when the layout fits in the layout buffer, the result will be:

          10
          |
foobarbaz |
          |

If one of the literals does not fit within the layout buffer, the result will be:

       7
       |
foo    |
barbaz |
       |

In contrast, had the group not been annotated, the result would have been:

       7
       |
foobar |
baz    |
       |

Since the composition between bar and baz was not guarded, and the layout solver is greedy and wants to fit as many literals on the same line as possible without overflowing the buffer. If the group still does not fit within the layout buffer, the group will be broken and the result will be:

    4
    |
foo |
bar |
baz |
    |

Seq constructor

The seq constructor forces the solver to break all of its compositions as soon as one of them is broken. This is useful when you have data that is a sequence or is list-like in nature; when one item in the sequence is put on a new line, then so should the rest of the items in the sequence.

foobarbaz = seq(text('foo') & text('bar') & text('baz'))

When rendering foobarbaz, when the layout fits in the layout buffer, the result will be:

          10
          |
foobarbaz |
          |

If one of the literals does not fit within the layout buffer, the result will be:

       7
       |
foo    |
bar    |
baz    |
       |

Since the compositions were part of a sequence; i.e when one of them broke, they all broke.

Nest constructor

The nest constructor is simply there to provide an extra level of indentation for all literals that it ranges over. The width of each level of indentation is given as a parameter to the render function.

foobarbaz = text('foo') & nest(text('bar') & text('baz'))

When rendering foobarbaz with a indentation width of 2, when the layout fits in the layout buffer, the result will be:

          10
          |
foobarbaz |
          |

If one of the literals does not fit within the layout buffer, the result will be;

       7
       |
foobar |
  baz  |
       |

And when the layout buffer will only hold one of the literals, the result will be:

    4
    |
foo |
  ba|r
  ba|z
    |

In this case bar and baz will overflow the layout buffer because of the given indentation.

Pack constructor

The pack constructor defines an indentation level, but implicitly sets the indentation width to the index of the first literal in the layout it annotates. This is e.g. useful if you are pretty printing terms in a lisp-like language, where all other arguments to an application is often 'indented' to the same buffer index as the first argument.

foobarbaz = text('foo') & pack(text('bar') & text('baz'))

When rendering foobarbaz, when the layout fits in the layout buffer, the result will be:

          10
          |
foobarbaz |
          |

When one of the literals do not fit, the result will be:

       7
       |
foobar |
   baz |
       |

When the layout buffer will only hold one literal, the result will be:

    4
    |
foo |
bar |
baz |
    |

The calculation of which buffer index to indent to is:

max((indent_level * indent_width), mark)

I.e the mark index will only be chosen if it is greater than the current indentation.

Forced linebreak composition

The forced linebreak composition does just that, it is a pre-broken composition.

foobar = line(text('foo'), text('bar'))

When rendering foobar, whether or not the layout fits in the layout buffer, the result will be:

        8
        |
foo     |
bar     |
        |

Infix fixed compositions

The infix fixed compositions are syntactic sugar for compositions where the rightmost literal of the left operand, and the leftmost literal of the right operand are fixed together. I.e. the two following layouts are equivalent:

foobarbaz1 = text('foo') + fix_unpad(text('bar'), text('baz'))
foobarbaz2 = text('foo') + fix(text('bar') & text('baz'))

The example above might make it seem trivial, and that infix fixed compositions do not give you much value; but remember that you are composing layouts, not just literals. As such normalising the infix fixed composition is actually quite challenging since there are many different cases to consider when the fix is 'sunk in place' in the layout tree; this is part of what the compiler is responsible for.

Infix fixed compositions are useful when you need to fix a literal to the beginning or end of some other layout, e.g. separators between items in a sequence or list-like data structure. Without this feature you would again need to use an accumulator variable if you want to fix to the next literal, and probably need continuations if you want to fix to the last literal.

Convenience constructors

A set of shorthands for common fragments and idioms:

space()                        # text(' ')
comma()                        # text(',')
semicolon()                    # text(';')
newline()                      # a bare linebreak
blank_line()                   # two linebreaks, i.e. one blank line

join_with(items, separator)    # items joined by separator (unpadded)
join_with_spaces(items)        # 'a b c'
join_with_commas(items)        # 'a, b, c'
join_with_lines(items)         # one item per line

parens(layout)                 # '(layout)'
brackets(layout)               # '[layout]'
braces(layout)                 # '{layout}'

For example a function call:

call = text('f') & parens(join_with_commas([text('a'), text('b')]))
# f(a, b)

Compiling the layout

Your custom layout function (pretty printer) will build a layout, which you then need to compile and render:

document = compile(layout)
result = render(document, 2, 80)
print(result)

I.e. the layout should be given to the compiler, which gives you back a document ready for rendering, which you in turn give to the renderer along with arguments for indentation width and layout buffer width; in the above case indentation width is 2 and the layout buffer width is 80.

The reason for splitting the solver into compile and render, is in case the result is to be displayed in a buffer where the width is variable; i.e. you will not need to re-compile the layout between renderings using varying buffer width.

For one-shot formatting there is also format_layout(layout, 2, 80), which compiles and renders in a single call.

DSL and parsing

Additionally a small DSL has been defined, and a parser implemented, which allow you to write your layouts more succinctly (versus spelling out the full layout tree with the given constructors, which we've so far been doing throughout in this introduction!):

layout = parse('{0} @ null @ {1}', fragment1, fragment2)

The full grammar is as such:

{i}       (Indexed variable for layout fragment substitution with index i)
null      (Constructor for the empty layout)
"x"       (Constructor for a word/text layout literal over a string x)
fix u     (Constructor for a fixed layout over a layout u)
grp u     (Constructor for a group layout over a layout u)
seq u     (Constructor for a sequence layout over a layout u)
nest u    (Constructor for a indented/nested layout over a layout u)
pack u    (Constructor for a indexed margin layout over a layout u)
u @ v     (Forced linebreak composition of layouts u and v)
u @@ v    (Forced double linebreak composition of layouts u and v)
u & v     (Unpadded composition of layouts u and v)
u !& v    (Infix fixed unpadded composition of layouts u and v)
u + v     (Padded composition of layouts u and v)
u !+ v    (Infix fixed padded composition of layouts u and v)

Unary constructors stack (fix grp u) and bind tighter than the binary operators; all binary operators share one precedence level and associate to the right; parentheses group. See docs/context/dsl-grammar.md for the full specification.

Examples

The test suite in tests/test_typeset.py doubles as a set of small, executable examples of every constructor and DSL form.

Release files for typeset-soren-n 3.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for typeset-soren-n 3.0.1
File Size Uploaded
typeset_soren_n-3.0.1.tar.gz 38.1 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for typeset-soren-n 3.0.1
File
typeset_soren_n-3.0.1-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details
typeset_soren_n-3.0.1-cp310-abi3-win32.whl CPython 3.10 abi3 Windows x86-32 Details
typeset_soren_n-3.0.1-cp310-abi3-musllinux_1_2_x86_64.whl CPython 3.10 abi3 Linux musl 1.2+ x86-64 Details
typeset_soren_n-3.0.1-cp310-abi3-musllinux_1_2_armv7l.whl CPython 3.10 abi3 Linux musl 1.2+ ARMv7l Details
typeset_soren_n-3.0.1-cp310-abi3-musllinux_1_2_aarch64.whl CPython 3.10 abi3 Linux musl 1.2+ ARM64 Details
typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details
typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl CPython 3.10 abi3 Linux glibc 2.17+ IBM System/390x Details
typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl CPython 3.10 abi3 Linux glibc 2.17+ PowerPC 64-le Details
typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl CPython 3.10 abi3 Linux glibc 2.17+ ARMv7l Details
typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details
typeset_soren_n-3.0.1-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details
typeset_soren_n-3.0.1-cp310-abi3-macosx_10_12_x86_64.whl CPython 3.10 abi3 macOS 10.12+ x86-64 Details

Total release size: 5.0 MB

Release files / typeset_soren_n-3.0.1.tar.gz

Download URL typeset_soren_n-3.0.1.tar.gz
Size 38.1 kB
Tags Source
SHA-256 checksum
How to use checksums
045dd2cdd5809218beb8c5c2c87103e811d04ba3b33dbd9b275ffcf39a9f456d
BLAKE2b-256 checksum
How to use checksums
02bd902e7b75559f3c557a4c03b10d813052ead7714153f1a9d4674f460d0646
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / typeset_soren_n-3.0.1-cp310-abi3-win_amd64.whl

Download URL typeset_soren_n-3.0.1-cp310-abi3-win_amd64.whl
Size 266.1 kB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
013b22dde6e2bce7984762e311ab54df04f793ee7e6adc0ced2fdb2e0666174e
BLAKE2b-256 checksum
How to use checksums
44da99a10d10289e63cf09ce56ae47002c0369b0c6583715a22aee883fb10a71
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / typeset_soren_n-3.0.1-cp310-abi3-win32.whl

Download URL typeset_soren_n-3.0.1-cp310-abi3-win32.whl
Size 248.7 kB
Tags CPython 3.10 Windows x86-32 abi3
SHA-256 checksum
How to use checksums
c29cb168259b71eb55d58dfaa662c286eab52645b2fa2fab8e9a25222d8a1e15
BLAKE2b-256 checksum
How to use checksums
5537ae72763c52d4f58d3dfb69963c7b69b6688ee12c852276e6fd920c3f3a4b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / typeset_soren_n-3.0.1-cp310-abi3-musllinux_1_2_x86_64.whl

Download URL typeset_soren_n-3.0.1-cp310-abi3-musllinux_1_2_x86_64.whl
Size 589.3 kB
Tags CPython 3.10 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
c8403e11c002475dbe893779b5771996acdd5c8ac80ee04507915a22d18b2a3f
BLAKE2b-256 checksum
How to use checksums
e20865907b0034bff66b816803765afedaafdfc7459b46359c98b75290e42af7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / typeset_soren_n-3.0.1-cp310-abi3-musllinux_1_2_armv7l.whl

Download URL typeset_soren_n-3.0.1-cp310-abi3-musllinux_1_2_armv7l.whl
Size 656.9 kB
Tags CPython 3.10 Linux musl 1.2+ ARMv7l abi3
SHA-256 checksum
How to use checksums
a342614b9d695b17a42626461b32094516650ddf248da25c241767d5f7dae89b
BLAKE2b-256 checksum
How to use checksums
b46d6c5465de4a9ccff5a3489da7641dc9e28bdece2dede75732cc5c693321e8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / typeset_soren_n-3.0.1-cp310-abi3-musllinux_1_2_aarch64.whl

Download URL typeset_soren_n-3.0.1-cp310-abi3-musllinux_1_2_aarch64.whl
Size 549.6 kB
Tags CPython 3.10 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
3e1de545fbbcc886cf789f3bffe988b9ee1883cc446f2ba26df82835e38ed588
BLAKE2b-256 checksum
How to use checksums
0ff80f65ea16fc8b85513e275d157eef65c888f11907be0dc701126f65bb47f8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 377.3 kB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
590666e8789bd4269f063af76c87eca35fa60945c28d659789efc46e208189e6
BLAKE2b-256 checksum
How to use checksums
df6a898cdbf78219301e94e28e6f3e5003320152a1770309202ba1c956ad6a21
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl

Download URL typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl
Size 408.1 kB
Tags CPython 3.10 Linux glibc 2.17+ IBM System/390x abi3
SHA-256 checksum
How to use checksums
83b0e8959402060234a1d43e2804bf9c596061cb9482306ea27f4faaf2aa5f4f
BLAKE2b-256 checksum
How to use checksums
5810b154b8e99e44fed25b139e71b5361b40f0e3fdcd3c367612398dbf8da8fe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl

Download URL typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl
Size 411.2 kB
Tags CPython 3.10 Linux glibc 2.17+ PowerPC 64-le abi3
SHA-256 checksum
How to use checksums
fba3e623fd08751365e352c740a277ede431e129b43a3b70d95b6a66940767a2
BLAKE2b-256 checksum
How to use checksums
d9526e5a6a215bf80f232c8ed8f8eb25e06b82d4649f24b2148522e6760780b7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl

Download URL typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
Size 382.2 kB
Tags CPython 3.10 Linux glibc 2.17+ ARMv7l abi3
SHA-256 checksum
How to use checksums
dc9eca20d0c4e765eaaadf92fb0d30ebfb24b42860cd775a43d8318338be7d9d
BLAKE2b-256 checksum
How to use checksums
77c744b890956650ecefd2c933ec317692ff89dd0baf107f71aaf1a266f32a11
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL typeset_soren_n-3.0.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 373.4 kB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
fb34edc73fb53cd78771a24b009924c670df6b58c52aa48c7559d73345203996
BLAKE2b-256 checksum
How to use checksums
aeed5faa945fad92d34b9f180f57951b524bde157f7f06dde2905ba3f6609bca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / typeset_soren_n-3.0.1-cp310-abi3-macosx_11_0_arm64.whl

Download URL typeset_soren_n-3.0.1-cp310-abi3-macosx_11_0_arm64.whl
Size 346.9 kB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
4346d01f1d62f3a5c864d3bcbc592538d08a5dda2c633a8dc96763f33891f201
BLAKE2b-256 checksum
How to use checksums
080b3dc038eb9af37ad37cfa9be29667a7c4d3106c5a15f323346a775d632153
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release files / typeset_soren_n-3.0.1-cp310-abi3-macosx_10_12_x86_64.whl

Download URL typeset_soren_n-3.0.1-cp310-abi3-macosx_10_12_x86_64.whl
Size 357.3 kB
Tags CPython 3.10 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
bd0619a29303d4b689f13a2566f6bd1d002d3bef7a02541a0a5c5adff8640541
BLAKE2b-256 checksum
How to use checksums
12b2cc24300a4964151118ead5cf610ad9aceda57f63ea4b15b96a33ad52cb6d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via maturin/1.14.1

Release history Release notifications | RSS feed

4.0.0

13 release files

This release

3.0.1 This release

13 release files

3.0.0

73 release files

2.1.7

81 release files

2.1.6

99 release files

2.1.5

99 release files

2.1.4

99 release files

2.1.3

99 release files

2.0.6

65 release files

2.0.4

76 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page