Skip to content

Commit

Permalink
Merge 4aec43e into 6dddbd7
Browse files Browse the repository at this point in the history
  • Loading branch information
MReinisch committed Sep 21, 2020
2 parents 6dddbd7 + 4aec43e commit 9542239
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 3 deletions.
Empty file modified setup.py 100644 → 100755
Empty file.
193 changes: 190 additions & 3 deletions src/black/__init__.py
Expand Up @@ -70,7 +70,7 @@
CACHE_DIR = Path(user_cache_dir("black", version=__version__))

STRING_PREFIX_CHARS: Final = "furbFURB" # All possible string prefix characters.

DEFAULT_REPORT = "console"

# types
FileContent = str
Expand Down Expand Up @@ -164,6 +164,11 @@ def from_configuration(
return cls.DIFF if diff else cls.YES


class ReportType(Enum):
CONSOLE = "report"
JUNIT = "junit"


class Changed(Enum):
NO = 0
CACHED = 1
Expand Down Expand Up @@ -357,6 +362,17 @@ def target_version_option_callback(
return [TargetVersion[val.upper()] for val in v]


def report_option_callback(
c: click.Context, p: Union[click.Option, click.Parameter], v: str
) -> ReportType:
"""Compute the report type from a --report flag.
This is its own function because mypy couldn't infer the type correctly
when it was a lambda, causing mypyc trouble.
"""
return ReportType[v.upper()]


@click.command(context_settings=dict(help_option_names=["-h", "--help"]))
@click.option("-c", "--code", type=str, help="Format the code passed in as a string.")
@click.option(
Expand Down Expand Up @@ -498,6 +514,25 @@ def target_version_option_callback(
callback=read_pyproject_toml,
help="Read configuration from FILE path.",
)
@click.option(
"--report-type",
type=click.Choice([v.name.lower() for v in ReportType]),
callback=report_option_callback,
default=DEFAULT_REPORT,
multiple=False,
help=(
"Option if the generated Report should be to the Console or a Junit XML File.\n"
"[default : console]"
),
)
@click.option(
"--junitxml",
type=click.File(mode="w"),
help=(
"Option is needed for --report-type junit it tells where to store the XML"
" Output.\nIf directory is not present it will be created"
),
)
@click.pass_context
def main(
ctx: click.Context,
Expand All @@ -518,6 +553,8 @@ def main(
force_exclude: Optional[str],
src: Tuple[str, ...],
config: Optional[str],
report_type: str,
junitxml: click.utils.LazyFile,
) -> None:
"""The uncompromising code formatter."""
write_back = WriteBack.from_configuration(check=check, diff=diff, color=color)
Expand All @@ -538,7 +575,18 @@ def main(
if code is not None:
print(format_str(code, mode=mode))
ctx.exit(0)
report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose)
if report_type is ReportType.JUNIT:
if junitxml is None:
print("Please provide a Filename for the JunitXML Report")
ctx.exit(1)
junitxml_path = os.path.dirname(os.path.abspath(junitxml.name))
if not os.path.exists(junitxml_path):
if not os.makedirs(junitxml_path):
print(f"Could not create path for JunitXML Report: {junitxml_path}")
ctx.exit(1)
report = JunitReport(check=check, diff=diff, quiet=quiet, verbose=verbose)
else:
report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose)
sources = get_sources(
ctx=ctx,
src=src,
Expand Down Expand Up @@ -573,7 +621,11 @@ def main(

if verbose or not quiet:
out("Oh no! 💥 💔 💥" if report.return_code else "All done! ✨ 🍰 ✨")
click.secho(str(report), err=True)
if isinstance(report, JunitReport):
junitxml.write(str(report)) # type: ignore
click.secho(str(report.summary()), err=True)
else:
click.secho(str(report), err=True)
ctx.exit(report.return_code)


Expand Down Expand Up @@ -6144,6 +6196,141 @@ def __str__(self) -> str:
return ", ".join(report) + "."


@dataclass
class JunitReport:
"""Provides a JunitXml formatted string that can be saved to a file"""

check: bool = False
diff: bool = False
quiet: bool = False
verbose: bool = False
change_count: int = 0
same_count: int = 0
skipped_count: int = 0
failure_count: int = 0
tests: List[str] = field(default_factory=list)

BODY = """<?xml version="1.0" encoding="utf-8"?>
<testsuite failures="{failed}" errors="{errors}" name="black" skipped="{skipped}" tests="{combined}">
{tests}</testsuite>"""
PASS_MSG = """\t<testcase classname="black" file="{file}" name="black-{file}"></testcase>\n"""
SKIP_MSG = """\t<testcase classname="black" file="{file}" name="black-{file}">
<skipped message="{msg}" />
</testcase>\n"""
FAIL_MSG = """\t<testcase classname="black" file="{file}" name="black-{file}">
<failure message="{msg}" />
</testcase>\n"""
ERROR_MSG = """\t<testcase classname="black" file="{file}" name="black-{file}">
<error message="{msg}" />
</testcase>\n"""

def done(self, src: Path, changed: Changed) -> None:
"""Increments the failure_counter if a file would be changed
and append the testcase section to the tests summary.
If the File would not be changed because already good formatted
or not changed since last run.
It creates a Pass testcase section in the tests summary and increment same_counter"""
if changed is Changed.YES:
reformatted = "would reformat" if self.check or self.diff else "reformatted"
if self.verbose or not self.quiet:
self.tests.append(self.FAIL_MSG.format(file=src, msg=reformatted))
self.change_count += 1
else:
if changed is Changed.NO:
msg = f"{src} already well formatted, good job."
self.tests.append(self.PASS_MSG.format(file=src, msg=msg))
else:
msg = f"{src} wasn't modified on disk since last run."
self.tests.append(self.PASS_MSG.format(file=src, msg=msg))
self.same_count += 1

def failed(self, src: Path, message: str) -> None:
"""Increment the counter for error reformatting. Adds a error Testcase section in tests summary."""
self.tests.append(
self.ERROR_MSG.format(
file=src, msg=f"error: cannot format {src}: {message}"
)
)
self.failure_count += 1

def path_ignored(self, path: Path, message: str) -> None:
"""Increment the counter for skipped reformatting. Adds a skipped Testcase section in tests summary."""
if self.verbose:
self.tests.append(
self.SKIP_MSG.format(file=path, msg=f"{path} ignored: {message}")
)
self.skipped_count += 1

@property
def return_code(self) -> int:
"""Return the exit code that the app should use.
This considers the current state of changed files and failures:
- if there were any failures, return 123;
- if any files were changed and --check is being used, return 1;
- otherwise return 0.
"""
# According to http://tldp.org/LDP/abs/html/exitcodes.html starting with
# 126 we have special return codes reserved by the shell.
if self.failure_count:
return 123

elif self.change_count and self.check:
return 1

return 0

def __str__(self) -> str:
"""
Combines the Body with the testcases sections to a JunitXML and returns it.
"""
combined_tests = (
self.change_count
+ self.same_count
+ self.failure_count
+ self.skipped_count
)
report = [
self.BODY.format(
failed=self.change_count,
errors=self.failure_count,
skipped=self.skipped_count,
combined=combined_tests,
tests="".join(self.tests),
)
]
return "".join(report)

def summary(self) -> str:
"""Render a color report of the current state.
Use `click.unstyle` to remove colors.
"""
if self.check or self.diff:
reformatted = "would be reformatted"
unchanged = "would be left unchanged"
failed = "would fail to reformat"
else:
reformatted = "reformatted"
unchanged = "left unchanged"
failed = "failed to reformat"
report = []
if self.change_count:
s = "s" if self.change_count > 1 else ""
report.append(
click.style(f"{self.change_count} file{s} {reformatted}", bold=True)
)
if self.same_count:
s = "s" if self.same_count > 1 else ""
report.append(f"{self.same_count} file{s} {unchanged}")
if self.failure_count:
s = "s" if self.failure_count > 1 else ""
report.append(
click.style(f"{self.failure_count} file{s} {failed}", fg="red")
)
return ", ".join(report) + "."


def parse_ast(src: str) -> Union[ast.AST, ast3.AST, ast27.AST]:
filename = "<unknown>"
if sys.version_info >= (3, 8):
Expand Down

0 comments on commit 9542239

Please sign in to comment.