From 3747424950bb2e3d4e7eb1a079ed5f079d451255 Mon Sep 17 00:00:00 2001 From: teh_coderer Date: Wed, 15 Feb 2023 16:49:07 -0600 Subject: [PATCH] Update SDK Docs Generation (#4262) * update generate_sdk_markdown.py to use sdk trailmap class * Update trailmap.py * Update trailmap.py --- generate_sdk.py | 11 +- openbb_terminal/forecast/forecast_view.py | 44 ++++ openbb_terminal/sdk.py | 3 + .../sdk_core/models/forecast_sdk_model.py | 4 +- .../sdk_core/trail_map_forecasting.csv | 2 +- openbb_terminal/sdk_core/trailmap.py | 137 +++++++----- tests/website/test_generate_docs.py | 36 +-- website/generate_sdk_markdown.py | 210 ++---------------- 8 files changed, 180 insertions(+), 267 deletions(-) diff --git a/generate_sdk.py b/generate_sdk.py index 75940b4923ca..811bb8a4a2c1 100644 --- a/generate_sdk.py +++ b/generate_sdk.py @@ -528,13 +528,12 @@ def generate_sdk(sort: bool = False) -> None: if __name__ == "__main__": - sys_args = sys.argv - sort = False - if len(sys_args) > 1: - if sys_args[1] == "sort": - sort = True + sort_csv = False + if len(sys.argv) > 1: + if sys.argv[1] == "sort": + sort_csv = True console.print("\n\n[bright_magenta]Sorting CSV...[/]\n") else: console.print("[red]Invalid argument.\n Accepted arguments: sort[/]") - generate_sdk(sort) + generate_sdk(sort_csv) diff --git a/openbb_terminal/forecast/forecast_view.py b/openbb_terminal/forecast/forecast_view.py index ac7af5e13f46..3d3140aececc 100644 --- a/openbb_terminal/forecast/forecast_view.py +++ b/openbb_terminal/forecast/forecast_view.py @@ -249,6 +249,23 @@ def show_df( export: str = "", sheet_name: str = None, ): + """Show a dataframe in a table + + Parameters + ---------- + data: pd.DataFrame + The dataframe to show + limit: int + The number of rows to show + limit_col: int + The number of columns to show + name: str + The name of the dataframe + export: str + Format to export data + sheet_name: str + Optionally specify the name of the sheet the data is exported to. + """ console.print( f"[green]{name} dataset has shape (row, column): {data.shape}\n[/green]" ) @@ -279,6 +296,19 @@ def show_df( def describe_df( data: pd.DataFrame, name: str = "", export: str = "", sheet_name: str = None ): + """Show descriptive statistics for a dataframe + + Parameters + ---------- + data: pd.DataFrame + The dataframe to show + name: str + The name of the dataframe + export: str + Format to export data + sheet_name: str + Optionally specify the name of the sheet the data is exported to. + """ new_df = forecast_model.describe_df(data) print_rich_table( new_df, @@ -299,6 +329,20 @@ def describe_df( def export_df( data: pd.DataFrame, export: str, name: str = "", sheet_name: str = None ) -> None: + """Export a dataframe to a file + + Parameters + ---------- + data: pd.DataFrame + The dataframe to export + export: str + The format to export the dataframe to + name: str + The name of the dataframe + sheet_name: str + Optionally specify the name of the sheet the data is exported to. + """ + export_data( export, os.path.dirname(os.path.abspath(__file__)), diff --git a/openbb_terminal/sdk.py b/openbb_terminal/sdk.py index 985f17210257..f040c52c44bc 100644 --- a/openbb_terminal/sdk.py +++ b/openbb_terminal/sdk.py @@ -217,9 +217,11 @@ def forecast(self): `delete`: Delete a column from a dataframe\n `delta`: Calculate the %change of a variable based on a specific column\n `desc`: Returns statistics for a given df\n + `desc_chart`: Show descriptive statistics for a dataframe\n `ema`: A moving average provides an indication of the trend of the price movement\n `expo`: Performs Probabilistic Exponential Smoothing forecasting\n `expo_chart`: Display Probabilistic Exponential Smoothing forecast\n + `export`: Export a dataframe to a file\n `linregr`: Perform Linear Regression Forecasting\n `linregr_chart`: Display Linear Regression Forecasting\n `load`: Load custom file into dataframe.\n @@ -244,6 +246,7 @@ def forecast(self): `season_chart`: Plot seasonality from a dataset\n `seasonalnaive`: Performs Seasonal Naive forecasting\n `seasonalnaive_chart`: Display SeasonalNaive Model\n + `show`: Show a dataframe in a table\n `signal`: A price signal based on short/long term price.\n `sto`: Stochastic Oscillator %K and %D : A stochastic oscillator is a momentum indicator comparing a particular closing\n `tcn`: Perform TCN forecasting\n diff --git a/openbb_terminal/sdk_core/models/forecast_sdk_model.py b/openbb_terminal/sdk_core/models/forecast_sdk_model.py index 29733b47f86c..cfc9bb320cda 100644 --- a/openbb_terminal/sdk_core/models/forecast_sdk_model.py +++ b/openbb_terminal/sdk_core/models/forecast_sdk_model.py @@ -28,9 +28,11 @@ class ForecastRoot(Category): `delete`: Delete a column from a dataframe\n `delta`: Calculate the %change of a variable based on a specific column\n `desc`: Returns statistics for a given df\n + `desc_chart`: Show descriptive statistics for a dataframe\n `ema`: A moving average provides an indication of the trend of the price movement\n `expo`: Performs Probabilistic Exponential Smoothing forecasting\n `expo_chart`: Display Probabilistic Exponential Smoothing forecast\n + `export`: Export a dataframe to a file\n `linregr`: Perform Linear Regression Forecasting\n `linregr_chart`: Display Linear Regression Forecasting\n `load`: Load custom file into dataframe.\n @@ -55,6 +57,7 @@ class ForecastRoot(Category): `season_chart`: Plot seasonality from a dataset\n `seasonalnaive`: Performs Seasonal Naive forecasting\n `seasonalnaive_chart`: Display SeasonalNaive Model\n + `show`: Show a dataframe in a table\n `signal`: A price signal based on short/long term price.\n `sto`: Stochastic Oscillator %K and %D : A stochastic oscillator is a momentum indicator comparing a particular closing\n `tcn`: Perform TCN forecasting\n @@ -136,7 +139,6 @@ def __init__(self): lib.forecast_seasonalnaive_view.display_seasonalnaive_forecast ) self.show = lib.forecast_view.show_df - self.show_chart = lib.forecast_view.show_df self.signal = lib.forecast_model.add_signal self.sto = lib.forecast_model.add_sto self.tcn = lib.forecast_tcn_model.get_tcn_data diff --git a/openbb_terminal/sdk_core/trail_map_forecasting.csv b/openbb_terminal/sdk_core/trail_map_forecasting.csv index 8fbfd6f5014f..3fabf7d3d257 100644 --- a/openbb_terminal/sdk_core/trail_map_forecasting.csv +++ b/openbb_terminal/sdk_core/trail_map_forecasting.csv @@ -30,7 +30,7 @@ forecast.rsi,forecast_model.add_rsi, forecast.rwd,forecast_rwd_model.get_rwd_data,forecast_rwd_view.display_rwd_forecast forecast.season,,forecast_view.display_seasonality forecast.seasonalnaive,forecast_seasonalnaive_model.get_seasonalnaive_data,forecast_seasonalnaive_view.display_seasonalnaive_forecast -forecast.show,forecast_view.show_df,forecast_view.show_df +forecast.show,forecast_view.show_df, forecast.signal,forecast_model.add_signal, forecast.sto,forecast_model.add_sto, forecast.tcn,forecast_tcn_model.get_tcn_data,forecast_tcn_view.display_tcn_forecast diff --git a/openbb_terminal/sdk_core/trailmap.py b/openbb_terminal/sdk_core/trailmap.py index 6d83b5bd0835..81831e8582c9 100644 --- a/openbb_terminal/sdk_core/trailmap.py +++ b/openbb_terminal/sdk_core/trailmap.py @@ -1,22 +1,44 @@ -import importlib import inspect from pathlib import Path from types import FunctionType -from typing import Dict, List, Optional +from typing import Any, Callable, Dict, ForwardRef, List, Optional import pandas as pd +import openbb_terminal.sdk_core.sdk_init as lib from openbb_terminal.core.config.paths import PACKAGE_DIRECTORY from openbb_terminal.rich_config import console from openbb_terminal.sdk_core.sdk_helpers import clean_attr_desc -from openbb_terminal.sdk_core.sdk_init import ( - FORECASTING_TOOLKIT_ENABLED, - OPTIMIZATION_TOOLKIT_ENABLED, -) + + +def get_signature_parameters( + function: Callable[..., Any], globalns: Dict[str, Any] +) -> Dict[str, inspect.Parameter]: + signature = inspect.signature(function) + params = {} + cache: dict[str, Any] = {} + for name, parameter in signature.parameters.items(): + annotation = parameter.annotation + if annotation is parameter.empty: + params[name] = parameter + continue + if annotation is None: + params[name] = parameter.replace(annotation=type(None)) + continue + + if isinstance(annotation, ForwardRef): + annotation = annotation.__forward_arg__ + + if isinstance(annotation, str): + annotation = eval(annotation, globalns, cache) # pylint: disable=W0123 + + params[name] = parameter.replace(annotation=annotation) + + return params class FuncAttr: - def __init__(self, func: Optional[str] = None): + def __init__(self) -> None: self.short_doc: Optional[str] = None self.long_doc: Optional[str] = None self.func_def: Optional[str] = None @@ -25,15 +47,11 @@ def __init__(self, func: Optional[str] = None): self.full_path: Optional[str] = None self.func_unwrapped: Optional[FunctionType] = None self.func_wrapped: Optional[FunctionType] = None - if func: - self.get_func_attrs(func) + self.params: Dict[str, inspect.Parameter] = {} def get_func_attrs(self, func: str) -> None: - attr = getattr( - importlib.import_module("openbb_terminal.sdk_core.sdk_init"), - func.split(".")[0], - ) - func_attr = getattr(attr, func.split(".")[1]) + module_path, function_name = func.rsplit(".", 1) + func_attr = getattr(getattr(lib, module_path), function_name) self.func_wrapped = func_attr add_lineon = 0 @@ -45,50 +63,63 @@ def get_func_attrs(self, func: str) -> None: self.func_unwrapped = func_attr self.lineon = inspect.getsourcelines(func_attr)[1] + add_lineon - self.func_def = self.get_definition() self.long_doc = func_attr.__doc__ self.short_doc = clean_attr_desc(func_attr) + for k, p in get_signature_parameters(func_attr, func_attr.__globals__).items(): + self.params[k] = p + self.path = inspect.getfile(func_attr) full_path = ( inspect.getfile(func_attr).replace("\\", "/").split("openbb_terminal/")[1] ) self.full_path = f"openbb_terminal/{full_path}" - def get_definition(self) -> str: + def get_definition( + self, location_path: list, class_attr: str, view: bool = False + ) -> str: """Creates the function definition to be used in SDK docs.""" - funcspec = inspect.getfullargspec(self.func_unwrapped) - + funcspec = self.params definition = "" - added_comma = False - for arg in funcspec.args: + for arg, param in funcspec.items(): annotation = ( - funcspec.annotations[arg] if arg in funcspec.annotations else "Any" + ( + str(param.annotation) + .replace("", "") + .replace("typing.", "") + .replace("pandas.core.frame.", "pd.") + .replace("pandas.core.series.", "pd.") + .replace("openbb_terminal.portfolio.", "") + ) + if param.annotation != inspect.Parameter.empty + else "Any" ) - annotation = ( - str(annotation) - .replace("", "") - .replace("typing.", "") - .replace("pandas.core.frame.", "pd.") - .replace("pandas.core.series.", "pd.") - .replace("openbb_terminal.portfolio.", "") - ) - definition += f"{arg}: {annotation}, " - added_comma = True - - if added_comma: - definition = definition[:-2] - - return_def = ( - funcspec.annotations["return"].__name__ - if "return" in funcspec.annotations - and hasattr(funcspec.annotations["return"], "__name__") - and funcspec.annotations["return"] is not None - else "None" - ) - definition = f"def {self.func_unwrapped.__name__}({definition }) -> {return_def}" # type: ignore - return definition + + default = "" + if param.default is not param.empty: + arg_default = ( + param.default + if param.default is not inspect.Parameter.empty + else "None" + ) + default = ( + f" = {arg_default}" + if not isinstance(arg_default, str) + else f' = "{arg_default}"' + ) + definition += f"{arg}: {annotation}{default}, " + + definition = definition.rstrip(", ") + + if location_path[0] == "root": + location_path[0] = "" + + trail = ".".join([t for t in location_path if t != ""]) + sdk_name = class_attr if not view else f"{class_attr}_chart" + sdk_path = f"{f'openbb.{trail}' if trail else 'openbb'}.{sdk_name}" + + return f"{sdk_path}({definition })" # pylint: disable=R0903 @@ -106,8 +137,14 @@ def __init__(self, trailmap: str, model: str, view: Optional[str] = None): self.model_func: Optional[str] = f"lib.{model}" if model else None self.view_func: Optional[str] = f"lib.{view}" if view else None self.func_attrs: Dict[str, FuncAttr] = {} - self.func_attrs["model"] = FuncAttr(self.model) - self.func_attrs["view"] = FuncAttr(self.view) # type: ignore + self.func_attrs["model"] = FuncAttr() + self.func_attrs["view"] = FuncAttr() + for k, cls in self.func_attrs.items(): + if getattr(self, f"{k}_func"): + cls.get_func_attrs(getattr(self, f"{k}")) + cls.func_def = cls.get_definition( + tmap, self.class_attr, view=k == "view" + ) def get_trailmaps(sort: bool = False) -> List[Trailmap]: @@ -119,7 +156,7 @@ def get_trailmaps(sort: bool = False) -> List[Trailmap]: PACKAGE_DIRECTORY / "sdk_core" / "trail_map_optimization.csv" ) - def load_csv(path: Path = None) -> pd.DataFrame: + def load_csv(path: Optional[Path] = None) -> Dict[str, Dict[str, str]]: path = path or MAP_PATH df = pd.read_csv(path, keep_default_na=False) df = df.set_index("trail") @@ -139,13 +176,13 @@ def print_error(error: str) -> None: def load(): map_dict = load_csv(path=MAP_PATH) - if FORECASTING_TOOLKIT_ENABLED: + if lib.FORECASTING_TOOLKIT_ENABLED: map_forecasting_dict = load_csv(path=MAP_FORECASTING_PATH) map_dict.update(map_forecasting_dict) else: print_error("Forecasting") - if OPTIMIZATION_TOOLKIT_ENABLED: + if lib.OPTIMIZATION_TOOLKIT_ENABLED: map_optimization_dict = load_csv(path=MAP_OPTIMIZATION_PATH) map_dict.update(map_optimization_dict) else: diff --git a/tests/website/test_generate_docs.py b/tests/website/test_generate_docs.py index e4a496420ead..fa99b6ce7285 100644 --- a/tests/website/test_generate_docs.py +++ b/tests/website/test_generate_docs.py @@ -3,6 +3,7 @@ import pytest try: + from openbb_terminal.sdk_core.trailmap import get_signature_parameters from website import ( generate_sdk_markdown as gen_sdk, generate_terminal_markdown as gen_term, @@ -42,25 +43,30 @@ def mock_func(arg1: Optional[str] = "Test", arg2: Optional[bool] = True) -> bool # pylint:disable=too-few-public-methods +class MockFuncAttrs: + """Mock function attributes""" + + def __init__(self): + self.lineon = 69 + self.full_path = "test/mock_func.py" + self.long_doc = mock_func.__doc__ + self.func_unwrapped = mock_func + self.func_def = ( + 'openbb.mock(arg1: Optional[str] = "Test", arg2: Optional[bool] = True)' + ) + self.params = {} + for k, p in get_signature_parameters(mock_func, mock_func.__globals__).items(): + self.params[k] = p + + class MockTrailMap: """Mock trail map""" def __init__(self): - self.trailmap = "" - self.func_attr = {"model": mock_func} - self.class_attr = {"model": "mock"} - self.lineon = {"model": 69} - self.full_path = {"model": "test/mock_func.py"} - self.model = "mock_func" - self.long_doc = {"model": mock_func.__doc__} - self.func_def = { - "model": 'openbb.mock(arg1: Optional[str] = "Test", arg2: Optional[bool] = True)' - } - self.params = {"model": {}} - for k, p in gen_sdk.get_signature_parameters( - mock_func, mock_func.__globals__ - ).items(): - self.params["model"][k] = p + self.func_attrs = {} + self.func_attrs["model"] = MockFuncAttrs() + self.model = mock_func + self.class_attr = "mock" EXPECTED_OUTPUT = """Stuff here or stuff there, it doesn't matter, it's everywhere. diff --git a/website/generate_sdk_markdown.py b/website/generate_sdk_markdown.py index 9b15f73b7995..bfb56551292f 100644 --- a/website/generate_sdk_markdown.py +++ b/website/generate_sdk_markdown.py @@ -1,210 +1,27 @@ -import csv # noqa: I001 -import importlib import inspect import json import os import shutil from pathlib import Path -from types import FunctionType -from typing import Any, Callable, Dict, ForwardRef, List, Literal, Optional, Union +from typing import Any, Dict, Literal, Union -import pandas as pd from docstring_parser import parse -from openbb_terminal.core.library.trail_map import ( - FORECASTING_TOOLKIT_ENABLED as FORECASTING, - MISCELLANEOUS_DIRECTORY, - OPTIMIZATION_TOOLKIT_ENABLED as OPTIMIZATION, -) -from openbb_terminal.rich_config import console +from openbb_terminal.sdk_core.trailmap import Trailmap, get_trailmaps from website.controller_doc_classes import sub_names_full as subnames -MAP_PATH = MISCELLANEOUS_DIRECTORY / "library" / "trail_map.csv" -MAP_FORECASTING_PATH = MISCELLANEOUS_DIRECTORY / "library" / "trail_map_forecasting.csv" -MAP_OPTIMIZATION_PATH = ( - MISCELLANEOUS_DIRECTORY / "library" / "trail_map_optimization.csv" -) website_path = Path(__file__).parent.absolute() -def clean_attr_desc(attr: Optional[FunctionType] = None) -> Optional[str]: - """Clean the attribute description.""" - if attr.__doc__ is None: - return None - return ( - attr.__doc__.splitlines()[1].lstrip() - if not attr.__doc__.splitlines()[0] - else attr.__doc__.splitlines()[0].lstrip() - if attr.__doc__ - else "" - ) - - -def get_signature_parameters( - function: Callable[..., Any], globalns: Dict[str, Any] -) -> Dict[str, inspect.Parameter]: - signature = inspect.signature(function) - params = {} - cache: dict[str, Any] = {} - for name, parameter in signature.parameters.items(): - annotation = parameter.annotation - if annotation is parameter.empty: - params[name] = parameter - continue - if annotation is None: - params[name] = parameter.replace(annotation=type(None)) - continue - - if isinstance(annotation, ForwardRef): - annotation = annotation.__forward_arg__ - - if isinstance(annotation, str): - annotation = eval(annotation, globalns, cache) # pylint: disable=W0123 - - params[name] = parameter.replace(annotation=annotation) - - return params - - -class Trailmap: - def __init__(self, trailmap: str, model: str, view: Optional[str] = None): - tmap = trailmap.split(".") - if len(tmap) == 1: - tmap = ["", tmap[0]] - self.class_attr: str = tmap.pop(-1) - self.location_path = tmap - self.model = model - self.view = view if view else None - self.short_doc: Dict[str, Optional[str]] = {} - self.long_doc: Dict[str, str] = {} - self.lineon: Dict[str, int] = {} - self.full_path: Dict[str, str] = {} - self.func_def: Dict[str, str] = {} - self.func_attr: Dict[str, FunctionType] = {} - self.params: Dict[str, Dict[str, inspect.Parameter]] = {} - self.get_docstrings() - - def get_docstrings(self) -> None: - """Gets the function docstrings. We get the short and long docstrings.""" - - for key, func in zip(["model", "view"], [self.model, self.view]): - if func: - module_path, function_name = func.rsplit(".", 1) - module = importlib.import_module(module_path) - - func_attr = getattr(module, function_name) - add_juan = 0 - if hasattr(func_attr, "__wrapped__"): - func_attr = func_attr.__wrapped__ - if hasattr(func_attr, "__wrapped__"): - func_attr = func_attr.__wrapped__ - add_juan = 1 - - self.func_attr[key] = func_attr - self.lineon[key] = inspect.getsourcelines(func_attr)[1] + add_juan - - self.long_doc[key] = func_attr.__doc__ - self.short_doc[key] = clean_attr_desc(func_attr) - - self.params[key] = {} - for k, p in get_signature_parameters( - func_attr, func_attr.__globals__ - ).items(): - self.params[key][k] = p - - self.func_def[key] = self.get_definition(key) - full_path = ( - inspect.getfile(self.func_attr[key]) - .replace("\\", "/") - .split("openbb_terminal/")[1] - ) - self.full_path[key] = f"openbb_terminal/{full_path}" - - def get_definition(self, key: str) -> str: - """Creates the function definition to be used in SDK docs.""" - funcspec = self.params[key] - definition = "" - added_comma = False - for arg in funcspec: - annotation = ( - ( - str(funcspec[arg].annotation) - .replace("", "") - .replace("typing.", "") - .replace("pandas.core.frame.", "pd.") - .replace("pandas.core.series.", "pd.") - .replace("openbb_terminal.portfolio.", "") - ) - if funcspec[arg].annotation != inspect.Parameter.empty - else "Any" - ) - - default = "" - if funcspec[arg].default is not funcspec[arg].empty: - arg_default = ( - funcspec[arg].default - if funcspec[arg].default is not inspect.Parameter.empty - else "None" - ) - default = ( - f" = {arg_default}" - if not isinstance(arg_default, str) - else f' = "{arg_default}"' - ) - definition += f"{arg}: {annotation}{default}, " - added_comma = True - - if added_comma: - definition = definition[:-2] - - trail = ".".join([t for t in self.location_path if t != ""]) - sdk_name = self.class_attr if key != "view" else f"{self.class_attr}_chart" - sdk_path = f"{f'openbb.{trail}' if trail else 'openbb'}.{sdk_name}" - - definition = f"{sdk_path}({definition })" - return definition - - -def get_trailmaps() -> List[Trailmap]: - trailmaps = [] - - def sort_csv(path: Path) -> None: - columns = ["trail", "model", "view"] - df = pd.read_csv(path, usecols=columns, keep_default_na=False) - df.set_index("trail", inplace=True) - df.sort_index(inplace=True) - df.to_csv(path, index=True) - - for tmap_csv in [MAP_PATH, MAP_FORECASTING_PATH, MAP_OPTIMIZATION_PATH]: - sort_csv(tmap_csv) - if tmap_csv == MAP_FORECASTING_PATH and not FORECASTING: - console.print( - "[bold red]Forecasting is disabled. Forecasting will not be included in the Generation of Docs[/bold red]" - ) - break - if tmap_csv == MAP_OPTIMIZATION_PATH and not OPTIMIZATION: - console.print( - "[bold red]Optimization is disabled. Optimization will not be included in the Generation of Docs[/bold red]" # noqa: E501 - ) - break - with open(tmap_csv) as csvfile: - reader = csv.reader(csvfile, delimiter=",") - next(reader) - for row in reader: - trailmaps.append(Trailmap(*row)) - - return trailmaps - - def get_function_meta(trailmap: Trailmap, trail_type: Literal["model", "view"]): """Gets the function meta data.""" - if trailmap.func_attr[trail_type] is None: + func_attr = trailmap.func_attrs[trail_type] + if not func_attr.func_unwrapped: return None - doc_parsed = parse(trailmap.long_doc[trail_type]) - line = trailmap.lineon[trail_type] - path = trailmap.full_path[trail_type] - func_def = trailmap.func_def[trail_type] + doc_parsed = parse(func_attr.long_doc) + line = func_attr.lineon + path = func_attr.full_path + func_def = func_attr.func_def source_code_url = ( "https://github.com/OpenBB-finance/OpenBBTerminal/tree/main/" + path @@ -215,8 +32,8 @@ def get_function_meta(trailmap: Trailmap, trail_type: Literal["model", "view"]): params = [] for param in doc_parsed.params: arg_default = ( - trailmap.params[trail_type][param.arg_name].default - if param.arg_name in trailmap.params[trail_type] + func_attr.params[param.arg_name].default + if param.arg_name in func_attr.params else None ) params.append( @@ -403,6 +220,9 @@ def main() -> bool: shutil.rmtree(file) for trailmap in trailmaps: try: + if trailmap.location_path[0] == "root": + trailmap.location_path[0] = "" + functions_dict = add_todict( functions_dict, trailmap.location_path, trailmap ) @@ -420,7 +240,9 @@ def main() -> bool: with open(filepath, "w", **kwargs) as f: # type: ignore f.write(markdown) except Exception as e: - print(f"Error generating {trailmap.class_attr} - {e}") + print( + f"Error generating {trailmap.location_path} {trailmap.class_attr} - {e}" + ) return False functions_dict = {