Skip to content

Commit 0d173fc

Browse files
authored
🐛 allow to specify output folder - use only relative paths (#136)
* 🐛 allow to specify output folder - use only relative paths - before other locations were not working, see #135 - everything is relative to the generated qmd report now - quarto without any additional parameters uses path relative to qmd file - in qmd report python snippets can use `report_dir` (cwd and quarto report file locaction) Commit qmd file for html output to get an idea of structure and prepare integration tests * 🐛 ensure path are absolute and fix dfi export (static_dir exports) relevant for static quarto based reports * 🐛 writing is w.r.t to output folder, file path is already specifying static dir. * 📝 some notes and comments - also correcting one typo in comment * 🎨 address copilot comments (typo and unclear comments) * 🚚 move file to subfolder, specify how reports shoudl be build * 🎨 move output_dir to init of QuartoReportView
1 parent e151711 commit 0d173fc

File tree

5 files changed

+396
-33
lines changed

5 files changed

+396
-33
lines changed

src/vuegen/quarto_reportview.py

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def __init__(
2525
report: r.Report,
2626
report_type: r.ReportType,
2727
quarto_checks: bool = False,
28+
output_dir: Optional[Path] = BASE_DIR,
2829
static_dir: str = STATIC_FILES_DIR,
2930
):
3031
"""_summary_
@@ -44,6 +45,7 @@ def __init__(
4445
super().__init__(report=report, report_type=report_type)
4546
self.quarto_checks = quarto_checks
4647
self.static_dir = static_dir
48+
self.output_dir = output_dir.resolve().absolute()
4749
# self.BUNDLED_EXECUTION = False
4850
self.quarto_path = "quarto"
4951
# self.env_vars = os.environ.copy()
@@ -72,25 +74,28 @@ def __init__(
7274
r.ComponentType.HTML: self._generate_html_content,
7375
}
7476

75-
def generate_report(self, output_dir: Path = BASE_DIR) -> None:
77+
def generate_report(self, output_dir: Optional[Path] = None) -> None:
7678
"""
7779
Generates the qmd file of the quarto report. It creates code for rendering each section and its subsections with all components.
7880
7981
Parameters
8082
----------
8183
output_dir : Path, optional
82-
The folder where the generated report files will be saved (default is BASE_DIR).
84+
The folder where the generated report files will be saved.
85+
Will overwrite value set on initialization of QuartoReportView.
8386
"""
87+
if output_dir is not None:
88+
self.output_dir = Path(output_dir).resolve().absolute()
89+
8490
self.report.logger.debug(
85-
f"Generating '{self.report_type}' report in directory: '{output_dir}'"
91+
f"Generating '{self.report_type}' report in directory: '{self.output_dir}'"
8692
)
87-
8893
# Create the output folder
89-
if create_folder(output_dir):
90-
self.report.logger.debug(f"Created output directory: '{output_dir}'")
94+
if create_folder(self.output_dir, is_nested=True):
95+
self.report.logger.debug(f"Created output directory: '{self.output_dir}'")
9196
else:
9297
self.report.logger.debug(
93-
f"Output directory already existed: '{output_dir}'"
98+
f"Output directory already existed: '{self.output_dir}'"
9499
)
95100

96101
# Create the static folder
@@ -207,6 +212,9 @@ def generate_report(self, output_dir: Path = BASE_DIR) -> None:
207212
self.report.logger.warning(
208213
f"No subsections found in section: '{section.title}'. To show content in the report, add subsections to the section."
209214
)
215+
# Add globally set output folder
216+
report_imports.append("from pathlib import Path")
217+
report_imports.append("report_dir = Path().cwd()")
210218

211219
# Remove duplicated imports
212220
report_unique_imports = set(report_imports)
@@ -224,7 +232,8 @@ def generate_report(self, output_dir: Path = BASE_DIR) -> None:
224232
report_formatted_imports = "\n".join(report_unique_imports)
225233

226234
# Write the navigation and general content to a Python file
227-
with open(Path(output_dir) / f"{self.BASE_DIR}.qmd", "w") as quarto_report:
235+
fname_qmd_report = self.output_dir / f"{self.BASE_DIR}.qmd"
236+
with open(fname_qmd_report, "w") as quarto_report:
228237
quarto_report.write(yaml_header)
229238
quarto_report.write(
230239
f"""\n```{{python}}
@@ -234,7 +243,7 @@ def generate_report(self, output_dir: Path = BASE_DIR) -> None:
234243
)
235244
quarto_report.write("\n".join(qmd_content))
236245
self.report.logger.info(
237-
f"Created qmd script to render the app: {self.BASE_DIR}.qmd"
246+
f"Created qmd script to render the app: {fname_qmd_report}"
238247
)
239248

240249
except Exception as e:
@@ -243,7 +252,7 @@ def generate_report(self, output_dir: Path = BASE_DIR) -> None:
243252
)
244253
raise
245254

246-
def run_report(self, output_dir: str = BASE_DIR) -> None:
255+
def run_report(self, output_dir: Optional[Path] = None) -> None:
247256
"""
248257
Runs the generated quarto report.
249258
@@ -253,8 +262,10 @@ def run_report(self, output_dir: str = BASE_DIR) -> None:
253262
The folder where the report was generated (default is 'sections').
254263
"""
255264
# from quarto_cli import run_quarto # entrypoint of quarto-cli not in module?
265+
if output_dir is not None:
266+
self.output_dir = Path(output_dir).resolve().absolute()
256267

257-
file_path_to_qmd = Path(output_dir) / f"{self.BASE_DIR}.qmd"
268+
file_path_to_qmd = Path(self.output_dir) / f"{self.BASE_DIR}.qmd"
258269
args = [self.quarto_path, "render", str(file_path_to_qmd)]
259270
self.report.logger.info(
260271
f"Running '{self.report.title}' '{self.report_type}' report with {args!r}"
@@ -554,13 +565,15 @@ def _generate_plot_content(self, plot) -> List[str]:
554565

555566
# Define plot path
556567
if self.is_report_static:
568+
# ? should that be in the output folder
557569
static_plot_path = (
558570
Path(self.static_dir) / f"{plot.title.replace(' ', '_')}.png"
559-
)
571+
).absolute()
572+
self.report.logger.debug(f"Static plot path: {static_plot_path}")
560573
else:
561574
html_plot_file = (
562575
Path(self.static_dir) / f"{plot.title.replace(' ', '_')}.html"
563-
)
576+
).absolute()
564577

565578
# Add content for the different plot types
566579
try:
@@ -572,7 +585,7 @@ def _generate_plot_content(self, plot) -> List[str]:
572585
plot_content.append(self._generate_plot_code(plot))
573586
if self.is_report_static:
574587
plot_content.append(
575-
f"""fig_plotly.write_image("{static_plot_path.relative_to("quarto_report").as_posix()}")\n```\n"""
588+
f"""fig_plotly.write_image("{static_plot_path.relative_to(self.output_dir).as_posix()}")\n```\n"""
576589
)
577590
plot_content.append(self._generate_image_content(static_plot_path))
578591
else:
@@ -581,7 +594,7 @@ def _generate_plot_content(self, plot) -> List[str]:
581594
plot_content.append(self._generate_plot_code(plot))
582595
if self.is_report_static:
583596
plot_content.append(
584-
f"""fig_altair.save("{static_plot_path.relative_to("quarto_report").as_posix()}")\n```\n"""
597+
f"""fig_altair.save("{static_plot_path.as_posix()}")\n```\n"""
585598
)
586599
plot_content.append(self._generate_image_content(static_plot_path))
587600
else:
@@ -597,7 +610,7 @@ def _generate_plot_content(self, plot) -> List[str]:
597610
networkx_graph, html_plot_file
598611
)
599612

600-
# Add number of nodes and edges to the plor conetnt
613+
# Add number of nodes and edges to the plot content
601614
num_nodes = networkx_graph.number_of_nodes()
602615
num_edges = networkx_graph.number_of_edges()
603616
plot_content.append(f"**Number of nodes:** {num_nodes}\n")
@@ -653,9 +666,11 @@ def _generate_plot_code(self, plot, output_file="") -> str:
653666
response.raise_for_status()
654667
plot_json = response.text\n"""
655668
else: # If it's a local file
656-
plot_rel_path = get_relative_file_path(plot.file_path, base_path="..")
669+
plot_rel_path = get_relative_file_path(
670+
plot.file_path, relative_to=self.output_dir
671+
).as_posix()
657672
plot_code += f"""
658-
with open('{plot_rel_path.as_posix()}', 'r') as plot_file:
673+
with open(report_dir /'{plot_rel_path}', 'r') as plot_file:
659674
plot_json = json.load(plot_file)\n"""
660675
# Add specific code for each visualization tool
661676
if plot.plot_type == r.PlotType.PLOTLY:
@@ -680,7 +695,9 @@ def _generate_plot_code(self, plot, output_file="") -> str:
680695
if is_url(plot.file_path) and plot.file_path.endswith(".html"):
681696
iframe_src = output_file
682697
else:
683-
iframe_src = Path("..") / output_file
698+
iframe_src = get_relative_file_path(
699+
output_file, relative_to=self.output_dir
700+
)
684701

685702
# Embed the HTML file in an iframe
686703
plot_code = f"""
@@ -757,12 +774,12 @@ def _generate_dataframe_content(self, dataframe) -> List[str]:
757774
df_file_path = dataframe.file_path
758775
else:
759776
df_file_path = get_relative_file_path(
760-
dataframe.file_path, base_path=".."
761-
)
777+
dataframe.file_path, relative_to=self.output_dir
778+
).as_posix()
762779
# Load the DataFrame using the correct function
763780
read_function = read_function_mapping[file_extension]
764781
dataframe_content.append(
765-
f"""df = pd.{read_function.__name__}('{df_file_path.as_posix()}')\n"""
782+
f"""df = pd.{read_function.__name__}(report_dir / '{df_file_path}')\n"""
766783
)
767784
# Display the dataframe
768785
dataframe_content.extend(self._show_dataframe(dataframe))
@@ -781,7 +798,7 @@ def _generate_dataframe_content(self, dataframe) -> List[str]:
781798
)
782799
)
783800
dataframe_content.append(
784-
f"df = pd.{read_function.__name__}('{df_file_path.as_posix()}', "
801+
f"df = pd.{read_function.__name__}(report_dir / '{df_file_path}', "
785802
f"sheet_name='{sheet_name}')\n"
786803
)
787804
# Display the dataframe
@@ -845,10 +862,12 @@ def _generate_markdown_content(self, markdown) -> List[str]:
845862
)
846863
)
847864
else: # If it's a local file
848-
md_rel_path = get_relative_file_path(markdown.file_path, base_path="..")
865+
md_rel_path = get_relative_file_path(
866+
markdown.file_path, relative_to=self.output_dir
867+
)
849868
markdown_content.append(
850869
f"""
851-
with open('{md_rel_path.as_posix()}', 'r') as markdown_file:
870+
with open(report_dir / '{md_rel_path.as_posix()}', 'r') as markdown_file:
852871
markdown_content = markdown_file.read()\n"""
853872
)
854873

@@ -896,9 +915,11 @@ def _show_dataframe(self, dataframe, suffix: Optional[str] = None) -> List[str]:
896915
fpath_df_image.stem + f"_{suffix.replace(' ', '_')}"
897916
)
898917
fpath_df_image = fpath_df_image.with_suffix(".png")
899-
918+
fpath_df_image_rel_static = get_relative_file_path(
919+
fpath_df_image, relative_to=self.output_dir
920+
)
900921
dataframe_content.append(
901-
f"df.dfi.export('{Path(fpath_df_image).relative_to('quarto_report').as_posix()}',"
922+
f"df.dfi.export('{fpath_df_image_rel_static}',"
902923
" max_rows=10, max_cols=5, table_conversion='matplotlib')\n```\n"
903924
)
904925
# Use helper method to add centered image content
@@ -935,7 +956,9 @@ def _generate_html_content(self, html) -> List[str]:
935956
if is_url(html.file_path):
936957
html_file_path = html.file_path
937958
else:
938-
html_file_path = get_relative_file_path(html.file_path, base_path="..")
959+
html_file_path = get_relative_file_path(
960+
html.file_path, relative_to=self.output_dir
961+
)
939962
iframe_code = f"""
940963
<div style="text-align: center;">
941964
<iframe src="{html_file_path.as_posix()}" alt="{html.title}" width="950px" height="530px"></iframe>
@@ -978,7 +1001,9 @@ def _generate_image_content(
9781001
if is_url(image_path):
9791002
src = image_path
9801003
else:
981-
src = get_relative_file_path(image_path, base_path="..").as_posix()
1004+
src = get_relative_file_path(
1005+
image_path, relative_to=self.output_dir
1006+
).as_posix()
9821007

9831008
return f"""![]({src}){{fig-alt={alt_text} width={width}}}\n"""
9841009

src/vuegen/report_generator.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,10 @@ def get_report(
124124
report=report,
125125
report_type=report_type,
126126
quarto_checks=quarto_checks,
127+
output_dir=report_dir,
127128
static_dir=static_files_dir,
128129
)
129-
quarto_report.generate_report(output_dir=report_dir)
130-
quarto_report.run_report(output_dir=report_dir)
130+
quarto_report.generate_report()
131+
quarto_report.run_report()
131132
# ? Could be also the path to the report file for quarto based reports
132133
return report_dir, config_path

src/vuegen/utils/__init__.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,9 @@ def create_folder(directory_path: str, is_nested: bool = False) -> bool:
174174
raise OSError(f"Error creating directory '{directory_path}': {e}")
175175

176176

177-
def get_relative_file_path(file_path: str, base_path: str = "") -> Path:
177+
def get_relative_file_path(
178+
file_path: str, base_path: str = "", relative_to: str = "."
179+
) -> Path:
178180
"""
179181
Returns the relative file path of a given file with respect to
180182
the current working directory (CWD).
@@ -189,13 +191,23 @@ def get_relative_file_path(file_path: str, base_path: str = "") -> Path:
189191
The full file path to be converted to a relative path.
190192
base_path : str, optional
191193
The base path to be prepended to the relative path, default is an empty string.
194+
relativ_to : str, optional
195+
The directory to which the file path should be relative,
196+
default is the current directory (".").
192197
193198
Returns
194199
-------
195200
Path
196201
The file path relative to the CWD.
197202
"""
198-
rel_path = Path(file_path).resolve().relative_to(Path.cwd().resolve())
203+
if relative_to == ".":
204+
# Use the current working directory as the base
205+
relative_to = Path.cwd()
206+
elif isinstance(relative_to, str):
207+
# ensure path is a Path object
208+
relative_to = Path(relative_to)
209+
rel_path = os.path.relpath(Path(file_path).resolve(), relative_to)
210+
rel_path = Path(rel_path) # Ensure rel_path is a Path object
199211

200212
if base_path:
201213
rel_path = Path(base_path) / rel_path

0 commit comments

Comments
 (0)