Skip to content

Commit b7beaf0

Browse files
committed
Add 'help function <type>' subcommand, join amount and commodity functions, as well as position and inventory functions
1 parent 31b8909 commit b7beaf0

File tree

2 files changed

+121
-63
lines changed

2 files changed

+121
-63
lines changed

beanquery/query_env.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def str_(x):
185185
return str(x)
186186

187187

188-
@function([datetime.date], datetime.date, groups=['atomic', 'date'])
188+
@function([datetime.date], datetime.date, name = 'date', groups=['date'])
189189
@function([str], datetime.date, name='date', groups=['atomic', 'date'])
190190
@function([object], datetime.date, name='date', groups=['atomic', 'date'])
191191
def date_(x):
@@ -420,30 +420,30 @@ def open_meta(context, account, key=None):
420420

421421

422422
# Stub kept only for function type checking and for generating documentation.
423-
@function([str], object, groups=['transaction'])
423+
@function([str], object)
424424
def meta(context, key):
425425
"""Get some metadata key of the posting."""
426426
raise NotImplementedError
427427

428428

429429
# Stub kept only for function type checking and for generating documentation.
430-
@function([str], object, groups=['transaction'])
430+
@function([str], object)
431431
def entry_meta(context, key):
432432
"""Get some metadata key of the transaction."""
433433
raise NotImplementedError
434434

435435

436436
# Stub kept only for function type checking and for generating documentation.
437-
@function([str], object, groups=['posting'])
437+
@function([str], object)
438438
def any_meta(context, key):
439439
"""Get metadata from the posting or its parent transaction if not present."""
440440
raise NotImplementedError
441441

442442

443-
@function([str], dict, pass_context=True)
444-
@function([str, str], object, pass_context=True)
445-
@function([str], dict, pass_context=True, name='commodity_meta')
446-
@function([str, str], object, pass_context=True, name='commodity_meta')
443+
@function([str], dict, pass_context=True, groups = ['amount'])
444+
@function([str, str], object, pass_context=True, groups = ['amount'])
445+
@function([str], dict, pass_context=True, name='commodity_meta', groups = ['amount'])
446+
@function([str, str], object, pass_context=True, name='commodity_meta', groups = ['amount'])
447447
def currency_meta(context, commodity, key=None):
448448
"""Get the metadata dict of the commodity directive of the currency."""
449449
entry = context.tables['commodities'].commodities.get(commodity)
@@ -550,10 +550,10 @@ def inventory_value(context, inv, date=None):
550550
return inv.reduce(convert.get_value, price_map, date)
551551

552552

553-
@function([str, str], Decimal, pass_context=True)
553+
@function([str, str], Decimal, pass_context=True, groups = ['position'])
554554
@function([str, str, datetime.date], Decimal, pass_context=True, name='getprice')
555555
def getprice(context, base, quote, date=None):
556-
"""Fetch a price."""
556+
"""Fetch a price. Arguments: Base currency, e.g. 'EUR'; Commodity name (string); Date: Price as of this date. Default: Latest price."""
557557
price_map = context.tables['prices'].price_map
558558
pair = (base.upper(), quote.upper())
559559
_, price = prices.get_price(price_map, pair, date)
@@ -872,7 +872,7 @@ def update(self, store, context):
872872
store[self.handle].add_amount(value)
873873

874874

875-
@aggregator([position.Position], name='sum', groups = ['position', 'inventory'])
875+
@aggregator([position.Position], name='sum', groups = ['position'])
876876
class SumPosition(query_compile.EvalAggregator):
877877
"""Calculate the sum of the position. The result is an Inventory."""
878878
def __init__(self, context, operands):

beanquery/shell.py

Lines changed: 110 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,20 @@
5555
TYPE_CATEGORIES = {
5656
'amount': [amount.Amount],
5757
'account': [], # Must be manually assigned as account names are strings
58-
'position': [position.Position],
59-
'inventory': [inventory.Inventory],
58+
'position': [position.Position, inventory.Inventory],
6059
'date': [datetime.date],
6160
'atomic': [] # fallback, default category
6261
}
6362

