Lightweight is a "Code over configuration" static site generator.

from lightweight import Site, markdown, paths, jinja, template, rss, atom, sass

def blog_posts(source):
    post_template = template('posts/_template.html')
    # Use globs to select files. # source = 'posts/**.md'
    return (markdown(path, post_template) for path in paths(source))

site = Site(url='')

# Render an index page from Jinja2 template.
site.add('index.html', jinja('pages/index.html'))

# Render markdown blog posts.
[site.add(f'posts/{post.source_path.stem}.html', post) for post in blog_posts('posts/**.md')]
site.add('posts.html', jinja('pages/posts.html'))

# Render SASS to CSS.
site.add('css/style.css', sass('styles/style.scss'))

# Include a copy of a directory.

# Execute all included content.


A simple CLI for lightweight projects:


def dev(host: str, port: int) -> Site:
    site = Site(url=f'http://{host}:{port}/', title='HOME')
    return site

if __name__ == '__main__':
    cli = SiteCli(build=dev, default_port=8081)

This allows to build the project: ./ build --url; and to run the dev server: ./ serve --port 8069

class SiteCli

····def help(self)

····def run(self)

def positional_args_count(func: Callable, equals: int) -> bool

not positional_args_count(func, equals=2): ...



class Content

An abstract content that can be included by a Site.

····def write(self, path: GenPath, ctx: GenContext)

Write the content to the file at path.


class DirectoryCopy

Site content which is a copy of a directory from the path provided as source.

····source: Union[Path, str]

····def write(self, path: GenPath, ctx: GenContext)

class FileCopy

Site content which is a copy of a file from the path provided as source.

····source: Union[Path, str]

····def write(self, path: GenPath, ctx: GenContext)

def copy(path: Union[str, Path])

Copy file or directory at path, ensuring their existence.


Render Jinja templates in place of Lightweight Content.

class JinjaPage

Content rendered from a Jinja Template.

····template: Template

····source_path: Path

····props: Dict[str, Any]

····def render(self, ctx)

····def write(self, path: GenPath, ctx: GenContext)

def from_ctx(func: Callable[GenContext, T]) -> Callable[GenContext, T]

Evaluate the provided function lazily from context at the point of generation.

 from lightweight import jinja, from_ctx


 def post_tasks(ctx: GenContext):
     return [task for task in ctx.tasks if[0] == 'posts']


 site.add('posts', jinja('posts.html', posts=from_ctx(post_tasks)))

def jinja(template_path: Union[str, Path], **props) -> JinjaPage

Renders the page at path with provided parameters.

Templates are resolved from the current directory (cwd).


Lightweight Markdown toolkit.

LwRenderer is an implementation of the mistune.Renderer adding table of contents, and overriding some elements.

class LwRenderer

Renders Markdown overriding the following:

  • links — allows linking to other Markdown pages by their .md file paths.
  • images — adds width=100% to <img/> tags.

Also provides a way to compile a table of contents via LwRenderer.table_of_contents.

····toc: TocBuilder

Rendering a given link or email address.

:param link: link content or email address. :param is_email: whether this is an email or not.

····def block_code(self, code, lang = None)

Rendering block level code. pre > code.

:param code: text content of the code block. :param lang: language of the given code.

····def block_html(self, html)

Rendering block level pure html content.

:param html: text content of the html snippet.

····def block_quote(self, text)

Rendering <blockquote> with the given text.

:param text: text content of the blockquote.

····def codespan(self, text)

Rendering inline code text.

:param text: text content for inline code.

····def double_emphasis(self, text)

Rendering strong text.

:param text: text content for emphasis.

····def emphasis(self, text)

Rendering emphasis text.

:param text: text content for emphasis.

····def escape(self, text)

Rendering escape sequence.

:param text: text content.

····def footnote_item(self, key, text)

Rendering a footnote item.

:param key: identity key for the footnote. :param text: text content of the footnote.

····def footnote_ref(self, key, index)

Rendering the ref anchor of a footnote.

