Skip to content

Commit 997f5c9

Browse files
committed
Add function/target documentation based on Sphinx/Jinja2. Replace command line function listings by hyperlink
1 parent 2a9b480 commit 997f5c9

21 files changed

Lines changed: 686 additions & 82 deletions

.github/workflows/docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
python-version: '3.11'
1414
- uses: actions/checkout@v4
1515
- run: python -m pip install .[docs]
16-
- run: python -m sphinx -W -b html docs/ build/html/
16+
- run: python docs/make_bql_doc.py
1717
- uses: actions/upload-pages-artifact@v3
1818
with:
1919
path: build/html

README.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,18 @@ beanquery is a customizable and extensible lightweight SQL query tool
55
that works on tabular data, including `Beancount`__ ledger data.
66

77
__ https://beancount.github.io/
8+
9+
With this tool you can write *queries* to extract information from your
10+
Beancount ledger. This is the documentation of functions and field names
11+
which are available for queries.
12+
13+
Please read the Manual of the `Beancount Query Language (BQL)`__ if you
14+
do not yet know how to write queries.
15+
16+
17+
__ http://furius.ca/beancount/doc/query
18+
19+
After you have learned the BQL, you can use the
20+
`list and documentation of available functions and columns`__
21+
22+
__ https://beancount.github.io/beanquery

beanquery/query_compile.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,14 @@ def __call__(self, context):
406406

407407

408408
class EvalFunction(EvalNode):
409+
"""Base class for function evaluation nodes.
410+
411+
Class Attributes:
412+
__intypes__: List of input parameter types for type checking.
413+
__outtype__: Output type of the function. None means no type
414+
annotation. To annotate that the function returns None, use
415+
types.NoneType.
416+
"""
409417
__slots__ = ('operands',)
410418

411419
# Type constraints on the input arguments.

beanquery/query_env.py

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,6 @@ def _extract_param_names(func):
8080
sig = inspect.signature(func)
8181
param_names = list(sig.parameters.keys())
8282

83-
# Remove 'self' if present (for class methods)
84-
if param_names and param_names[0] == 'self':
85-
param_names = param_names[1:]
86-
8783
return param_names
8884

8985

@@ -154,6 +150,8 @@ def __call__(self, row):
154150
Func.__name__ = name if name is not None else func.__name__
155151
Func.__doc__ = func.__doc__
156152
query_compile.FUNCTIONS[Func.__name__].append(Func)
153+
_add_to_doc_groups(Func, intypes, groups)
154+
157155
return func
158156
return decorator
159157

@@ -173,6 +171,8 @@ def decorator(cls):
173171
if name is not None:
174172
cls.__name__ = name
175173
query_compile.FUNCTIONS[cls.__name__].append(cls)
174+
_add_to_doc_groups(cls, cls.__intypes__, groups)
175+
176176
return cls
177177
return decorator
178178

@@ -857,11 +857,7 @@ def interval(x):
857857

858858
@function([relativedelta, datetime.date, datetime.date], datetime.date, groups = ['date'])
859859
def date_bin(stride, source, origin):
860-
"""Bin a date into the specified stride aligned with the specified origin.
861860

862-
As an extension to the the SQL standard ``date_bin()`` function this
863-
function also accepts strides containing units of months and years.
864-
"""
865861
if stride.months or stride.years:
866862
if origin + stride <= origin:
867863
# FIXME: this should raise and error: stride must be greater than zero
@@ -1067,3 +1063,47 @@ def update(self, store, context):
10671063
cur = store[self.handle]
10681064
if cur is None or value > cur:
10691065
store[self.handle] = value
1066+
1067+
def _describe_functions(functions, aggregates=False, type_filter=None):
1068+
"""Describe functions, optionally filtered by input type category.
1069+
1070+
Args:
1071+
functions: Dictionary of (function name: EvalFunction subclass),
1072+
the actual class, not an object, which would represent a particular
1073+
function call.
1074+
aggregates: If True, show aggregates; if False, show regular functions
1075+
type_filter: Optional filter by input type category (see TYPE_CATEGORIES)
1076+
"""
1077+
# Determine which functions to iterate over
1078+
if type_filter:
1079+
# Use the pre-populated FUNCTION_DOC_GROUPS for filtering
1080+
funcs_to_process = FUNCTION_DOC_GROUPS.get(type_filter, [])
1081+
else:
1082+
# Collect all functions from all groups
1083+
funcs_to_process = []
1084+
for name, funcs in functions.items():
1085+
funcs_to_process.extend(funcs)
1086+
1087+
entries = []
1088+
for func in funcs_to_process:
1089+
# Filter by aggregate vs non-aggregate
1090+
if aggregates != issubclass(func, query_compile.EvalAggregator):
1091+
continue
1092+
1093+
# Get the function name
1094+
name = func.__name__.lower()
1095+
1096+
# Assemble function signature for output using parameter names
1097+
args = ', '.join(f'{param_name}: {types.name(dtype)}'
1098+
for param_name, dtype in zip(func.__param_names__, func.__intypes__))
1099+
1100+
if func.__outtype__:
1101+
outtype = types.name(func.__outtype__)
1102+
else:
1103+
outtype = None
1104+
1105+
doc = func.__doc__ or ''
1106+
entries.append((name, doc, args, outtype))
1107+
1108+
entries.sort()
1109+
return entries

