Skip to content

Commit

Permalink
API documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
nnym committed Oct 10, 2024
1 parent b3aa353 commit 19c3e2d
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 61 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/web.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: deploy

on:
push:
branches: ["a"]
pull_request:
branches: ["a"]

permissions:
pages: write
id-token: write

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/configure-pages@v4

- uses: actions/setup-python@v5
with:
python-version: 3.13

- name: Generate
run: bt documentation

- uses: actions/upload-pages-artifact@v3
with:
path: .web

- uses: actions/deploy-pages@v4
56 changes: 4 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ This is a **b**uild **t**ool like Make with Python **b**uild **s**cripts.<br>
Python 3.12 is required.<br>
Setting bt up in a project is easy; see the [setup section](#setup).

[**API documentation**](//nnym.github.io/bt)

```py
bt.debug = True

Expand Down Expand Up @@ -50,7 +52,7 @@ foo bar
The execution of bt is always accompanied by a build script.
bt lets the build script run and define [tasks](#tasks) and do any other setup.
When the build script exits, bt takes over.
It looks at the command line arguments that were passed and sets [parameters](#parameter) and determines which tasks to run.
It looks at the command line arguments that were passed and sets [parameters](//nnym.github.io/bt#bt.parameter) and determines which tasks to run.
Before running a task, bt runs all of its [dependencies](#dependencies) which may include tasks and callables.
Since tasks can take long, bt provides facilities for [caching](#cache) them so that they don't have to run every time.

Expand Down Expand Up @@ -110,7 +112,7 @@ def quebec(): print("bar")
3. Run it: `./bs quebec # bar` (`py bs quebec` on Windows).

### Usage
bt takes as arguments names of tasks to run and `name=value` pairs which set [parameters](#parameter) for the build.
bt takes as arguments names of tasks to run and `name=value` pairs which set [parameters](//nnym.github.io/bt#bt.parameter) for the build.

### Tasks
Tasks are functions that can be run on demand from the command line or as [dependencies](#dependencies) of other tasks.<br>
Expand Down Expand Up @@ -359,53 +361,3 @@ $ bt 37
> 37
37 * 37 = 1369
```

### API
[Importing](#library) or [executing](#executable) bt gives a build script direct access to
- module `bt` containing
- variable [`debug`](#debug)
- variable [`current`](#current)
- classes
- [`Arguments`](#arguments)
- [`Files`](#files)
- `Task`
- functions
- [`parameter`](#parameter)
- [`sh`](#sh)
- [`task`](#tasks).

If a name is not listed here, then it should be assumed to be internal.

#### `debug`
This flag determines whether to print debugging information. Currently only names of tasks before they run are printed.

#### `current`
This variable stores the task that is currently running.

#### `parameter`
A parameter `name` can be set to `"value"` by passing `name=value` in the command line.

The function `parameter(name, default = None, require = False)` allows the build script to read parameter values.
If the parameter is not set, then
- if `require`, then an error message is printed and the build is terminated
- otherwise `default` is returned.

#### `sh`
bt exports function `sh` for running shell commands.
`sh` forwards its parameters to `subprocess.run` and sets `shell = True` and `text = True` by default.
If the command line is an [`Arguments`](#arguments), then it is converted into a string.
```py
sh("tr ab ba", input = "abr abz")
```

#### `Arguments`
`Arguments` is a `list` derivative that stores a full or partial command line.
It flattens every added `Iterable`. Supported element types are `str` and `Iterable`.
Its string representation joins its elements with spaces.
Its method `split` splits its string representation into a list.
```py
source = ["main.c"]
exe = "foo"
options = ["-Ofast", "-std=c2x"]
sh(Arguments("gcc", source, "-o", exe, options))
```
49 changes: 40 additions & 9 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import threading
import time
import traceback
import typing
from collections.abc import Iterable, Iterator, Mapping, Sequence
from dataclasses import dataclass
from enum import Enum
Expand All @@ -25,10 +26,17 @@

__version__ = 3
assert __name__ == "bt", f'bt\'s module name is "{__name__}" instead of "bt"'

bt = sys.modules["bt"]
"bt's main module."

type Runnable = Callable[[], Any]
type FileSpecifier = str | Iterable[FileSpecifier] | Callable[[], FileSpecifier]
Runnable = Runnable.__value__
"A function that can be called without arguments."

type FileSpecifier = str | typing.Iterable[FileSpecifier] | Callable[[], FileSpecifier]
FileSpecifier = FileSpecifier.__value__
"""A path or collection of paths."""

class State(Enum):
NORMAL = 0
Expand Down Expand Up @@ -76,6 +84,20 @@ def __add__(this, x):
return this.copy().extend(x)

class Arguments(FlatList):
"""`Arguments` is a `list` derivative that stores a full or partial command line.
Only `None`, strings and `Iterable`s may be added;
`None` is discarded and every `Iterable` is flattened.
```python
source = ["main.c"]
exe = "foo"
options = "-Ofast -std=c2x"
command = Arguments("gcc", source, "-o", exe, options, parameter("o"))
print(command) # gcc main.c -o foo -Ofast -std=c2x
print(command.split()) # ['gcc', 'main.c', '-o', 'foo', '-Ofast', '-std=c2x']
"""

def __init__(this, *arguments):
for arg in arguments: this.append(arg)

Expand All @@ -87,9 +109,13 @@ def transform(this, args):
if isinstance(args, Iterable): return Arguments(*args)
if args: raise TypeError(f"{args!r} is not iterable or a string")

def split(this): return shlex.split(str(this))
def split(this):
"Split this argument list's `__str__` into a list."
return shlex.split(str(this))

def __str__(this): return " ".join(this)
def __str__(this):
"Return all elements joined by spaces."
return " ".join(this)

@dataclass
class Files:
Expand Down Expand Up @@ -180,13 +206,13 @@ def task(*dependencies: str | Task | Runnable, name: Optional[str] = None, defau
"""Declare a task named `name` to be run at most once from the command line or as a dependency.
Each dependency will run before the task.
If `default`, then the task will run when no tasks are specified in the command line.
If `export`, then it will be available in the command line.
If `default`, then the task will run when no tasks are specified in the command line.\n
If `export`, then it will be available in the command line.\n
If `pure`, then dependent tasks may be skipped even if this task runs.
If `source` or `output` is not an empty list or `input` is not `None`, then caching will be enabled.
`source` and `outputs` will be searched for files recursively.
`source` and `output` will be searched for files recursively.
Callables found therein will be converted into their results.
`Iterable`s in `input` that are not `Sequence`s will be replaced by lists.
Expand Down Expand Up @@ -393,15 +419,20 @@ def main():

start()

debug = False
"""Whether to print debugging information.
Currently only names of tasks before they run are printed."""

current: Task = None
"The task that is currently running."

exports = bt, Arguments, Files, Task, parameter, require, read, rm, sh, shout, task, write
exports = {export.__name__: export for export in exports} | {"path": path}
exports = {export.__name__: export for export in exports} | {"FileSpecifier": FileSpecifier, "Runnable": Runnable, "path": path}
__all__ = list(exports.keys())

CACHE = ".bt"

debug = False
tasks: dict[str, Task] = {}
current: Task = None

started = False

Expand Down
5 changes: 5 additions & 0 deletions bs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ def build():
shutil.rmtree("dist", ignore_errors = True)
sh("python -m build")

@task
def documentation(command = "singlehtml"):
rm(".web")
sh("sphinx-build -M", command, "web .web 2>&1")

@task(build)
def publish(): sh("twine upload -r pypi dist/*")

Expand Down
47 changes: 47 additions & 0 deletions web/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import inspect
import os
import re
import sphinx
import sys
import textwrap
from sphinx import application
from sphinx.ext.autodoc import Documenter, FunctionDocumenter, ModuleDocumenter
from typing import TypeAliasType as TypeAlias

project = "bt"
extensions = ["sphinx.ext.autodoc"]
html_theme = "furo"
html_static_path = ["."]
html_css_files = ["style.css"]
autodoc_default_options = {
"members": True,
"special-members": True
}

codeBlock = re.compile(r"```(\w+)\s*?\n([\s\S]*?)(```|$)")
inlineCode = re.compile(r"(`[^`]*?`)(\w*)")

class BtDocumenter(ModuleDocumenter):
def sort_members(this, members: list[tuple[Documenter, bool]], order: str) -> list[tuple[Documenter, bool]]:
super().sort_members(members, order)
members.sort(key = lambda m: {"data": 0, "class": 2, "function": 4}[m[0].objtype] + m[0].name[m[0].name.rindex(":") + 1].isupper())
return members

def skip(app, scope, name, ob, skip, options):
if name in options.get("exclude-members", []): return True
if scope == "module" and not callable(ob) and "__doc__" in dir(ob): return False

if scope == "class":
try: return (skip or (name[0] == "_" and not callable(ob)) or not ob.__doc__
or not os.path.samefile(inspect.getsourcefile(ob), sys.modules["bt"].__file__))
except: return True

def docstring(app, type, name, ob, options, lines):
doc = "\n".join(lines)
doc = codeBlock.sub(lambda m: f".. code-block:: {m[1]}\n\n" + textwrap.indent(m[2], " "), doc)
lines[:] = inlineCode.sub(lambda m: m[1] + "\\" + m[2] if m[2] else m[0], doc).split("\n")

def setup(app: application.Sphinx):
app.add_autodocumenter(BtDocumenter, True)
app.connect("autodoc-skip-member", skip)
app.connect("autodoc-process-docstring", docstring)
12 changes: 12 additions & 0 deletions web/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
This page lists the symbols that `bt` exports.
If the build script is run by the executable, they are made accessible to the build script automatically.
Mutable variables can be accessed through the module `bt`.

If a name is not listed here, then it should be assumed to be internal.

.. autodata:: bt::bt
:no-value:

.. automodule:: bt
:members:
:exclude-members: bt
29 changes: 29 additions & 0 deletions web/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
html {
--width: 80em;
}

.bottom-of-page,
.sidebar-search-container {
display: none
}

.sidebar-drawer {
width: calc(50% - (var(--width) / 2));
}

.content {
width: calc(var(--width) - 6em);
}

.sig-prename {
display: none;
}

cite {
font-family: monospace;
font-style: unset;
}

body:not([data-theme=light]) {
--color-highlight-on-target: #2c2e33
}

0 comments on commit 19c3e2d

Please sign in to comment.