Skip to main content

Minimal templating using preprocessor directives

Project description

PPDPY

PPDPY - "Preprocessor Directives for Python" - is a minimal templating library. Use code like preprocessor directives (or compiler instructions) to make a plain text into dynamic template.

Currently there is only one type of directive: conditionals. The conditionals accepts simple logic expressions with boolean symbols.

Installing

$ pip install ppdpy

Quick Start

Suppose you have a template file named "mytemplate.txt" with the following contents:

line 1
#if a or b
line 2
#elif c
line 3
#else
line 4
#endif
line 5

Then this file can be rendered like this:

>>> import ppdpy
>>> with open('mytemplate.txt') as f:
...     template = ppdpy.compile(f)
>>>
>>> print(template.render({'a'}))
line 1
line 2
line 5

>>> print(template.render({'b'}))
line 1
line 2
line 5

>>> print(template.render({'c'}))
line 1
line 3
line 5

>>> print(template.render({}))
line 1
line 4
line 5

The ppdpy module has the following functions:

  • render(file, symbols) render a file using the given set of symbols, and returns the rendered string;
  • renders(text, symbols) render a string using the given set of symbols, and returns the rendered string;
  • compile(file) compiles a file, and returns a template object that can be rendered later;
  • compiles(text) compiles a string, and returns a template object that can be rendered later.

Example use case - SQL

This template engine can be used with any kind of text files, but the main use case this library was implemented for was to manipulate raw SQL files.

PPDPY is minimal, but it can prove itself handy when used with raw SQL queries. The following example outputs queries to be used with pyscopg2 (PostgreSQL).

select channel.id, channel.name, membership.joined_at

#if select_unread_count
    ,(select coalesce(count(*), 0) from messages m
      where m.channel_id = channel.id
        and m.sender_id != chat_user.id
        and (last_read.id IS NULL OR m.sent_at > last_read.sent_at)
    ) as unread_count
#endif

from channel

    inner join membership
    on membership.channel_id = channel.id

    inner join chatuser
    on chat_user.id = membership.chat_user_id

#if select_unread_count
    left join message last_read
    on last_read.id = membership.last_read_id
    and last_read.channel_id = channel.id
#endif

where chat_user.id = %(user_id)s

#if filter_by_status
    and channel.status = %(status_filter)s
#endif

order by
#if order_by_join_date
    membership.joined_at
#elif order_by_readcount
    4
#else
    channel.name
#endif

#if sort_descending
    DESC
#else
    ASC
#endif

Usually there are very complex SQL commands that are hard to write using ORMs. Either ORMs does not have the necessary features to write this kind of queries at all, or the resulting code is unmaintainable - very complicated and unreadable, and certainly very difficult fix or change it later.

And sometimes using Raw SQL is not viable, because ORMs allows us to dynamically change the selected columns, the joined tables, the where filters and the sorting columns and directions. A more "traditional" SQL approach would be to create multiple SQL files (or strings) for the combinations of the desired queries, but you would end up with lots of similar files to do slightly different things (what is also bad for maintainability).

Another solution would be to use raw SQL with "manual" string manipulation. But frankly, this is not nice and also bad for maintainability.

Since PPDPY considers its input as plain text, any kind of text file can be used (not just SQL).

API

The ppdpy module contains the following functions.

def render(file, symbols): receives a file pointer and a set of strings, and returns the rendered string. The set of strings is used to evaluate the expressions in the template, and each string a symbol that computes to True. Symbols are computed to False if they are not present in the strings set.

>>> import ppdpy
>>> with open('testfile.txt') as f:
...     print(ppdpy.render(f, {'test'}))
foobar
test block reached

Other iterable types are accepted on the symbols argument. They are converted to set internally. Also, dictionaries are accepted. In this case the keys of the dictionary will be used as symbols. Internally, it runs symbols = set(symbols.keys()).

def renders(text, symbols): the same as render, but receives a string at the first argument.

>>> print(ppdpy.renders("""foobar
#if test
test block reached
#endif
""", {'test'}))
foobar
test block reached

def compile(file): compiles the contents of a file and returns a Template object, which can be used to render it later.

>>> import ppdpy
>>> with open('testfile.txt') as f:
...     template = ppdpy.compile(f)
>>>
>>> template.render({'test'})
foobar
test block reached

def compiles(text): compiles the given string and returns a Template object.

>>> import ppdpy
>>> template = ppdpy.compiles("""foobar
#if test
test block reached
#endif
""")
>>>
>>> template.render({'test'})
foobar
test block reached
>>> template.render({})
foobar

