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

Refactor Metrics: fix metric base type, Add report interface for report and support functions #17

Merged
merged 3 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
62 changes: 31 additions & 31 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,34 +30,34 @@ jobs:
run: |
poetry install

# - name: Run Tests
# run: |
# make test

# - name: Persist Coverage
# uses: actions/upload-artifact@v3
# with:
# name: coverage
# path: |
# coverage.xml
# .coverage

# coverage:
# needs: [test]
# name: coverage
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# - name: Load coverage artifact
# uses: actions/download-artifact@v3
# with:
# name: coverage
# - name: Push coverage report
# uses: paambaati/[email protected]
# env:
# CC_TEST_REPORTER_ID: ${{secrets.REPORTER_ID}}
# with:
# prefix: ${{github.workspace}}
# coverageLocations: |
# ${{github.workspace}}/.coverage/:coverage.py
# ${{github.workspace}}/coverage.xml/:coverage.py
- name: Run Tests
run: |
make test

- name: Persist Coverage
uses: actions/upload-artifact@v3
with:
name: coverage
path: |
coverage.xml
.coverage

coverage:
needs: [test]
name: coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Load coverage artifact
uses: actions/download-artifact@v3
with:
name: coverage
- name: Push coverage report
uses: paambaati/[email protected]
env:
CC_TEST_REPORTER_ID: ${{secrets.REPORTER_ID}}
with:
prefix: ${{github.workspace}}
coverageLocations: |
${{github.workspace}}/.coverage/:coverage.py
${{github.workspace}}/coverage.xml/:coverage.py
23 changes: 0 additions & 23 deletions maintain.svg

This file was deleted.

137 changes: 136 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ typer = "^0.12.5"
rich = "^13.9.2"
pydriller = "^2.6"
radon = "^6.0.1"
pydantic = "^2.9.2"

[tool.poetry.group.dev.dependencies]
ruff = "^0.6.9"
Expand Down
53 changes: 0 additions & 53 deletions src/asunder/analysis/risk_assement.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,59 +176,6 @@ def calculate_risk_score(self, commits, authors, complexity=0, length=0):
solo_author_bonus = (len(authors) == 1) * 2
return (commits * 0.5) + solo_author_bonus + (complexity * 0.1) + (length * 0.05)

def generate_hierarchical_data(self):
def create_node(name, full_path, risk_score, children=[]):
return {
"name": name,
"full_path": full_path,
"risk_score": risk_score,
"children": children,
}

hierarchical_data = []

for submodule, submodule_data in self.risk_data.items():
submodule_node = create_node(
os.path.basename(submodule),
submodule,
self.calculate_risk_score(submodule_data["commits"], submodule_data["authors"]),
)

for file_path, file_data in submodule_data["files"].items():
file_node = create_node(
os.path.basename(file_path),
file_path,
self.calculate_risk_score(file_data["commits"], file_data["authors"]),
)

for class_name, class_data in file_data["classes"].items():
class_node = create_node(
class_name,
f"{file_path}::{class_name}",
self.calculate_risk_score(class_data["commits"], class_data["authors"]),
)

for method_name, method_data in class_data["methods"].items():
method_node = create_node(
method_name,
f"{file_path}::{class_name}::{method_name}",
self.calculate_risk_score(
method_data["commits"],
method_data["authors"],
method_data["complexity"],
method_data["length"],
),
)
class_node["children"].append(method_node)

file_node["children"].append(class_node)

submodule_node["children"].append(file_node)

hierarchical_data.append(submodule_node)

return hierarchical_data

def generate_risk_report(self):
report = {"submodules": [], "files": [], "classes": [], "methods": []}

Expand Down
1 change: 1 addition & 0 deletions src/asunder/command/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from asunder._version import version_info
from asunder.command.analyze import app as analyze
from asunder.command.report import app as report
from asunder.command.extract import app as extract

from asunder.utils.logging import get_logger_console

Expand Down
53 changes: 53 additions & 0 deletions src/asunder/command/report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import logging
from pathlib import Path
from typing import Optional

import typer
from typer import Context

from asunder.utils.logging import get_logger_console
from asunder.utils.checks import is_url
from asunder.utils.data import search_repo_data
from asunder.report.report import generate_report

app = typer.Typer(add_completion=True, no_args_is_help=True)

logger = logging.getLogger("asunder")


@app.command(no_args_is_help=True)
def extract(
ctx: Context,
path: Path = typer.Option(Path.cwd(), help="path or url to package source code"),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line too long (85 > 79 characters)

repo_token: Optional[str] = typer.Option(
None,
"--git-token",
envvar="GITHUB_TOKEN",
help="Git personal access token for repository analysis",
),
) -> None:
logger, console = get_logger_console()

