Skip to content
Open
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
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ optional arguments:
--debug DEBUG display AST output for each element in the parsed tree
--sort SORT sort files from highest to lowest magnitude
--verbose VERBOSE display marked-up file
--format {text,json}
output format
```

Given `file.py` that contains the following text:
Expand Down Expand Up @@ -72,6 +74,33 @@ $ python -m python_abc /path/to/file.py
/path/to/file.py <1, 7, 10> (12.2)
```

You can also emit machine-readable JSON:

```bash
$ python -m python_abc /path/to/file.py --format json
{
"path": "/path/to/file.py",
"files": [
{
"path": "/path/to/file.py",
"status": "ok",
"vector": {
"assignment": 1,
"branch": 7,
"condition": 10
},
"magnitude": 12.2,
"error": null
}
],
"summary": {
"files": 1,
"parsed": 1,
"syntax_errors": 0
}
}
```

Passing the `verbose` flag will give more detail:

```bash
Expand Down
30 changes: 20 additions & 10 deletions python_abc/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from joblib import Parallel, delayed

from python_abc.calculate import calculate_abc
from python_abc.output import AnalysisResult, render_json, render_text


def main():
Expand Down Expand Up @@ -39,8 +40,18 @@ def main():
parser.add_argument(
"--verbose", dest="verbose", action="store_true", help="display marked-up file",
)
parser.add_argument(
"--format",
dest="format",
choices=("text", "json"),
default="text",
help="output format",
)

args = vars(parser.parse_args())
if args["format"] == "json" and (args["debug"] or args["verbose"]):
parser.error("--format json cannot be used with --debug or --verbose")

path = args["path"][0]
files: List[str] = []

Expand All @@ -55,31 +66,30 @@ def main():
else:
files.append(path)

max_path_length = max(len(file) for file in files)

def analyze_file(filename):
with open(filename, "r") as f:
source = f.read()

try:
abc_vector = calculate_abc(source, args["debug"], args["verbose"])
except SyntaxError:
return (filename, None, 0.0)
return AnalysisResult(filename, None, 0.0)
else:
return (filename, abc_vector, abc_vector.get_magnitude_value())
return AnalysisResult(
filename, abc_vector, abc_vector.get_magnitude_value()
)

output = Parallel(n_jobs=args["cores"])(
delayed(analyze_file)(filename) for filename in files
)

if args["sort"] is True:
output.sort(key=lambda x: x[2], reverse=True)
output.sort(key=lambda result: result.sort_magnitude, reverse=True)

for filename, vector, _ in output:
if vector is None:
print(f"{filename:<{max_path_length}} {'Unable to parse AST':>26}")
else:
print(f"{filename:<{max_path_length}} {vector.magnitude:>26}")
if args["format"] == "json":
print(render_json(path, output))
else:
print(render_text(output))


if __name__ == "__main__":
Expand Down
75 changes: 75 additions & 0 deletions python_abc/output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import json
from dataclasses import dataclass
from typing import List, Optional

from python_abc import vector


@dataclass
class AnalysisResult:
path: str
vector: Optional[vector.Vector]
sort_magnitude: float

@property
def status(self) -> str:
if self.vector is None:
return "syntax_error"

return "ok"


def render_text(results: List[AnalysisResult]) -> str:
max_path_length = max((len(result.path) for result in results), default=0)
lines = []

for result in results:
if result.vector is None:
lines.append(f"{result.path:<{max_path_length}} {'Unable to parse AST':>26}")
else:
lines.append(f"{result.path:<{max_path_length}} {result.vector.magnitude:>26}")

return "\n".join(lines)


def render_json(path: str, results: List[AnalysisResult]) -> str:
files = []

for result in results:
if result.vector is None:
files.append(
{
"path": result.path,
"status": result.status,
"vector": None,
"magnitude": None,
"error": "Unable to parse AST",
}
)
else:
files.append(
{
"path": result.path,
"status": result.status,
"vector": {
"assignment": result.vector.assignment,
"branch": result.vector.branch,
"condition": result.vector.condition,
},
"magnitude": result.vector.get_magnitude_value(),
"error": None,
}
)

syntax_errors = sum(1 for result in results if result.vector is None)
payload = {
"path": path,
"files": files,
"summary": {
"files": len(results),
"parsed": len(results) - syntax_errors,
"syntax_errors": syntax_errors,
},
}

return json.dumps(payload, indent=2)
58 changes: 58 additions & 0 deletions tests/test_output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import json

from python_abc import vector
from python_abc.output import AnalysisResult, render_json, render_text


def test_render_text():
results = [
AnalysisResult("file.py", vector.Vector(1, 2, 3), 3.7),
AnalysisResult("bad.py", None, 0.0),
]

assert render_text(results) == "\n".join(
[
"file.py <1, 2, 3> (3.7)",
"bad.py Unable to parse AST",
]
)


def test_render_json():
results = [
AnalysisResult("file.py", vector.Vector(1, 2, 3), 3.7),
AnalysisResult("bad.py", None, 0.0),
]

payload = json.loads(render_json("src", results))

assert payload == {
"path": "src",
"files": [
{
"path": "file.py",
"status": "ok",
"vector": {
"assignment": 1,
"branch": 2,
"condition": 3,
},
"magnitude": 3.7,
"error": None,
},
{
"path": "bad.py",
"status": "syntax_error",
"vector": None,
"magnitude": None,
"error": "Unable to parse AST",
},
],
"summary": {
"files": 2,
"parsed": 1,
"syntax_errors": 1,
},
}

assert "display" not in payload["files"][0]