def set_directive_prefix(prefix): use this to change the directive prefix, if the file type you want to render uses the # char as special (like comments). This function will set the directive prefix globally.

The prefix can be of any length, containing any char of the following: digits, ASCII letters and punctuation (refer to Python's string module). Invisible characters (like spaces, tabs and line breaks) are not allowed.

Template object

The template object only has the following method:

def render(self, symbols): renders the template with the given symbols (set of strings) and returns the rendered string.

Exceptions

ppdpy.exceptions.DirectiveSyntaxError is raised when there are errors related to directives.

ppdpy.exceptions.ExpressionSyntaxError is raised when there are errors related to the boolean expressions.

Directives

All directives starts with # char, and it must be the first visible char in the line to be considered as a directive. For example, all the following lines are considered as directives:

#if something
    #if another_thing
    #else
  #endif
#endif

The available directives are:

  • #if starts a conditional block;
  • #elif is an else-if conditional block, to create composite conditional blocks (optional);
  • #else is the "fallback" conditional block - if none of the previous blocks calculates to True, then this block is rendered (optional);
  • #endif ends a conditional block.

The directives are case insensitive, so #IF something is equal to #if something.

Examples:

Simple conditional block:

#if my_symbol
foobar
#endif

Simple conditional with else block:

#if my_symbol
foobar
#else
noop
#endif

Conditional block with elif:

#if my_symbol
foobar
#elif other_symbol
fuzzbuzz
#endif

Conditional block with elif and else:

#if my_symbol
foobar
#elif other_symbol
fuzzbuzz
#else
noop
#endif

Nested conditional blocks are supported. For example:

#if a and b
    foo
    #if c or d
        bar
    #else
        test
    #endif
#endif

Expressions

The expressions supported by #if and #elif directives are basic boolean logic with these operators: and, or and not. These operators are case insensitive Examples of supported expressions:

  • #if a
  • #if not a
  • #if a and b
  • #if a and not b
  • #if a or b
  • #if a or not b
  • #if a and (b or c)
  • #if a and not (b or c)
  • #IF a AND NOT (b OR c)
  • etc.

The not and and operators has higher precedence, and the or operator has lower precedence. So a and b or c is the same as (a and b) or c, and not a or b is the same as (not a) or b.

The symbols are simple strings, and only the following characters are not considered as part of a symbol: parentheses, whitespace, tabs, linebreaks, and other invisible characters. The symbols are case sensitive.

When rendering, the symbols in the expression are checked against the symbols set passed during the "render" call. For example, when the #if foobar expression is executed, it checks if the "foobar" string is present on the symbols set.

Examples of supported symbols:

  • #if a
  • #if a1
  • #if mysymbol
  • #if MySymbol
  • #if MYSYMBOL
  • #if my_symbol
  • #if my-symbol
  • #if MY_SYMBOL
  • #if MY-SYMBOL

Each of the symbols above is considered a distinct one.

More exotic symbols such as $my@symbol! or {my[symbol]}? can work, but it is advised to not use them since it may change in the future.

Parenthesis in the symbols will produce errors. #if my(symbol) is parsed as #if my ( symbol ), and DirectiveSyntaxError will be thrown.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ppdpy-0.0.1.tar.gz (14.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ppdpy-0.0.1-py3-none-any.whl (13.0 kB view details)

Uploaded Python 3

File details

Details for the file ppdpy-0.0.1.tar.gz.

File metadata

  • Download URL: ppdpy-0.0.1.tar.gz
  • Upload date:
  • Size: 14.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.23.0 setuptools/46.1.3 requests-toolbelt/0.9.1 tqdm/4.46.0 CPython/3.8.0

File hashes

Hashes for ppdpy-0.0.1.tar.gz
Algorithm Hash digest
SHA256 6ffb311e9f2030bec459de41bdc1d1e41a0008e396d1510dbf6fe467b353d5e0
MD5 ce4622eff8408b8375d8f93d557aa389
BLAKE2b-256 bedf9d26f8a6d081b3b168a37225c9ed1051f7c0720c0a29cd6a5c6e9693bc38

See more details on using hashes here.

File details

Details for the file ppdpy-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: ppdpy-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 13.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.23.0 setuptools/46.1.3 requests-toolbelt/0.9.1 tqdm/4.46.0 CPython/3.8.0

File hashes

Hashes for ppdpy-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 6d2d15abb83ece21a47fa0fb553ff99f1a202e5aa5f83049f624dd4de338ecd7
MD5 2a1e05933a53387ea74c0d0960e99e5a
BLAKE2b-256 3c3f8e49a51186974be17f8a04a46bf371ef4669cb06c5125f5554bb860c96b5

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page