dry_run = ctx.obj.get("dry_run", True)

logger.info("Analyzing Repository")
if is_url:
logger.error("Online Analysis not implemented")
typer.Exit(code=1)
# analyze github or gitlab pull/merge requests, issues
if not repo_token:
raise ValueError(
"token required for repository analysis of pull requests and issues, you can define the env variable GITHUB_TOKEN"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line too long (130 > 79 characters)

)
if not path.exists():
logger.error(f"Error: Path not found: {path}")
raise typer.Exit(code=1)
# project = Project(path=path, console=console)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this commented out code.


repo_data = search_repo_data(path)
generate_report(repo_data)
if not dry_run:
logger.info("Perfoming Changes")
# perfom changes

typer.Exit()
2 changes: 2 additions & 0 deletions src/asunder/metrics/functional_metrics.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from radon import cc_visit

from asunder.metrics.process_metrics import Metric


class CycleComplexityMetric(Metric):
name: str = "complexity"
Expand Down
49 changes: 25 additions & 24 deletions src/asunder/metrics/process_metrics.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
from abc import abstractmethod

# from pydantic import BaseModel, Field
from typing import Dict
from typing import Any

from pydantic import BaseModel


# Base Metric class with Pydantic
class Metric(Protocol):
class Metric(BaseModel):
def __init__(self, name):
self.name = name

@abstractmethod
def calculate(self, **kwargs):
pass

@abstractmethod
def update(self, **kwargs):
pass

@abstractmethod
def reset(self):
pass

Expand Down Expand Up @@ -57,44 +54,44 @@ def reset(self):
self.total_lines = 0


class ChangeSetMetric(BaseMetric):
class ChangeSetMetric(Metric):
name: str = "change_set"

def calculate(self, data: Dict[str, Any]) -> float:
def calculate(self, data: dict[str, Any]) -> float:
# The size of the change set is often defined as the number of files modified in a commit
return data.get("change_set_size", 0)


class CodeChurnMetric(BaseMetric):
class CodeChurnMetric(Metric):
name: str = "code_churn"

def calculate(self, data: Dict[str, Any]) -> float:
def calculate(self, data: dict[str, Any]) -> float:
# Code churn is typically the sum of lines added and lines removed
additions = data.get("additions", 0)
deletions = data.get("deletions", 0)
return additions + deletions


class CommitsCountMetric(BaseMetric):
class CommitsCountMetric(Metric):
name: str = "commits_count"

def calculate(self, data: Dict[str, Any]) -> float:
def calculate(self, data: dict[str, Any]) -> float:
# Number of commits impacting a particular entity (submodule, file, etc.)
return len(data.get("commits", []))


class ContributorsCountMetric(BaseMetric):
class ContributorsCountMetric(Metric):
name: str = "contributors_count"

def calculate(self, data: Dict[str, Any]) -> float:
def calculate(self, data: dict[str, Any]) -> float:
# Number of unique contributors/authors
return len(set(data.get("authors", [])))


class ContributorsExperienceMetric(BaseMetric):
class ContributorsExperienceMetric(Metric):
name: str = "contributors_experience"

def calculate(self, data: Dict[str, Any]) -> float:
def calculate(self, data: dict[str, Any]) -> float:
# Calculate the average experience (number of commits) of each contributor
author_commits = data.get("author_commit_counts", {})
if not author_commits:
Expand All @@ -104,25 +101,29 @@ def calculate(self, data: Dict[str, Any]) -> float:
return total_commits / num_contributors if num_contributors > 0 else 0


class HunksCountMetric(BaseMetric):
class HunksCountMetric(Metric):
name: str = "hunks_count"

def calculate(self, data: Dict[str, Any]) -> float:
def calculate(self, data: dict[str, Any]) -> float:
# A hunk is typically a contiguous block of changes in a diff
return data.get("hunks_count", 0)


class LinesCountMetric(BaseMetric):
class LinesCountMetric(Metric):
name: str = "lines_count"

def calculate(self, data: Dict[str, Any]) -> float:
def calculate(self, data: dict[str, Any]) -> float:
# Total number of lines in the entity (could be file or class)
return data.get("lines_count", 0)


class BugFixingCommentsMetric(BaseMetric):
class BugFixingCommentsMetric(Metric):
name: str = "bug_fixing_comments"

def calculate(self, data: Dict[str, Any]) -> float:
def calculate(self, data: dict[str, Any]) -> float:
# Counting comments or commit messages that indicate a bug fix
return sum(1 for comment in data.get("comments", []) if "fix" in comment.lower() or "bug" in comment.lower())
return sum(
1
for comment in data.get("comments", [])
if "fix" in comment.lower() or "bug" in comment.lower()
)
Loading
Loading