:param key: identity key for the footnote. :param index: the index count of current footnote.

····def footnotes(self, text)

Wrapper for all footnotes.

:param text: contents of all footnotes.

····def header(self, text, level, raw = None)

····def hrule(self)

Rendering method for <hr> tag.

····def image(self, src, title, text)

Rendering a image with title and text.

:param src: source link of the image. :param title: title text of the image. :param text: alt text of the image.

····def inline_html(self, html)

Rendering span level pure html content.

:param html: text content of the html snippet.

····def linebreak(self)

Rendering line break like <br>.

····def list(self, body, ordered = True)

Rendering list tags like <ul> and <ol>.

:param body: body contents of the list. :param ordered: whether this list is ordered or not.

····def list_item(self, text)

Rendering list item snippet. Like <li>.

····def newline(self)

Rendering newline element.

····def paragraph(self, text)

Rendering paragraph tags. Like <p>.

····def placeholder(self)

Returns the default, empty output value for the renderer.

All renderer methods use the '+=' operator to append to this value. Default is a string so rendering HTML can build up a result string with the rendered Markdown.

Can be overridden by Renderer subclasses to be types like an empty list, allowing the renderer to create a tree-like structure to represent the document (which can then be reprocessed later into a separate format like docx or pdf).

····def reset(self)

····def strikethrough(self, text)

Rendering ~~strikethrough~~ text.

:param text: text content for strikethrough.

····def table(self, header, body)

Rendering table element. Wrap header and body in it.

:param header: header part of the table. :param body: body part of the table.

····def table_cell(self, content, **flags)

Rendering a table cell. Like <th> <td>.

:param content: content of current table cell. :param header: whether this is header or not. :param align: align of current table cell.

····def table_of_contents(self, level) -> TableOfContents

····def table_row(self, content)

Rendering a table row. Like <tr>.

:param content: content of current table row.

····def text(self, text)

Rendering unformatted text.

:param text: text content.

class Section

Table of contents item.

····html: str

····id: Optional[str]

····sections: List[Section]

····title: str

····slug: str

class TableOfContents

Table of contents of a Markdown document.

Composed from a list of sections.

The id property is set to the root <ul> element.

····html: str

····id: Optional[str]

····sections: List[Section]

class TocBuilder

Table of Contents builder used by TocMixin.

····entries: List[TocEntry]

····def append(self, entry: TocEntry)

Add an item to the table of contents.

····def compile(self, level) -> TableOfContents

Create a new Table of contents.

class TocEntry

An entry of the TocBuilder.

····count: method_descriptor

····index: method_descriptor

····level: int

····slug: str

····title: str

class TocMixin

A mixin used by LwRenderer compiling a table of contents.

Note: requires calling reset after rendering.

····toc: TocBuilder

····def header(self, text, level, raw = None)

····def reset(self)

····def table_of_contents(self, level) -> TableOfContents


Lightweight Markdown Content.


from lightweight import markdown, template


site.add('hello.html', markdown('posts/', template('templates/post.html')))

class MarkdownPage

Content generated from rendering a markdown file to a Jinja template.

····template: Template

····source_path: Path

····text: str

····renderer: Type[LwRenderer]

····title: Optional[str]

····summary: Optional[str]

····created: Optional[datetime]

····updated: Optional[datetime]

····front_matter: Dict[str, Any]

····props: Dict[str, Any]

····def render(self, ctx: GenContext) -> RenderedMarkdown

Render Markdown to html, extracting the ToC.

····def write(self, path: GenPath, ctx: GenContext)

Writes a rendered Jinja template with rendered Markdown, parameters from front-matter and code to the file provided path.

class RenderedMarkdown

The result of parsing and rendering Markdown.

····html: str

····preview_html: str

····toc: TableOfContents

def markdown(md_path: Union[str, Path], template: Template, renderer = <class 'lightweight.content.lwmd.LwRenderer'>, **kwargs) -> MarkdownPage

