Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: give analyze the ability to display browsable results #94

Merged
merged 23 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b99ed4b
feat: analyze can now store results in a md file
Poiuy7312 Oct 24, 2023
2a79a78
feat: added test_case for using --markdown-storage
Poiuy7312 Oct 24, 2023
cf4a499
fix: fixed formatting
Poiuy7312 Oct 24, 2023
3bd0660
fix: disabled ruff rule for analyze
Poiuy7312 Oct 24, 2023
b952feb
feat: added frogmouth to dependencies
Poiuy7312 Oct 25, 2023
07e2724
feat: created function to display with frogmouth
Poiuy7312 Oct 25, 2023
c3bf113
feat: added test cases for main
Poiuy7312 Oct 25, 2023
ae5752d
feat: added display subcommand for analyze
Poiuy7312 Oct 25, 2023
5be0187
feat: made another testcase
Poiuy7312 Oct 25, 2023
6142dd6
style: moved location of code
Poiuy7312 Oct 25, 2023
69ee20e
fix: fixed formatting
Poiuy7312 Oct 25, 2023
206ef09
fix: changed checks file
Poiuy7312 Oct 27, 2023
535fd55
Merge branch 'master' into Analyze-report
Poiuy7312 Oct 27, 2023
62bf45a
feat: added test_case for executable_name
Poiuy7312 Oct 30, 2023
9a445a5
feat: added debug handling to frogmouth function
Poiuy7312 Oct 30, 2023
efc59f4
format: moved exectuable_name() to util.py
Poiuy7312 Oct 30, 2023
effaf49
fix: reformated main
Poiuy7312 Oct 30, 2023
9a9428c
format: removed test_database.py file
Poiuy7312 Oct 30, 2023
65c4311
Merge branch 'Analyze-report' of github.com:AstuteSource/chasten into…
Poiuy7312 Oct 30, 2023
1b4bd5f
changed comments for test case
Poiuy7312 Oct 31, 2023
997a277
feat: displays information of frogmouth executable
Poiuy7312 Oct 31, 2023
7a6150e
Merge branch 'master' of github.com:AstuteSource/chasten into Analyze…
Poiuy7312 Nov 3, 2023
5a98846
Merge branch 'master' into Analyze-report
Poiuy7312 Nov 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions chasten/constants.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Define constants with dataclasses for use in chasten."""

from dataclasses import dataclass
from pathlib import Path


# chasten constant
@dataclass(frozen=True)
class Chasten:
"""Define the Chasten dataclass for constant(s)."""

Analyze_Storage: Path
Application_Name: str
Application_Author: str
Chasten_Database_View: str
Expand All @@ -26,6 +28,7 @@ class Chasten:


chasten = Chasten(
Analyze_Storage=Path("analysis.md"),
Application_Name="chasten",
Application_Author="ChastenedTeam",
Chasten_Database_View="chasten_complete",
Expand Down
42 changes: 27 additions & 15 deletions chasten/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from sqlite_utils import Database

from chasten import constants, enumerations, filesystem, output
from chasten import constants, enumerations, filesystem, output, util

CHASTEN_SQL_SELECT_QUERY = """
SELECT
Expand Down Expand Up @@ -129,18 +129,6 @@ def display_datasette_details(
output.console.print()


def executable_name(OpSystem: str = "Linux") -> str:
"""Get the executable directory depending on OS"""
exe_directory = "/bin/"
executable_name = constants.datasette.Datasette_Executable
# Checks if the OS is windows and changed where to search if true
if OpSystem == "Windows":
exe_directory = "/Scripts/"
executable_name += ".exe"
virtual_env_location = sys.prefix
return virtual_env_location + exe_directory + executable_name


def start_datasette_server( # noqa: PLR0912, PLR0913
database_path: Path,
datasette_metadata: Path,
Expand All @@ -160,7 +148,9 @@ def start_datasette_server( # noqa: PLR0912, PLR0913
# chasten will exist in a bin directory. For instance, the "datasette"
# executable that is a dependency of chasten can be found by starting
# the search from this location for the virtual environment.
full_executable_name = executable_name(OpSystem)
full_executable_name = util.executable_name(
constants.datasette.Datasette_Executable, OpSystem
)
(found_executable, executable_path) = filesystem.can_find_executable(
full_executable_name
)
Expand Down Expand Up @@ -224,7 +214,7 @@ def start_datasette_server( # noqa: PLR0912, PLR0913
# datasette-publish-fly plugin) and thus need to exit and not proceed
if not found_publish_platform_executable:
output.console.print(
":person_shrugging: Was not able to find '{datasette_platform}'"
f":person_shrugging: Was not able to find '{datasette_platform}'"
)
return None
# was able to find the fly or vercel executable that will support the
Expand Down Expand Up @@ -276,3 +266,25 @@ def start_datasette_server( # noqa: PLR0912, PLR0913
# there is debugging output in the console to indicate this option.
proc = subprocess.Popen(cmd)
proc.wait()


def display_results_frog_mouth(result_file, OpSystem) -> None:
"""Run frogmouth as a subprocess of chasten"""
cmd = [
"frogmouth",
result_file,
]
executable = util.executable_name("frogmouth", OpSystem)
exec_found, executable_path = filesystem.can_find_executable(executable)
if exec_found:
# run frogmouth with specified path
output.console.print("\n🐸 Frogmouth Information\n")
output.console.print(f" {small_bullet_unicode} Venv: {sys.prefix}")
output.console.print(f" {small_bullet_unicode} Program: {executable_path}")
proc = subprocess.Popen(cmd)
proc.wait()
else:
output.console.print(
":person_shrugging: Was not able to find frogmouth executable try installing it separately"
)
return None
93 changes: 84 additions & 9 deletions chasten/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""💫 Chasten checks the AST of a Python program."""

import os
import sys
import time
from pathlib import Path
Expand Down Expand Up @@ -32,6 +33,7 @@

# create a small bullet for display in the output
small_bullet_unicode = constants.markers.Small_Bullet_Unicode
ANALYSIS_FILE = constants.chasten.Analyze_Storage


# ---
Expand Down Expand Up @@ -125,7 +127,8 @@ def validate_file(
else:
output.opt_print_log(verbose, newline="")
output.opt_print_log(
verbose, label=f":sparkles: Contents of {configuration_file_str}:\n"
verbose,
label=f":sparkles: Contents of {configuration_file_str}:\n",
)
output.opt_print_log(verbose, config_file=configuration_file_yml)
return validated
Expand All @@ -135,7 +138,8 @@ def validate_configuration_files(
config: Path,
verbose: bool = False,
) -> Tuple[
bool, Union[Dict[str, List[Dict[str, Union[str, Dict[str, int]]]]], Dict[Any, Any]]
bool,
Union[Dict[str, List[Dict[str, Union[str, Dict[str, int]]]]], Dict[Any, Any]],
]:
"""Validate the configuration."""
# there is a specified configuration directory path;
Expand Down Expand Up @@ -358,7 +362,8 @@ def configure( # noqa: PLR0913
)
# write the configuration file for the chasten tool in the created directory
filesystem.create_configuration_file(
created_directory_path, constants.filesystem.Main_Configuration_File
created_directory_path,
constants.filesystem.Main_Configuration_File,
)
# write the check file for the chasten tool in the created directory
filesystem.create_configuration_file(
Expand Down Expand Up @@ -426,6 +431,18 @@ def analyze( # noqa: PLR0912, PLR0913, PLR0915
writable=True,
resolve_path=True,
),
store_result: Path = typer.Option(
None,
"--markdown-storage",
"-r",
help="A directory for storing results in a markdown file",
exists=True,
file_okay=False,
dir_okay=True,
readable=True,
writable=True,
resolve_path=True,
),
config: Path = typer.Option(
None,
"--config",
Expand All @@ -444,8 +461,10 @@ def analyze( # noqa: PLR0912, PLR0913, PLR0915
"-t",
help="Specify the destination for debugging output.",
),
display: bool = typer.Option(False, help="Display results using frogmouth"),
verbose: bool = typer.Option(False, help="Enable verbose mode output."),
save: bool = typer.Option(False, help="Enable saving of output file(s)."),
force: bool = typer.Option(False, help="Force creation of new markdown file"),
) -> None:
"""💫 Analyze the AST of Python source code."""
start_time = time.time()
Expand Down Expand Up @@ -522,6 +541,27 @@ def analyze( # noqa: PLR0912, PLR0913, PLR0915
"\n:person_shrugging: Cannot perform analysis due to invalid search directory.\n"
)
sys.exit(constants.markers.Non_Zero_Exit)
if store_result:
# creates an empty string for storing results temporarily
analysis_result = ""
analysis_file_dir = store_result / ANALYSIS_FILE
# clears markdown file of results if it exists and new results are to be store
if filesystem.confirm_valid_file(analysis_file_dir):
if not force:
if display:
database.display_results_frog_mouth(
analysis_file_dir, util.get_OS()
)
sys.exit(0)
else:
output.console.print(
"File already exists: use --force to recreate markdown directory."
)
sys.exit(constants.markers.Non_Zero_Exit)
else:
analysis_file_dir.write_text("")
# creates file if doesn't exist already
analysis_file_dir.touch()
# create the list of directories
valid_directories = [input_path]
# output the list of directories subject to checking
Expand All @@ -536,7 +576,9 @@ def analyze( # noqa: PLR0912, PLR0913, PLR0915
# iterate through and perform each of the checks
for current_check in check_list:
# extract the pattern for the current check
current_xpath_pattern = str(current_check[constants.checks.Check_Pattern]) # type: ignore
current_xpath_pattern = str(
current_check[constants.checks.Check_Pattern]
) # type: ignore
# extract the minimum and maximum values for the checks, if they exist
# note that this function will return None for a min or a max if
# that attribute does not exist inside of the current_check; importantly,
Expand All @@ -561,8 +603,7 @@ def analyze( # noqa: PLR0912, PLR0913, PLR0915
match_generator = pyastgrepsearch.search_python_files(
paths=valid_directories, expression=current_xpath_pattern, xpath2=True
)

# materialize a list from the generator of (potential) matches;
# materia>>> mastlize a list from the generator of (potential) matches;
# note that this list will also contain an object that will
# indicate that the analysis completed for each located file
match_generator_list = list(match_generator)
Expand Down Expand Up @@ -597,6 +638,19 @@ def analyze( # noqa: PLR0912, PLR0913, PLR0915
f" {check_status_symbol} id: '{check_id}', name: '{check_name}'"
+ f", pattern: '{current_xpath_pattern_escape}', min={min_count}, max={max_count}"
)
if store_result:
# makes the check marks or x's appear as words instead for markdown
check_pass = (
"PASSED:"
if check_status_symbol == "[green]\u2713[/green]"
else "FAILED:"
)
# stores check type in a string to stored in file later
analysis_result += (
f"\n# {check_pass} **ID:** '{check_id}', **Name:** '{check_name}'"
+ f", **Pattern:** '{current_xpath_pattern_escape}', min={min_count}, max={max_count}\n\n"
)

# for each potential match, log and, if verbose model is enabled,
# display details about each of the matches
current_result_source = results.Source(
Expand Down Expand Up @@ -633,6 +687,9 @@ def analyze( # noqa: PLR0912, PLR0913, PLR0915
output.console.print(
f" {small_bullet_unicode} {file_name} - {len(matches_list)} matches"
)
if store_result:
# stores details of checks in string to be stored later
analysis_result += f" - {file_name} - {len(matches_list)} matches\n"
# extract the lines of source code for this file; note that all of
# these matches are organized for the same file and thus it is
# acceptable to extract the lines of the file from the first match
Expand Down Expand Up @@ -662,15 +719,20 @@ def analyze( # noqa: PLR0912, PLR0913, PLR0915
),
linematch_context=util.join_and_preserve(
current_match.file_lines,
max(0, position_end - constants.markers.Code_Context),
max(
0,
position_end - constants.markers.Code_Context,
),
position_end + constants.markers.Code_Context,
),
)
# save the entire current_match that is an instance of
# pyastgrepsearch.Match for verbose debugging output as needed
current_check_save._matches.append(current_match)
# add the match to the listing of matches for the current check
current_check_save.matches.append(current_match_for_current_check_save) # type: ignore
current_check_save.matches.append(
current_match_for_current_check_save
) # type: ignore
# add the current source to main object that contains a list of source
chasten_results_save.sources.append(current_result_source)
# display all of the analysis results if verbose output is requested
Expand All @@ -690,10 +752,23 @@ def analyze( # noqa: PLR0912, PLR0913, PLR0915

if not all_checks_passed:
output.console.print("\n:sweat: At least one check did not pass.")
if store_result:
# writes results of analyze into a markdown file
analysis_file_dir.write_text(analysis_result, encoding="utf-8")
output.console.print(
f"\n:sparkles: Results saved in: {os.path.abspath(analysis_file_dir)}\n"
)
sys.exit(constants.markers.Non_Zero_Exit)
output.console.print(
f"\n:joy: All checks passed. Elapsed Time: {elapsed_time} seconds"
)
if store_result:
# writes results of analyze into a markdown file
result_path = os.path.abspath(analysis_file_dir)
analysis_file_dir.write_text(analysis_result, encoding="utf-8")
output.console.print(f"\n:sparkles: Results saved in: {result_path}\n")
if display:
database.display_results_frog_mouth(result_path, util.get_OS())


@cli.command()
Expand Down Expand Up @@ -760,7 +835,7 @@ def integrate( # noqa: PLR0913
if combined_json_file_name:
output.console.print(f"\n:sparkles: Saved the file '{combined_json_file_name}'")
# "flatten" (i.e., "un-nest") the now-saved combined JSON file using flatterer
# create the SQLite3 database and then configure the database for use in datasett
# create the SQLite3 database and then configure the database for use in datasette
combined_flattened_directory = filesystem.write_flattened_csv_and_database(
combined_json_file_name,
output_directory,
Expand Down
12 changes: 12 additions & 0 deletions chasten/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import importlib.metadata
import platform
import sys

from chasten import constants

Expand All @@ -25,6 +26,17 @@ def get_OS() -> str:
return OpSystem


def executable_name(executable_name: str, OpSystem: str = "Linux") -> str:
"""Get the executable directory depending on OS"""
exe_directory = "/bin/"
# Checks if the OS is windows and changed where to search if true
if OpSystem == "Windows":
exe_directory = "/Scripts/"
executable_name += ".exe"
virtual_env_location = sys.prefix
return virtual_env_location + exe_directory + executable_name


def get_symbol_boolean(answer: bool) -> str:
"""Produce a symbol-formatted version of a boolean value of True or False."""
if answer:
Expand Down
Loading
Loading