63+
# Category information for help display. Tuples: (title, description)
64+
CATEGORY_INFO = {
65+
'amount': ("Amount and Commodity Functions", "An amount is a value with a currency/commodity."),
66+
'account': ("Account Functions", ""),
67+
'position': ("Position & Inventory Functions", "A position is a single amount held at cost.\n\nExample: 10 HOOL {100.30 USD}\n\nA collection of multiple positions is an inventory"),
68+
'date': ("Date Functions", ""),
69+
'atomic': ("Atomic Functions", "Work on basic types: strings, numbers, etc.")
70+
}
71+
6472

6573
class style:
6674
ERROR = '\033[31;1m'
@@ -296,7 +304,27 @@ def completenames(self, text, *ignored):
296304

297305
def do_help(self, arg):
298306
"""List available commands with "help" or detailed help with "help cmd"."""
299-
super().do_help(arg.lower())
307+
if not arg:
308+
super().do_help(arg)
309+
return
310+
311+
# Split arg by space to get command and additional arguments
312+
# e.g. "help functions amount" -> command="functions", args=["amount"]
313+
parts = arg.split()
314+
command = parts[0].lower()
315+
args = parts[1:] if len(parts) > 1 else []
316+
317+
# Check if there's a help method for this command
318+
help_method = getattr(self, f'help_{command}', None)
319+
if help_method and args:
320+
# Call the help method with the additional arguments
321+
try:
322+
help_method(' '.join(args))
323+
except TypeError:
324+
# Fallback if the help method doesn't accept arguments
325+
super().do_help(command)
326+
else:
327+
super().do_help(command)
300328