Create a markdown page that can be included by a Site. Markdown page is compiled from a markdown file at path (*.md) and a Jinja Template.

Provided key-word arguments are passed as props to the template on render. Such props can also be lazily evaluated from GenContext by using the from_ctx(func) decorator.


SCSS/Sass Lightweight Content.

Allows rendering single file, style directories and corresponding sourcemaps.


from lightweight import sass


site.add('css/style.css', sass('styles/style.scss', sourcemap=False))

class Sass

Content created by compiling Sass and SCSS.

····path: Path

····sourcemap: bool

····def write(self, path: GenPath, ctx: GenContext)

def sass(location: str, sourcemap: bool = True) -> Sass

Run Sass/SCSS compiler on files at location. Can be a file name or a directory.

Sourcemaps are written under "<location>.map".

 site.add('css/style.css', sass('styles/style.scss'))

Creates 2 files: css/styles.css and css/


class AbsolutePathIncluded

····args: getset_descriptor

····with_traceback: method_descriptor

class IncludedDuplicate

····args: getset_descriptor

····with_traceback: method_descriptor

class InvalidCommand

An invalid CLI command.

····args: getset_descriptor

····with_traceback: method_descriptor

class InvalidSiteCliUsage

····args: getset_descriptor

····with_traceback: method_descriptor


Lightweight utilities for working with files.

def directory(location: Union[str, Path])

Execute following statements using the provided location as "cwd" (current working directory).

 from pathlib import Path

 project_location = Path(__file__).absolute().parent

 with directory(project_location):

def paths(pattern: Union[str, Path]) -> List[Path]

List paths matching the provided glob pattern.

 >>> print(paths('lightweight/**/'))
 [PosixPath('lightweight/'), PosixPath('lightweight/content/')]

 >>> print(paths('lightweight/*.typed'))



class GenContext

A generation context. Contains the data useful during the generation: the site and the list of tasks to be executed in the process.

The context is created by a Site upon starting generation and provided to the Content.write(path, ctx) method as a second parameter.

····site: Site

····out: Path

····tasks: Tuple[GenTask, ...]

····generated: datetime

····version: str

····def path(self, p: Union[Path, str]) -> GenPath

Create a new GenPath in this generation context from a regular path.


class GenPath

A path for writing content. It contains both, the relative path (as specified by site.add(relative_path, content)) and the real path (an absolute path which in site’s out).

File system operations performed on real_path; relative path is used for all other operations, e.g. __str__ returns a relative path representation.

Also, proper URL can be obtained from generation path

 site = Site('')
 resources = GenPath(Path('resources'), out='/tmp/out', url_factory=lambda location: site/location)

 teapot: GenPath = resources / 'teapot.txt'

 print(str(teapot))  # resources/teapot.txt
 print(teapot.absolute())  # '/tmp/out/resources/teapot.txt'
 print(teapot.url)  #

 print(teapot.exists())  # False

 # Create a file with text.
 teapot.create('I am a teapot')

 print(teapot.exists())  # True

····location: str

····name: str

····parent: GenPath

····parts: Tuple[str, ...]

····real_path: Path

····suffix: str

····url: str

····relative_path: Path

····out: Path

····url_factory: Callable[str, str]

····def absolute(self) -> Path

An alias of GenPath.real_path.

····def create(self, contents: Union[str, bytes]) -> None

Create a file with provided contents. Contents can be str or bytes.

····def exists(self) -> bool

Checks if file exists

····def mkdir(self, mode = 511, parents = True, exist_ok = True)

Create directory at path.

Differs from the defaults in other Python mkdir signatures: creates whole parent hierarchy of directories if they do not exist.

····def open(self, mode = 'r', buffering = -1, encoding = None, errors = None, newline = None) -> IO[Any]

Open the file. Same as

····def with_name(self, name: str) -> GenPath

Create a new GenPath which differs from the current only by file name.

····def with_suffix(self, suffix: str) -> GenPath

Create a new GenPath with a different file suffix (extension).