beanquery/shell.py

Lines changed: 18 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -629,69 +629,50 @@ def help_targets(self):
629629
template = textwrap.dedent("""
630630
631631
The list of comma-separated target expressions may consist of columns,
632-
simple functions and aggregate functions. If you use any aggregate
633-
function, you must also provide a GROUP-BY clause.
632+
simple functions and aggregate functions. You can use AS to determine
633+
the output column name, for example:
634634
635-
Columns
636-
-------
635+
SELECT yearmonth(date) AS month ....
637636
638-
{columns}
637+
If you use any aggregate function, you must also provide a GROUP-BY
638+
clause.
639639
640-
Functions
641-
---------
640+
See the online Beanquery documentation for the full list of columns,
641+
functions, and aggregates:
642642
643-
{functions}
644-
645-
Aggregate functions
646-
-------------------
647-
648-
{aggregates}
643+
https://beancount.github.io/beanquery/
649644
650645
""")
651-
print(template.format(**_describe(self.context.tables['postings'],
652-
query_compile.FUNCTIONS)), file=self.outfile)
646+
647+
print(template, file=self.outfile)
653648

654649
def help_from(self):
655650
template = textwrap.dedent("""
656651
657652
A logical expression that consist of columns on directives (mostly
658653
transactions) and simple functions.
659654
660-
Columns
661-
-------
662-
663-
{columns}
664-
665-
Functions
666-
---------
655+
See the online Beanquery documentation for the full list of columns,
656+
functions, and aggregates:
667657
668-
{functions}
658+
https://beancount.github.io/beanquery/
669659
670660
""")
671-
print(template.format(**_describe(self.context.tables['entries'],
672-
query_compile.FUNCTIONS)),
673-
file=self.outfile)
661+
print(template, file=self.outfile)
674662

675663
def help_where(self):
676664
template = textwrap.dedent("""
677665
678666
A logical expression that consist of columns on postings and simple
679667
functions.
680668
681-
Columns
682-
-------
669+
See the online Beanquery documentation for the full list of columns,
670+
functions, and aggregates:
683671
684-
{columns}
685-
686-
Functions
687-
---------
688-
689-
{functions}
672+
https://beancount.github.io/beanquery/
690673
691674
""")
692-
print(template.format(**_describe(self.context.tables['postings'],
693-
query_compile.FUNCTIONS)), file=self.outfile)
694-
675+
print(template, file=self.outfile)
695676

696677
def _describe_columns(columns):
697678
out = io.StringIO()
@@ -702,35 +683,6 @@ def _describe_columns(columns):
702683
print(file=out)
703684
return out.getvalue().rstrip()
704685

