From 8ffa8a0eea06bcfc1e526d20edb661c8d70bf5e7 Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Thu, 20 Nov 2025 15:00:05 +0100 Subject: [PATCH 1/3] refactor(graphics): inherit from `core.graphics.Graphics` to avoid repetition --- codesectools/sasts/all/graphics.py | 82 +++++------------------------- 1 file changed, 12 insertions(+), 70 deletions(-) diff --git a/codesectools/sasts/all/graphics.py b/codesectools/sasts/all/graphics.py index fba5aeb..c11dd7a 100644 --- a/codesectools/sasts/all/graphics.py +++ b/codesectools/sasts/all/graphics.py @@ -1,28 +1,25 @@ """Provides classes for generating plots and visualizations from aggregated SAST results.""" -import shutil -import tempfile - -import matplotlib import matplotlib.pyplot as plt -import typer from matplotlib.figure import Figure -from rich import print from codesectools.sasts.all.sast import AllSAST +from codesectools.sasts.core.graphics import Graphics as CoreGraphics from codesectools.utils import shorten_path -## Matplotlib config -matplotlib.rcParams.update( - { - "font.family": "serif", - "font.size": 11, - } -) +class Graphics(CoreGraphics): + """Base class for generating plots for aggregated SAST results. + + Attributes: + project_name (str): The name of the project being visualized. + all_sast (AllSAST): The instance managing all SAST tools. + output_dir (Path): The directory containing the aggregated results. + color_mapping (dict): A dictionary mapping SAST tool names to colors. + sast_names (list[str]): A list of names of the SAST tools involved in the analysis. + plot_functions (list): A list of methods responsible for generating plots. -class Graphics: - """Base class for generating graphics from aggregated SAST results.""" + """ def __init__(self, project_name: str) -> None: """Initialize the Graphics object.""" @@ -38,61 +35,6 @@ def __init__(self, project_name: str) -> None: self.sast_names.append(sast.name) self.plot_functions = [] - # Plot options - self.limit = 10 - - self.has_latex = shutil.which("pdflatex") - if self.has_latex: - matplotlib.use("pgf") - matplotlib.rcParams.update( - { - "pgf.texsystem": "pdflatex", - "text.usetex": True, - "pgf.rcfonts": False, - } - ) - else: - print("pdflatex not found, pgf will not be generated") - - def export(self, overwrite: bool, pgf: bool, show: bool) -> None: - """Generate, save, and optionally display all registered plots. - - Args: - overwrite: If True, overwrite existing figure files. - pgf: If True and LaTeX is available, export figures in PGF format. - show: If True, open the generated figures using the default viewer. - - """ - for plot_function in self.plot_functions: - fig = plot_function() - fig_name = plot_function.__name__.replace("plot_", "") - fig.set_size_inches(12, 7) - - if show: - with tempfile.NamedTemporaryFile(delete=True) as temp: - fig.savefig(f"{temp.name}.png", bbox_inches="tight") - typer.launch(f"{temp.name}.png", wait=False) - - figure_dir = self.output_dir / "_figures" - figure_dir.mkdir(exist_ok=True, parents=True) - figure_path = figure_dir / f"{fig_name}.png" - if figure_path.is_file() and not overwrite: - if not typer.confirm( - f"Found existing figure at {figure_path}, would you like to overwrite?" - ): - print(f"Figure {fig_name} not saved") - continue - - fig.savefig(figure_path, bbox_inches="tight") - print(f"Figure {fig_name} saved at {figure_path}") - - if pgf and self.has_latex: - figure_path_pgf = figure_dir / f"{fig_name}.pgf" - fig.savefig(figure_path_pgf, bbox_inches="tight") - print(f"Figure {fig_name} exported to pgf") - - plt.close(fig) - ## Single project class ProjectGraphics(Graphics): From 91d7e64b1b2b8c685b65dd6d486ba660b19e0948 Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Thu, 20 Nov 2025 15:02:48 +0100 Subject: [PATCH 2/3] feat(graphics)!: drop pgf, add support for pdf and svg BREAKING CHANGE: `pgf` format is dropped, please use `pdf` or `svg` instead --- codesectools/sasts/all/cli.py | 22 +++------ codesectools/sasts/core/cli.py | 40 ++++------------ codesectools/sasts/core/graphics.py | 71 +++++++++-------------------- 3 files changed, 36 insertions(+), 97 deletions(-) diff --git a/codesectools/sasts/all/cli.py b/codesectools/sasts/all/cli.py index 2cd32ca..94b696d 100644 --- a/codesectools/sasts/all/cli.py +++ b/codesectools/sasts/all/cli.py @@ -8,7 +8,7 @@ import typer from click import Choice from rich import print -from typing_extensions import Annotated +from typing_extensions import Annotated, Literal from codesectools.datasets import DATASETS_ALL from codesectools.datasets.core.dataset import FileDataset, GitRepoDataset @@ -200,26 +200,16 @@ def plot( help="Overwrite existing figures", ), ] = False, - show: Annotated[ - bool, - typer.Option( - "--show", - help="Display figures", - ), - ] = False, - pgf: Annotated[ - bool, - typer.Option( - "--pgf", - help="Export figures to pgf format (for LaTeX document)", - ), - ] = False, + format: Annotated[ + Literal["png", "pdf", "svg"], + typer.Option("--format", help="Figures export format"), + ] = "png", ) -> None: """Generate and display plots for a project's aggregated analysis results.""" from codesectools.sasts.all.graphics import ProjectGraphics project_graphics = ProjectGraphics(project_name=project) - project_graphics.export(overwrite=overwrite, show=show, pgf=pgf) + project_graphics.export(overwrite=overwrite, format=format) @cli.command(help="Generate an HTML report") def report( diff --git a/codesectools/sasts/core/cli.py b/codesectools/sasts/core/cli.py index d7d61e0..4195df9 100644 --- a/codesectools/sasts/core/cli.py +++ b/codesectools/sasts/core/cli.py @@ -12,7 +12,7 @@ import typer from click import Choice from rich import print -from typing_extensions import Annotated +from typing_extensions import Annotated, Literal from codesectools.datasets import DATASETS_ALL from codesectools.datasets.core.dataset import FileDataset, GitRepoDataset @@ -314,30 +314,12 @@ def plot( help="Overwrite existing figures", ), ] = False, - show: Annotated[ - bool, - typer.Option( - "--show", - help="Display figures", - ), - ] = False, - pgf: Annotated[ - bool, - typer.Option( - "--pgf", - help="Export figures to pgf format (for LaTeX document)", - ), - ] = False, + format: Annotated[ + Literal["png", "pdf", "svg"], + typer.Option("--format", help="Figures export format"), + ] = "png", ) -> None: - """Generate and export plots for a given project or dataset result. - - Args: - result: The name of the analysis result to plot. - overwrite: If True, overwrite existing figure files. - show: If True, display the generated figures. - pgf: If True, export figures in PGF format for LaTeX documents. - - """ + """Generate and export plots for a given project or dataset result.""" from codesectools.sasts.core.graphics import ( FileDatasetGraphics, GitRepoDatasetGraphics, @@ -347,7 +329,7 @@ def plot( if result in self.sast.list_results(project=True): project = result project_graphics = ProjectGraphics(self.sast, project_name=project) - project_graphics.export(overwrite=overwrite, show=show, pgf=pgf) + project_graphics.export(overwrite=overwrite, format=format) elif result in self.sast.list_results(dataset=True): dataset = result dataset_name, lang = dataset.split("_") @@ -356,15 +338,11 @@ def plot( file_dataset_graphics = FileDatasetGraphics( self.sast, dataset=dataset ) - file_dataset_graphics.export( - overwrite=overwrite, show=show, pgf=pgf - ) + file_dataset_graphics.export(overwrite=overwrite, format=format) elif isinstance(dataset, GitRepoDataset): git_repo_dataset_graphics = GitRepoDatasetGraphics( self.sast, dataset=dataset ) - git_repo_dataset_graphics.export( - overwrite=overwrite, show=show, pgf=pgf - ) + git_repo_dataset_graphics.export(overwrite=overwrite, format=format) else: print("Not supported yet") diff --git a/codesectools/sasts/core/graphics.py b/codesectools/sasts/core/graphics.py index 14d0cd0..841e0f8 100644 --- a/codesectools/sasts/core/graphics.py +++ b/codesectools/sasts/core/graphics.py @@ -5,9 +5,6 @@ benchmark performance. """ -import shutil -import tempfile - import matplotlib import matplotlib.pyplot as plt import numpy as np @@ -20,28 +17,24 @@ from codesectools.shared.cwe import CWE from codesectools.utils import shorten_path -## Matplotlib config -matplotlib.rcParams.update( - { - "font.family": "serif", - "font.size": 11, - } -) - class Graphics: - """Base class for generating graphics from SAST results. + """Base class for generating plots and visualizations from SAST results. Attributes: - sast (SAST): The SAST tool instance. - output_dir (Path): The directory containing the analysis results. - color_mapping (dict): A mapping of categories to colors for plotting. - plot_functions (list): A list of methods that generate plots. - limit (int): The maximum number of items to show in top-N plots. - has_latex (bool): True if a LaTeX installation is found. + limit (int): The maximum number of items to display in charts (default is 10). + filetypes (dict[str, str]): A mapping of file extensions to matplotlib backends. + sast (SAST): The SAST tool instance associated with the graphics. + output_dir (Path): The directory where the analysis results are stored. + color_mapping (dict): A dictionary mapping categories to colors for plots. + plot_functions (list): A list of methods responsible for generating plots. """ + limit = 10 + + filetypes = {"png": "AGG", "pdf": "PDF", "svg": "SVG"} + def __init__(self, sast: SAST, project_name: str) -> None: """Initialize the Graphics object. @@ -56,44 +49,27 @@ def __init__(self, sast: SAST, project_name: str) -> None: self.color_mapping["NONE"] = "BLACK" self.plot_functions = [] - # Plot options - self.limit = 10 - - self.has_latex = shutil.which("pdflatex") - if self.has_latex: - matplotlib.use("pgf") - matplotlib.rcParams.update( - { - "pgf.texsystem": "pdflatex", - "text.usetex": True, - "pgf.rcfonts": False, - } - ) - else: - print("pdflatex not found, pgf will not be generated") - - def export(self, overwrite: bool, pgf: bool, show: bool) -> None: - """Generate, save, and optionally display all registered plots. + def export(self, overwrite: bool, format: str) -> None: + """Generate and save the configured plots to the output directory. + + Iterates through the registered plot functions, generates the figures, + and saves them to a `_figures` subdirectory within the output directory. Args: - overwrite: If True, overwrite existing figure files. - pgf: If True and LaTeX is available, export figures in PGF format. - show: If True, open the generated figures using the default viewer. + overwrite: If True, overwrite existing figure files without prompting. + format: The file format for the exported figures (e.g., "png", "pdf", "svg"). """ + matplotlib.use(self.filetypes[format]) + for plot_function in self.plot_functions: fig = plot_function() fig_name = plot_function.__name__.replace("plot_", "") fig.set_size_inches(12, 7) - if show: - with tempfile.NamedTemporaryFile(delete=True) as temp: - fig.savefig(f"{temp.name}.png", bbox_inches="tight") - typer.launch(f"{temp.name}.png", wait=False) - figure_dir = self.output_dir / "_figures" figure_dir.mkdir(exist_ok=True, parents=True) - figure_path = figure_dir / f"{fig_name}.png" + figure_path = figure_dir / f"{fig_name}.{format}" if figure_path.is_file() and not overwrite: if not typer.confirm( f"Found existing figure at {figure_path}, would you like to overwrite?" @@ -104,11 +80,6 @@ def export(self, overwrite: bool, pgf: bool, show: bool) -> None: fig.savefig(figure_path, bbox_inches="tight") print(f"Figure {fig_name} saved at {figure_path}") - if pgf and self.has_latex: - figure_path_pgf = figure_dir / f"{fig_name}.pgf" - fig.savefig(figure_path_pgf, bbox_inches="tight") - print(f"Figure {fig_name} exported to pgf") - plt.close(fig) From 3a44aae06d1b282c71b944d8e8432a8675d62957 Mon Sep 17 00:00:00 2001 From: Villon CHEN Date: Thu, 20 Nov 2025 15:03:02 +0100 Subject: [PATCH 3/3] chore(release): bump project version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0852bd5..8808a1a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "CodeSecTools" -version = "0.13.4" +version = "0.13.5" description = "A framework for code security that provides abstractions for static analysis tools and datasets to support their integration, testing, and evaluation." readme = "README.md" license = "AGPL-3.0-only" diff --git a/uv.lock b/uv.lock index a09eb17..b16e6ba 100644 --- a/uv.lock +++ b/uv.lock @@ -221,7 +221,7 @@ wheels = [ [[package]] name = "codesectools" -version = "0.13.4" +version = "0.13.5" source = { editable = "." } dependencies = [ { name = "gitpython" },