class GenTask

A task executed by Site during generation.

All of site’s tasks can be accessed during generation via GenContext.

Generation Task objects differ from the Site’s IncludedContent by having the path of GenPath type (instead of regular Path). GenPath has knowledge of the generation out directory, and is passed directly to content.write(path, ctx).

Includes cwd (current working directory) in which the original content was created. Content write(...) operates from this directory.

····path: GenPath

····ctx: GenContext

····content: Content

····cwd: str

····def execute(self)


class IncludedContent

The content included by a lightweight.Site.

Contains the site’s location and cwd (current working directory) of the content.

Location is a string with an output path relative to generation out directory. It does not include a leading forward slash.

cwd is important for proper subsite generation.

····path: None

····location: str

····content: Content

····cwd: str

····def make_tasks(self, ctx: GenContext) -> List[GenTask]

class Includes

····ics: List[IncludedContent]

····by_cwd: Dict[str, List[IncludedContent]]

····by_location: Dict[str, IncludedContent]

····def add(self, ic: IncludedContent)


Lightweight CLI.

Access CLI help via:

lw --help


python -m lightweight.lw --help

Initialize project using:

lw init example_project --url

Additional help:

lw init --help

Start a server for the project:

lw serve website:dev

Additional help:

lw serve --help

class Color

A color from red, green and blue.

····r: int

····g: int

····b: int

····def bright()

Create a new bright color.

····def css(self, alpha = None) -> str

A string representation of color which can be used in CSS.

class FailedGeneration

····args: getset_descriptor

····with_traceback: method_descriptor

class Generator

····url: str

····def generate(self)

····def load_executable(self)

class Process

····authkey: None

····daemon: None

····exception: None

····exitcode: None

····ident: None

····name: None

····pid: None

····sentinel: None

····traceback: None

····def close(self)

Close the Process object.

This method releases resources held by the Process object. It is an error to call this method if the child process is still running.

····def is_alive(self)

    Return whether process is alive

····def join(self, timeout = None)

    Wait until child process terminates

····def kill(self)

    Terminate process; sends SIGKILL signal or uses TerminateProcess()

····def run(self)

····def start(self)

    Start child process

····def terminate(self)

    Terminate process; sends SIGTERM signal or uses TerminateProcess()

def absolute_out(out: Optional[Path], abs_source: Path) -> Path

def add_init_cli(subparsers)

def add_log_arguments(parser)

def add_version_cli(subparsers)

def argument_parser()

def custom_jinja_tags()

def load_module(p: Path) -> Any

def lw_version()

def main()

def parse_args()

def parse_log_level(value: str)

def positional_args_count(func: Callable, equals: int) -> bool

not positional_args_count(func, equals=2): ...

def quickstart(location: str, title: Optional[str])

def set_log_level(args)

def slugify_title(title)

def start_server(func_file: Path, func_name: str, source: Path, out: Path, host: str, port: int, enable_reload: bool, loop = None)

def sys_path_starting(with_: Path)


Dev HTTP server serving static files using asyncio.


  • Allows to drop ".html" in URLs
    • which corresponds to nginx try_files $uri $uri.html $uri/index.html =404;
  • Can inject live-reload JS to HTML.

Mostly stolen from picoweb -- web pico-framework for Pycopy 2019 MIT

class DevServer

A server serving static files from the provided directory.

 server = DevServer('app/static')
 loop = asyncio.get_event_loop()
 loop.create_task(server.serve('localhost', 8080))
 except KeyboardInterrupt:

····def find_file(self, location: str) -> File

Override to change how path is resolved to file.

····def handle(self, writer: StreamWriter, request: HttpRequest)

Handle the request and write the response.

····def handle_static(self, writer: StreamWriter, request: HttpRequest)

Look for file and write it to the writer. In case the file not found or there are other problems -- write an error.

····def http_error(writer: StreamWriter, status: str)

····def respond(self, reader: StreamReader, writer: StreamWriter)

····def sendfile(self, writer: StreamWriter, file: File)