705-
706-
def _describe_functions(functions, aggregates=False):
707-
entries = []
708-
for name, funcs in functions.items():
709-
if aggregates != issubclass(funcs[0], query_compile.EvalAggregator):
710-
continue
711-
name = name.lower()
712-
for func in funcs:
713-
args = ', '.join(types.name(d) for d in func.__intypes__)
714-
doc = re.sub(r'[ \n\t]+', ' ', func.__doc__ or '')
715-
entries.append((name, doc, args))
716-
entries.sort()
717-
out = io.StringIO()
718-
wrapper = textwrap.TextWrapper(initial_indent=' ', subsequent_indent=' ', width=80)
719-
for key, entries in itertools.groupby(entries, key=lambda x: x[:2]): # noqa: B020
720-
for name, doc, args in entries:
721-
print(f'{name}({args})', file=out)
722-
print(wrapper.fill(doc), file=out)
723-
print(file=out)
724-
return out.getvalue().rstrip()
725-
726-
727-
def _describe(table, functions):
728-
return dict(
729-
columns=_describe_columns(table.columns),
730-
functions=_describe_functions(functions, aggregates=False),
731-
aggregates=_describe_functions(functions, aggregates=True))
732-
733-
734686
def summary_statistics(entries):
735687
"""Calculate basic summary statistics to output a brief welcome message.
736688

beanquery/sources/beancount.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ class Position(types.Structure):
104104

105105

106106
class Cost(types.Structure):
107+
"""The amount which was payed for a position. This object
108+
saves besides the amount also the date and label (if assigned).
109+
This serves to identify the position to sell according to
110+
a `given booking rule`__.
111+
112+
__ https://beancount.github.io/docs/beancount_language_syntax.html#reducing-positions
113+
"""
107114
name = 'cost'
108115
columns = _typed_namedtuple_to_columns(data.Cost)
109116

@@ -296,7 +303,8 @@ def id(entry):
296303

297304
@columns.register(str)
298305
def type(entry):
299-
"""The data type of the directive."""
306+
"""The data type of the directive. Currently, beanquery only can list
307+
the directives of type 'transaction'."""
300308
return type(entry).__name__.lower()
301309

302310
@columns.register(str)

docs/bql_functions.rst.j2

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
List of all BQL Functions
2+
==========================
3+
4+
This page documents all available BQL functions.
5+
6+
..
7+
Disambiguate functions and types by pretending they are in
8+
different modules (we are documenting BQL with the Sphinx Python
9+
domain). We put functions in the virtual module "fun" (thus
10+
functions must be explicitly linked like :func:`~fun.myfunction`).
11+
Types have no module such that they are auto-linked by the
12+
`.. function:` directives.
13+
14+
.. currentmodule:: fun
15+
16+
{# Render documentation for all functions. This uses the function docstrings
17+
defined in beancount/query_env.py. There are multiple variants of many functions,
18+
differing in the type and number of arguments. We group the variants which
19+
have the same docstrings to render them en bloc. If all variants of a function
20+
of a certain name have the same docstring, we list all signatures directly in
21+
the RST function:: directive. Otherwise, we write .. function:: myfunction(...)
22+
and render code blocks with the signatures, followed by the documentation for
23+
this group of variants.
24+
#}
25+
{% macro render_functions(functions) %}
26+
{% for name, args, has_multiple_docs, docs in preprocess_function_documentation(functions) %}
27+
{% if has_multiple_docs %}
28+
.. function:: {{ name }}(...)
29+
30+
{% for variant in docs %}
31+
::
32+
33+
{{ variant.signatures }}
34+
35+
{{ variant.doc_text | indent(2, first=True) }}
36+
37+
{% endfor %}
38+
{% else %}
39+
.. function::
40+
{% for sig in args %}
41+
{{ sig }}
42+
{% endfor %}
43+
44+
{{ docs | indent(2, first=True) }}
45+
46+
{% endif %}
47+
{% endfor %}
48+
{% endmacro %}
49+
50+
Simple functions
51+
----------------
52+
53+
{{ render_functions(_describe_functions(FUNCTIONS)) }}
54+
55+
Aggregation functions
56+
---------------------
57+
58+
{{ render_functions(_describe_functions(FUNCTIONS, aggregates=True)) }}

docs/columns_entry.rst.j2

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Targets: Entry
2+
==============
3+
4+
This is the list of all the fields you can use in the
5+
SELECT ... clause and the FROM ... clause of a BQL query.
6+
7+
For available functions and aggregates to use with these
8+
columns, see :doc:`functions_index`.
9+
10+
{% for name, type_name, doc in preprocess_targets(EntriesTable) -%}
11+
* **{{ name }}**: ({{ type_name }}) {{ doc }}
12+
13+
{% endfor %}
14+

docs/columns_postings.rst.j2

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Fields: Posting
2+
===============
3+
4+
This is the list of all the fields you can use in the
5+
WHERE ... clause of a BQL query.
6+
7+
For available functions and aggregates to use with these
8+
columns, see :doc:`functions_index`.
9+
10+
{% for name, type_name, doc in preprocess_targets(PostingsTable) -%}
11+
* **{{ name }}**: ({{ type_name }}) {{ doc }}
12+
13+
{% endfor %}
14+

docs/conf.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1+
import beanquery
2+
13
project = 'beanquery'
24
copyright = '2014-2022, beanquery Contributors'
35
author = 'beanquery Contributors'
4-
version = '0.1'
6+
version = beanquery.__version__
57
language = 'en'
68
html_theme = 'furo'
79
html_title = f'{project} {version}'
810
html_logo = 'logo.svg'
911
extensions = [
10-
'sphinx.ext.autodoc',
1112
'sphinx.ext.napoleon',
12-
'sphinx.ext.intersphinx',
13-
'sphinx.ext.extlinks',
14-
'sphinx.ext.githubpages',
13+
'sphinx.ext.autodoc',
14+
#'sphinx.ext.intersphinx',
15+
#'sphinx.ext.extlinks',
16+
#'sphinx.ext.githubpages',
1517
]
1618
extlinks = {
1719
'issue': ('https://github.com/beancount/beanquery/issues/%s', '#'),
@@ -25,3 +27,5 @@
2527
napoleon_use_param = False
2628
autodoc_typehints = 'none'
2729
autodoc_member_order = 'bysource'
30+
# see make_bql_doc.py, we use virtual module "fun" for BQL functions
31+
add_module_names = False

0 commit comments

Comments
 (0)