301329
def do_history(self, arg):
302330
"""Print the command-line history."""
@@ -575,9 +603,9 @@ def on_Select(self, statement):
575603
directive are made available in this context as well. Simple functions
576604
(that return a single value per row) and aggregation functions (that
577605
return a single value per group) are available. For the complete
578-
list of supported columns and functions, see help on "targets".
579-
You can also provide a wildcard here, which will select a reasonable
580-
default set of columns for rendering a journal.
606+
list of supported columns and functions, see help on "targets" and
607+
"functions". You can also provide a wildcard here, which will
608+
select a reasonable default set of columns for rendering a journal.
581609
582610
from_expr: A logical expression that matches on the attributes of
583611
the directives (not postings). This allows you to select a subset of
@@ -654,10 +682,13 @@ def help_targets(self):
654682
655683
{columns}
656684
685+
----------------------------------------------------------------------
686+
657687
For available functions and aggregates, see "help functions".
658688
659689
""")
660-
print(template.format(**_describe_columns(self.context.tables['postings'].columns)),
690+
691+
print(template.format(columns = _describe_columns(self.context.tables['postings'].columns)),
661692
file=self.outfile)
662693

663694
def help_from(self):
@@ -694,51 +725,78 @@ def help_where(self):
694725
print(template.format(columns=_describe_columns(self.context.tables['postings'].columns)),
695726
file=self.outfile)
696727

697-
def help_functions(self):
698-
"""Show all available functions and aggregates grouped by type."""
699-
template = textwrap.dedent("""
700-
701-
Functions are organized by the type of their first argument:
702-
703-
Amount Functions (work on amounts)
704-
--------------------------------
705-
706-
{amount_functions}
707-
708-
Position Functions (work on positions)
709-
------------------------------------
710-
711-
{position_functions}
712-
713-
Inventory Functions (work on inventories)
714-
---------------------------------------
715-
716-
{inventory_functions}
717-
718-
Date Functions (work on dates)
719-
----------------------------
720-
721-
{date_functions}
722-
723-
Atomic Functions (work on basic types: strings, numbers, etc.)
724-
-----------------------------------------------------------
725-
726-
{atomic_functions}
727-
728-
Aggregates
729-
----------
730-
731-
{aggregates}
732-
733-
""")
734-
print(template.format(
735-
amount_functions=_describe_functions(query_compile.FUNCTIONS, aggregates=False, type_filter='amount'),
736-
position_functions=_describe_functions(query_compile.FUNCTIONS, aggregates=False, type_filter='position'),
737-
inventory_functions=_describe_functions(query_compile.FUNCTIONS, aggregates=False, type_filter='inventory'),
738-
date_functions=_describe_functions(query_compile.FUNCTIONS, aggregates=False, type_filter='date'),
739-
atomic_functions=_describe_functions(query_compile.FUNCTIONS, aggregates=False, type_filter='atomic'),
740-
aggregates=_describe_functions(query_compile.FUNCTIONS, aggregates=True)
741-
), file=self.outfile)
728+
def help_functions(self, arg=None):
729+
"""Show all available functions and aggregates grouped by type.
730+
731+
Usage: help functions [type]
732+
733+
Without type argument, shows only category descriptions.
734+
With type argument, shows functions for that specific type.
735+
Available types: amount, position, date, atomic, aggregates
736+
"""
737+
if arg is None:
738+
arg = ''
739+
else:
740+
arg = arg.strip().lower()
741+
742+
743+
if not arg:
744+
# Show only category descriptions when no type specified
745+
sections = []
746+
sections.append("\nUsage: help functions [type]\n")
747+
sections.append("Functions are organized by the type of their main argument:\n")
748+
749+
for category in TYPE_CATEGORIES.keys():
750+
title, description = CATEGORY_INFO.get(category, (f"{category.title()} Functions", ""))
751+
section = f" {category}: {title}"
752+
if description:
753+
section += f" - {description}"
754+
sections.append(section)
755+
756+
# Add aggregates info
757+
sections.append(" aggregates: Aggregation Functions - Functions that compute summary values across groups, as defined by the SELECT ... GROUP BY clause.")
758+
759+
# Format output with proper wrapping and indentation
760+
wrapper = textwrap.TextWrapper(width=80, subsequent_indent=' ')
761+
formatted_sections = []
762+
for section in sections:
763+
if section.startswith(' '):
764+
# For category lines, wrap with proper indentation
765+
wrapped = wrapper.fill(section)
766+
formatted_sections.append(wrapped)
767+
else:
768+
# For headers and usage, keep as is
769+
formatted_sections.append(section)
770+
print('\n'.join(formatted_sections), file=self.outfile)
771+
return
772+
773+
# Show functions for specific type
774+
if arg == 'aggregates':
775+
aggregates_content = _describe_functions(query_compile.FUNCTIONS, aggregates=True)
776+
if aggregates_content.strip():
777+
print("Aggregates\n----------\n", file=self.outfile)
778+
print(aggregates_content, file=self.outfile)
779+
else:
780+
print("No aggregate functions found.", file=self.outfile)
781+
return
782+
783+
if arg not in TYPE_CATEGORIES:
784+
available_types = list(TYPE_CATEGORIES.keys()) + ['aggregates']
785+
available_types = [t for t in available_types if t != 'account'] # Skip account
786+
print(f"Unknown type '{arg}'. Available types: {', '.join(available_types)}", file=self.outfile)
787+
return
788+
789+
title, description = CATEGORY_INFO.get(arg, (f"{arg.title()} Functions", ""))
790+
underline = '-' * len(title)
791+
functions_content = _describe_functions(query_compile.FUNCTIONS, aggregates=False, type_filter=arg)
792+
793+
if functions_content.strip():
794+
print(f"{title}\n{underline}", file=self.outfile)
795+
if description:
796+
print(f"\n{description}", file=self.outfile)
797+
print(f"\n{functions_content}", file=self.outfile)
798+
else:
799+
print(f"No functions found for type '{arg}'.", file=self.outfile)
742800

743801

744802
def _describe_columns(columns):

0 commit comments

Comments
 (0)