Override to response with file is put together.

····def serve(self, host, port, loop: BaseEventLoop)

Creates an asyncio coroutine, that serves requests on the provided host and port.

 loop = asyncio.get_event_loop()
 server.serve('localhost', 8080, loop=loop)

····def shutdown(self, loop)

····def start_response(writer: StreamWriter, content_type: str = 'text/html; charset=utf-8', status: str = '200', headers: Optional[Dict[str, str]] = None)

class File

A file that is served via HTTP.

····path: Path

····mime_type: MimeType

····def read(self) -> bytes

class HttpRequest

A request processed by the server.

····headers: Dict[str, str]

····reader: StreamReader

····qs: str

····location: str

····method: str

class LiveReloadServer

····stopped: Event

····def find_file(self, location: str) -> File

Override to change how path is resolved to file.

····def handle(self, writer: StreamWriter, request: HttpRequest)

····def handle_static(self, writer: StreamWriter, request: HttpRequest)

Look for file and write it to the writer. In case the file not found or there are other problems -- write an error.

····def http_error(writer: StreamWriter, status: str)

····def on_source_changed(self)

····def respond(self, reader: StreamReader, writer: StreamWriter)

····def send_live_reload_id(self, writer: StreamWriter)

····def sendfile(self, writer: StreamWriter, file: File)

····def serve(self, host, port, loop)

····def shutdown(self, loop)

····def start_response(writer: StreamWriter, content_type: str = 'text/html; charset=utf-8', status: str = '200', headers: Optional[Dict[str, str]] = None)

····def watch_source(self)

class MimeType

Mime-type of the file written to response Content-Type.

····css: MimeType

····gif: MimeType

····html: MimeType

····jpeg: MimeType

····name: DynamicClassAttribute

····plaintext: MimeType

····png: MimeType

····svg: MimeType

····value: DynamicClassAttribute

def check_directory(working_dir: Path)

def now_repr()

def u(string: str) -> bytes

Shortcut to encode string to bytes.

class Site

A static site for generation, which is basically a collection of Content.

Site is one of the few mutable Lightweight components. It is available to content during write, as a property of the provided ctx.

The only required parameter is the URL of the site. Other parameters may be useful for different content types.

The following code output to the out directory the following content:

  • two rendered Jinja 2 HTML templates;
  • CSS rendered from SCSS;
  • copies of img and js directories.
 site = Site('')

 site.add('index.html', jinja('index.html'))
 site.add('about.html', jinja('about.html'))
 site.add('css/style.css', sass('styles/main.scss'))


····url: str

····content: Includes

····title: Optional[str]

····def add(self, location: str, content: Union[Content, str, None] = None)

Include the content at the location.

Note the content write is executed only upon calling Site.generate().

The location cannot be absolute. It cannot start with a forward slash.

During the add the cwd (current working directory) is recorded. The content’s write will be executed from this directory.

Check overloads for alternative signatures.

····def add(self, location: str)

Include a file, a directory, or multiple files with a glob pattern.

····def add(self, location: str, content: Content)

Include the content at the provided location.

····def add(self, location: str, content: str)

Copy files from content to location.

····def create_ctx(self, out: Path) -> GenContext

Override for custom context types.

····def debug(self, text)

····def generate(self, out: Union[str, Path] = 'out')

Generate the site in directory provided as out.

If the out directory does not exist — it will be created along with its whole hierarchy.

If the out directory already exists – it will be deleted with all of it contents.

····def info(self, text)


This module configures a Jinja environment to use with the app (jinja_env). This environment is configured to locate templates and resolve their inner references according to the current working directory (cwd).

Also a strict undefined is enabled. This means that any operations with an undefined Jinja template parameter will result in an error. This is a safer approach in contrast to Jinja defaults.

template is a shortcut for loading templates using this environment.

object jinja_env

An instance of Environment.

def template(location: Union[str, Path]) -> Template

A shorthand for loading a Jinja2 template from the current working directory.