diff-cover 10.4.1__tar.gz → 10.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. {diff_cover-10.4.1 → diff_cover-10.5.0}/PKG-INFO +16 -1
  2. {diff_cover-10.4.1 → diff_cover-10.5.0}/README.rst +15 -0
  3. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/diff_cover_tool.py +15 -2
  4. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/diff_quality_tool.py +29 -5
  5. diff_cover-10.5.0/diff_cover/hookspecs.py +16 -0
  6. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/violationsreporters/violations_reporter.py +47 -10
  7. {diff_cover-10.4.1 → diff_cover-10.5.0}/pyproject.toml +2 -2
  8. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_diff_cover_tool.py +28 -0
  9. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_diff_quality_main.py +46 -1
  10. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_violations_reporter.py +64 -1
  11. diff_cover-10.4.1/diff_cover/hookspecs.py +0 -12
  12. {diff_cover-10.4.1 → diff_cover-10.5.0}/LICENSE +0 -0
  13. {diff_cover-10.4.1 → diff_cover-10.5.0}/NOTICE +0 -0
  14. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/__init__.py +0 -0
  15. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/command_runner.py +0 -0
  16. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/config_parser.py +0 -0
  17. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/diff_reporter.py +0 -0
  18. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/git_diff.py +0 -0
  19. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/git_path.py +0 -0
  20. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/hook.py +0 -0
  21. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/report_generator.py +0 -0
  22. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/snippets.py +0 -0
  23. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/templates/console_coverage_report.txt +0 -0
  24. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/templates/console_quality_report.txt +0 -0
  25. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/templates/external_style.css +0 -0
  26. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/templates/github_coverage_annotations.txt +0 -0
  27. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/templates/html_coverage_report.html +0 -0
  28. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/templates/html_quality_report.html +0 -0
  29. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/templates/markdown_coverage_report.md +0 -0
  30. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/templates/markdown_quality_report.md +0 -0
  31. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/templates/snippet_content.html +0 -0
  32. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/templates/snippet_content.md +0 -0
  33. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/templates/snippet_content.txt +0 -0
  34. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/templates/snippet_style.html +0 -0
  35. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/util.py +0 -0
  36. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/violationsreporters/__init__.py +0 -0
  37. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/violationsreporters/base.py +0 -0
  38. {diff_cover-10.4.1 → diff_cover-10.5.0}/diff_cover/violationsreporters/java_violations_reporter.py +0 -0
  39. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/__init__.py +0 -0
  40. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/conftest.py +0 -0
  41. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/add_console_report.txt +0 -0
  42. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/add_html_report.html +0 -0
  43. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/add_json_report.json +0 -0
  44. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/add_json_report_float.json +0 -0
  45. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/add_markdown_report.md +0 -0
  46. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/changed_console_report.txt +0 -0
  47. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/changed_html_report.html +0 -0
  48. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/coverage.xml +0 -0
  49. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/coverage1.xml +0 -0
  50. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/coverage2.xml +0 -0
  51. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/coverage_missing_lines.xml +0 -0
  52. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/cpp_functions_console_report.txt +0 -0
  53. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/cpp_functions_coverage.lcov +0 -0
  54. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/delete_console_report.txt +0 -0
  55. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/delete_html_report.html +0 -0
  56. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/dotnet_coverage.xml +0 -0
  57. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/empty.txt +0 -0
  58. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/empty_pycodestyle_violations.txt +0 -0
  59. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/expand_console_report.txt +0 -0
  60. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/external_css_html_report.html +0 -0
  61. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/external_style.css +0 -0
  62. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_add.txt +0 -0
  63. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_changed.txt +0 -0
  64. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_code_dupe.txt +0 -0
  65. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_cpp.txt +0 -0
  66. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_cpp_functions.txt +0 -0
  67. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_delete.txt +0 -0
  68. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_dotnet.txt +0 -0
  69. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_external_css.txt +0 -0
  70. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_lua.txt +0 -0
  71. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_moved.txt +0 -0
  72. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_mult.txt +0 -0
  73. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_python.txt +0 -0
  74. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_subdir.txt +0 -0
  75. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_typescript.txt +0 -0
  76. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_typescript_branches.txt +0 -0
  77. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_unicode.txt +0 -0
  78. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_violations.txt +0 -0
  79. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/git_diff_violations_two_files.txt +0 -0
  80. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/hello.py +0 -0
  81. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/hi.py +0 -0
  82. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/html_report.html +0 -0
  83. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/html_report_empty.html +0 -0
  84. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/html_report_one_snippet.html +0 -0
  85. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/html_report_two_snippets.html +0 -0
  86. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/lcov.info +0 -0
  87. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/lua_console_report.txt +0 -0
  88. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/luacoverage.xml +0 -0
  89. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/markdown_report_one_snippet.md +0 -0
  90. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/markdown_report_two_snippets.md +0 -0
  91. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/moved_console_report.txt +0 -0
  92. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/moved_coverage.xml +0 -0
  93. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/moved_html_report.html +0 -0
  94. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/mult_inputs_console_report.txt +0 -0
  95. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/mult_inputs_html_report.html +0 -0
  96. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pycodestyle_report.txt +0 -0
  97. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pycodestyle_violations_report.html +0 -0
  98. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pycodestyle_violations_report.json +0 -0
  99. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pycodestyle_violations_report.md +0 -0
  100. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pycodestyle_violations_report.txt +0 -0
  101. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pycodestyle_violations_report_external_css.html +0 -0
  102. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pycodestyle_violations_report_float.json +0 -0
  103. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pyflakes_two_files.txt +0 -0
  104. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pyflakes_violations_report.html +0 -0
  105. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pyflakes_violations_report.txt +0 -0
  106. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pylint_dupe.txt +0 -0
  107. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pylint_dupe_violations_report.txt +0 -0
  108. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pylint_report.txt +0 -0
  109. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pylint_violations_console_report.txt +0 -0
  110. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pylint_violations_report.html +0 -0
  111. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pylint_violations_report.txt +0 -0
  112. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/pylintrc +0 -0
  113. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/real_cpp_console_report.txt +0 -0
  114. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/real_cpp_coverage.lcov +0 -0
  115. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/real_python_console_report.txt +0 -0
  116. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/real_python_coverage.lcov +0 -0
  117. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/real_typescript_console_report.txt +0 -0
  118. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/real_typescript_coverage.lcov +0 -0
  119. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/show_uncovered_lines_console.txt +0 -0
  120. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet.css +0 -0
  121. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_8859.py +0 -0
  122. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_arabic_output.html +0 -0
  123. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_default.html +0 -0
  124. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_invalid_violations.html +0 -0
  125. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_list.html +0 -0
  126. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_list.md +0 -0
  127. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_list2.md +0 -0
  128. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_list3.md +0 -0
  129. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_no_filename_ext.html +0 -0
  130. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_src.py +0 -0
  131. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_src2.cpp +0 -0
  132. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_src3.cpp +0 -0
  133. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_unicode.html +0 -0
  134. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_unicode.py +0 -0
  135. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/snippet_unicode_html_output.html +0 -0
  136. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/subdir_coverage_console_report.txt +0 -0
  137. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/subdir_coverage_html_report.html +0 -0
  138. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/test_src.txt +0 -0
  139. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/typescript_branches_console_report.txt +0 -0
  140. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/typescript_branches_coverage.lcov +0 -0
  141. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/unicode_console_report.txt +0 -0
  142. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/unicode_coverage.xml +0 -0
  143. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/unicode_html_report.html +0 -0
  144. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/unicode_test_src.txt +0 -0
  145. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/fixtures/violations_test_file.py +0 -0
  146. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/helpers.py +0 -0
  147. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/snippet_list_unicode.html +0 -0
  148. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_clover_violations_reporter/test.xml +0 -0
  149. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_clover_violations_reporter.py +0 -0
  150. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_config_parser.py +0 -0
  151. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_diff_cover_main.py +0 -0
  152. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_diff_reporter.py +0 -0
  153. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_git_diff.py +0 -0
  154. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_git_diff_file.py +0 -0
  155. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_git_path.py +0 -0
  156. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_integration.py +0 -0
  157. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_java_violations_reporter.py +0 -0
  158. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_report_generator.py +0 -0
  159. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_snippets.py +0 -0
  160. {diff_cover-10.4.1 → diff_cover-10.5.0}/tests/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: diff_cover
3
- Version: 10.4.1
3
+ Version: 10.5.0
4
4
  Summary: Run coverage and linting reports on diffs
5
5
  License: Apache-2.0
6
6
  Author: See Contributors
@@ -606,6 +606,21 @@ Important notes:
606
606
  * Marked with the ``@diff_cover_hookimpl`` decorator
607
607
  * Named ``diff_cover_report_quality``. (This distinguishes it from any other
608
608
  plugin types ``diff_cover`` may support.)
609
+ * The function may declare either, both, or neither of the hook's arguments:
610
+
611
+ * ``reports`` - the list of open file handles for the pre-generated reports
612
+ passed on the command line.
613
+ * ``options`` - the string given to ``--options``.
614
+
615
+ Only the arguments you declare are passed, so the zero-argument form above
616
+ keeps working. A plugin that wants the user's input would be written as:
617
+
618
+ .. code:: python
619
+
620
+ @diff_cover_hookimpl
621
+ def diff_cover_report_quality(reports, options):
622
+ return SQLFluffViolationReporter(reports=reports, options=options)
623
+
609
624
  * The function should return an object with the following properties and methods:
610
625
 
611
626
  * ``supported_extensions`` property with a list of supported file extensions
@@ -574,6 +574,21 @@ Important notes:
574
574
  * Marked with the ``@diff_cover_hookimpl`` decorator
575
575
  * Named ``diff_cover_report_quality``. (This distinguishes it from any other
576
576
  plugin types ``diff_cover`` may support.)
577
+ * The function may declare either, both, or neither of the hook's arguments:
578
+
579
+ * ``reports`` - the list of open file handles for the pre-generated reports
580
+ passed on the command line.
581
+ * ``options`` - the string given to ``--options``.
582
+
583
+ Only the arguments you declare are passed, so the zero-argument form above
584
+ keeps working. A plugin that wants the user's input would be written as:
585
+
586
+ .. code:: python
587
+
588
+ @diff_cover_hookimpl
589
+ def diff_cover_report_quality(reports, options):
590
+ return SQLFluffViolationReporter(reports=reports, options=options)
591
+
577
592
  * The function should return an object with the following properties and methods:
578
593
 
579
594
  * ``supported_extensions`` property with a list of supported file extensions
@@ -76,6 +76,16 @@ def format_type(value):
76
76
  return dict((item.split(":", 1) for item in value.split(",")) if value else {})
77
77
 
78
78
 
79
+ def normalize_format(config):
80
+ """
81
+ Config files provide ``format`` as a raw string, because ``type=`` is only
82
+ applied to values that come from the command line. Parse it the same way.
83
+ """
84
+ if isinstance(config.get("format"), str):
85
+ config["format"] = format_type(config["format"])
86
+ return config
87
+
88
+
79
89
  def parse_coverage_args(argv):
80
90
  """
81
91
  Parse command line arguments, returning a dict of
@@ -99,7 +109,7 @@ def parse_coverage_args(argv):
99
109
  parser.add_argument(
100
110
  "--format",
101
111
  type=format_type,
102
- default="",
112
+ default=None,
103
113
  help=FORMAT_HELP,
104
114
  )
105
115
 
@@ -217,6 +227,7 @@ def parse_coverage_args(argv):
217
227
  "show_uncovered": False,
218
228
  "show_covered": False,
219
229
  "compare_branch": "origin/main",
230
+ "format": {},
220
231
  "fail_under": 0,
221
232
  "ignore_staged": False,
222
233
  "ignore_unstaged": False,
@@ -230,7 +241,9 @@ def parse_coverage_args(argv):
230
241
  "total_percent_float": False,
231
242
  }
232
243
 
233
- return get_config(parser=parser, argv=argv, defaults=defaults, tool=Tool.DIFF_COVER)
244
+ return normalize_format(
245
+ get_config(parser=parser, argv=argv, defaults=defaults, tool=Tool.DIFF_COVER)
246
+ )
234
247
 
235
248
 
236
249
  def generate_coverage_report(
@@ -4,6 +4,7 @@ Implement the command-line tool interface for diff_quality.
4
4
 
5
5
  import argparse
6
6
  import contextlib
7
+ import inspect
7
8
  import io
8
9
  import logging
9
10
  import os
@@ -33,6 +34,7 @@ from diff_cover.diff_cover_tool import (
33
34
  TOTAL_PERCENT_FLOAT_HELP,
34
35
  format_type,
35
36
  handle_old_format,
37
+ normalize_format,
36
38
  )
37
39
  from diff_cover.diff_reporter import GitDiffReporter
38
40
  from diff_cover.git_diff import GitDiffTool
@@ -119,7 +121,7 @@ def parse_quality_args(argv):
119
121
  parser.add_argument(
120
122
  "--format",
121
123
  type=format_type,
122
- default="",
124
+ default=None,
123
125
  help=FORMAT_HELP,
124
126
  )
125
127
 
@@ -211,6 +213,7 @@ def parse_quality_args(argv):
211
213
  defaults = {
212
214
  "ignore_whitespace": False,
213
215
  "compare_branch": "origin/main",
216
+ "format": {},
214
217
  "diff_range_notation": "...",
215
218
  "input_reports": [],
216
219
  "fail_under": 0,
@@ -221,8 +224,8 @@ def parse_quality_args(argv):
221
224
  "total_percent_float": False,
222
225
  }
223
226
 
224
- return get_config(
225
- parser=parser, argv=argv, defaults=defaults, tool=Tool.DIFF_QUALITY
227
+ return normalize_format(
228
+ get_config(parser=parser, argv=argv, defaults=defaults, tool=Tool.DIFF_QUALITY)
226
229
  )
227
230
 
228
231
 
@@ -297,6 +300,27 @@ def generate_quality_report(
297
300
  return reporter.total_percent_covered()
298
301
 
299
302
 
303
+ def _call_reporter_factory(factory_fn, reports, options):
304
+ """
305
+ Call a plugin's ``diff_cover_report_quality`` implementation.
306
+
307
+ Plugins are free to declare only the arguments they need (including none
308
+ at all, as documented in the README), so pass only what the function
309
+ actually accepts.
310
+ """
311
+ available = {"reports": reports, "options": options}
312
+ try:
313
+ parameters = inspect.signature(factory_fn).parameters
314
+ except (TypeError, ValueError): # pragma: no cover - builtins/C callables
315
+ return factory_fn(**available)
316
+
317
+ if any(p.kind is inspect.Parameter.VAR_KEYWORD for p in parameters.values()):
318
+ return factory_fn(**available)
319
+
320
+ kwargs = {name: value for name, value in available.items() if name in parameters}
321
+ return factory_fn(**kwargs)
322
+
323
+
300
324
  def main(argv=None, directory=None):
301
325
  """
302
326
  Main entry point for the tool, script installed via pyproject.toml
@@ -366,8 +390,8 @@ def main(argv=None, directory=None):
366
390
 
367
391
  reporter = QualityReporter(driver, input_reports, user_options)
368
392
  elif reporter_factory_fn:
369
- reporter = reporter_factory_fn(
370
- reports=input_reports, options=user_options
393
+ reporter = _call_reporter_factory(
394
+ reporter_factory_fn, input_reports, user_options
371
395
  )
372
396
 
373
397
  percent_passing = generate_quality_report(
@@ -0,0 +1,16 @@
1
+ import pluggy
2
+
3
+ hookspec = pluggy.HookspecMarker("diff_cover")
4
+
5
+
6
+ @hookspec
7
+ def diff_cover_report_quality(reports, options): # pylint: disable=unused-argument
8
+ """
9
+ Return a 2-part tuple:
10
+ - Quality plugin name
11
+ - Object that implements the BaseViolationReporter protocol
12
+
13
+ ``reports`` is the list of open pre-generated report file handles and
14
+ ``options`` the user options string; both are passed by ``diff-quality``.
15
+ A plugin may declare either or neither argument.
16
+ """
@@ -46,6 +46,11 @@ class XmlCoverageReporter(BaseViolationReporter):
46
46
  # Values are output of `self._get_xml_classes()`
47
47
  self._xml_cache = [{} for i in range(len(xml_roots))]
48
48
 
49
+ # Which format each report is in. Classifying costs a search through the
50
+ # document, and the answer cannot change, so do it once here rather than
51
+ # once per source file in `_cache_file`.
52
+ self._report_formats = [self._detect_report_format(root) for root in xml_roots]
53
+
49
54
  self._src_roots = src_roots or [""]
50
55
  self._expand_coverage_report = expand_coverage_report
51
56
  self._branch_coverage = branch_coverage
@@ -129,19 +134,53 @@ class XmlCoverageReporter(BaseViolationReporter):
129
134
  If file is not present in `xml_document`, return None
130
135
  """
131
136
 
132
- files = [
133
- file_tree
134
- for file_tree in xml_document.findall(".//file")
135
- if GitPathTool.relative_path(file_tree.get("path")) == src_path
136
- ]
137
+ files = []
138
+ normalized_src_path = util.to_unix_path(src_path)
139
+ for file_tree in xml_document.findall(".//file"):
140
+ file_path = file_tree.get("path") or file_tree.get("name")
141
+ if not file_path:
142
+ continue
143
+
144
+ normalized_file_path = util.to_unix_path(file_path)
145
+ relative_file_path = util.to_unix_path(GitPathTool.relative_path(file_path))
146
+ if (
147
+ relative_file_path == normalized_src_path
148
+ or normalized_file_path.endswith(f"/{normalized_src_path}")
149
+ ):
150
+ files.append(file_tree)
137
151
  if not files:
138
152
  return None
139
153
  lines = []
140
154
  for file_tree in files:
155
+ # Clover marks an executable line as one of these three types. PHPUnit's
156
+ # writer emits `method` for the declaration line of every function it
157
+ # measured; leaving it out reported those lines as unmeasured.
158
+ # https://github.com/sebastianbergmann/php-code-coverage/blob/main/src/Report/Clover.php
159
+ lines.append(file_tree.findall('./line[@type="method"]'))
141
160
  lines.append(file_tree.findall('./line[@type="stmt"]'))
142
161
  lines.append(file_tree.findall('./line[@type="cond"]'))
143
162
  return list(itertools.chain(*lines))
144
163
 
164
+ @staticmethod
165
+ def _detect_report_format(xml_document):
166
+ """
167
+ Return which of the supported formats `xml_document` is in.
168
+
169
+ Clover writes a `clover` attribute on the root, but PHPUnit's Clover
170
+ writer does not, so fall back to the shape of its line elements.
171
+ """
172
+ if (
173
+ xml_document.findall(".[@clover]")
174
+ or xml_document.find(".//file/line[@num][@count]") is not None
175
+ ):
176
+ # see etc/schema/clover.xsd at https://bitbucket.org/atlassian/clover/src
177
+ return "clover"
178
+ if xml_document.findall(".[@name]"):
179
+ # https://github.com/jacoco/jacoco/blob/master/org.jacoco.report/src/org/jacoco/report/xml/report.dtd
180
+ return "jacoco"
181
+ # https://github.com/cobertura/web/blob/master/htdocs/xml/coverage-04.dtd
182
+ return "cobertura"
183
+
145
184
  def _measured_source_path_matches(self, package_name, file_name, src_path):
146
185
  # find src_path in any of the source roots
147
186
  if not src_path.endswith(util.to_unix_path(file_name)):
@@ -204,22 +243,20 @@ class XmlCoverageReporter(BaseViolationReporter):
204
243
 
205
244
  # Loop through the files that contain the xml roots
206
245
  for i, xml_document in enumerate(self._xml_roots):
207
- if xml_document.findall(".[@clover]"):
208
- # see etc/schema/clover.xsd at https://bitbucket.org/atlassian/clover/src
246
+ report_format = self._report_formats[i]
247
+ if report_format == "clover":
209
248
  line_nodes = self.get_src_path_line_nodes_clover(
210
249
  xml_document, src_path
211
250
  )
212
251
  _number = "num"
213
252
  _hits = "count"
214
- elif xml_document.findall(".[@name]"):
215
- # https://github.com/jacoco/jacoco/blob/master/org.jacoco.report/src/org/jacoco/report/xml/report.dtd
253
+ elif report_format == "jacoco":
216
254
  line_nodes = self.get_src_path_line_nodes_jacoco(
217
255
  xml_document, src_path
218
256
  )
219
257
  _number = "nr"
220
258
  _hits = "ci"
221
259
  else:
222
- # https://github.com/cobertura/web/blob/master/htdocs/xml/coverage-04.dtd
223
260
  line_nodes = self.get_src_path_line_nodes_cobertura(
224
261
  i, xml_document, src_path
225
262
  )
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "diff_cover"
3
- version = "10.4.1"
3
+ version = "10.5.0"
4
4
  description = "Run coverage and linting reports on diffs"
5
5
  authors = ["See Contributors"]
6
6
  homepage = "https://github.com/Bachmann1234/diff-cover"
@@ -61,7 +61,7 @@ pydocstyle = "^6.1.1"
61
61
  black = "^26.1.0"
62
62
  isort = ">=6.0.1,<9.0.0"
63
63
  doc8 = "2.0.0"
64
- ruff = ">=0.11.10,<0.16.0"
64
+ ruff = ">=0.11.10,<0.17.0"
65
65
 
66
66
  [tool.poetry.extras]
67
67
  toml = ["tomli"]
@@ -156,3 +156,31 @@ def test_parse_with_include():
156
156
 
157
157
  def test_parse_with_exclude():
158
158
  _test_parse_with_path_patterns("exclude")
159
+
160
+
161
+ def test_parse_format_from_config_file(tmp_path):
162
+ config_file = tmp_path / "diff_cover.toml"
163
+ config_file.write_text('[tool.diff_cover]\nformat = "html:report.html"\n')
164
+
165
+ arg_dict = parse_coverage_args(
166
+ ["reports/coverage.xml", "--config-file", str(config_file)]
167
+ )
168
+
169
+ assert arg_dict["format"] == {"html": "report.html"}
170
+
171
+
172
+ def test_parse_format_cli_overrides_config_file(tmp_path):
173
+ config_file = tmp_path / "diff_cover.toml"
174
+ config_file.write_text('[tool.diff_cover]\nformat = "html:report.html"\n')
175
+
176
+ arg_dict = parse_coverage_args(
177
+ [
178
+ "reports/coverage.xml",
179
+ "--config-file",
180
+ str(config_file),
181
+ "--format",
182
+ "json:report.json",
183
+ ]
184
+ )
185
+
186
+ assert arg_dict["format"] == {"json": "report.json"}
@@ -2,9 +2,15 @@
2
2
 
3
3
  """Test for diff_cover.diff_quality - main"""
4
4
 
5
+ import pluggy
5
6
  import pytest
6
7
 
7
- from diff_cover.diff_quality_tool import main, parse_quality_args
8
+ from diff_cover import hookspecs
9
+ from diff_cover.diff_quality_tool import (
10
+ _call_reporter_factory,
11
+ main,
12
+ parse_quality_args,
13
+ )
8
14
 
9
15
 
10
16
  def test_parse_with_html_report():
@@ -147,3 +153,42 @@ def _run_main(report, argv):
147
153
  quality_reporter = report.call_args[0][0]
148
154
  assert quality_reporter.driver.name == "pylint"
149
155
  assert quality_reporter.options == "--foobar"
156
+
157
+
158
+ def test_parse_format_from_config_file(tmp_path):
159
+ config_file = tmp_path / "diff_quality.toml"
160
+ config_file.write_text('[tool.diff_quality]\nformat = "html:report.html"\n')
161
+
162
+ arg_dict = parse_quality_args(
163
+ ["--violations", "pylint", "--config-file", str(config_file)]
164
+ )
165
+
166
+ assert arg_dict.get("format") == {"html": "report.html"}
167
+
168
+
169
+ def test_plugin_may_declare_hook_arguments():
170
+ """A plugin declaring reports/options must validate against the hookspec."""
171
+ hookimpl = pluggy.HookimplMarker("diff_cover")
172
+
173
+ class Plugin:
174
+ @hookimpl
175
+ def diff_cover_report_quality(self, reports, options):
176
+ return (reports, options)
177
+
178
+ plugin_manager = pluggy.PluginManager("diff_cover")
179
+ plugin_manager.add_hookspecs(hookspecs)
180
+ plugin_manager.register(Plugin(), name="myplugin")
181
+
182
+
183
+ @pytest.mark.parametrize(
184
+ "factory,expected",
185
+ [
186
+ (lambda: "none", "none"),
187
+ (lambda options: options, "--foobar"),
188
+ (lambda reports: reports, ["report"]),
189
+ (lambda reports, options: (reports, options), (["report"], "--foobar")),
190
+ (lambda **kwargs: kwargs, {"reports": ["report"], "options": "--foobar"}),
191
+ ],
192
+ )
193
+ def test_call_reporter_factory_passes_declared_arguments(factory, expected):
194
+ assert _call_reporter_factory(factory, ["report"], "--foobar") == expected
@@ -497,6 +497,60 @@ class TestCloverXmlCoverageReporterTest:
497
497
  result = coverage.violations("file1.java")
498
498
  assert result == violations
499
499
 
500
+ def test_phpunit_clover_without_clover_attribute(self):
501
+ xml = self._coverage_xml(
502
+ ["/workspace/project/subdir/file.java"],
503
+ self.FEW_VIOLATIONS,
504
+ self.FEW_MEASURED,
505
+ )
506
+ del xml.attrib["clover"]
507
+ file_node = xml.find(".//file")
508
+ file_node.set("name", file_node.attrib.pop("path"))
509
+
510
+ coverage = XmlCoverageReporter([xml])
511
+
512
+ assert coverage.violations("subdir/file.java") == self.FEW_VIOLATIONS
513
+ assert coverage.measured_lines("subdir/file.java") == self.FEW_MEASURED
514
+
515
+ def test_method_lines_are_measured(self):
516
+ # PHPUnit's Clover writer emits type="method" for the declaration line of
517
+ # every measured function. Those lines carry a count like any other and
518
+ # must not be reported as unmeasured.
519
+ xml = self._coverage_xml(
520
+ ["file1.java"], self.FEW_VIOLATIONS, self.FEW_MEASURED, method_lines={42}
521
+ )
522
+
523
+ coverage = XmlCoverageReporter([xml])
524
+
525
+ assert 42 in coverage.measured_lines("file1.java")
526
+ assert coverage.violations("file1.java") == self.FEW_VIOLATIONS
527
+
528
+ def test_method_line_can_be_a_violation(self):
529
+ xml = self._coverage_xml(
530
+ ["file1.java"], self.FEW_VIOLATIONS, self.FEW_MEASURED, method_lines={42}
531
+ )
532
+ method_line = xml.find('.//line[@type="method"]')
533
+ method_line.set("count", "0")
534
+
535
+ coverage = XmlCoverageReporter([xml])
536
+
537
+ assert 42 in coverage.measured_lines("file1.java")
538
+ assert Violation(42, None) in coverage.violations("file1.java")
539
+
540
+ def test_report_format_is_detected_once(self):
541
+ # Classifying the report is a search through the whole document; doing it
542
+ # per source file was the review comment on #617.
543
+ xml = self._coverage_xml(
544
+ ["file1.java", "file2.java"], self.FEW_VIOLATIONS, self.FEW_MEASURED
545
+ )
546
+
547
+ coverage = XmlCoverageReporter([xml])
548
+
549
+ assert coverage._report_formats == ["clover"]
550
+ coverage.violations("file1.java")
551
+ coverage.violations("file2.java")
552
+ assert coverage._report_formats == ["clover"]
553
+
500
554
  def test_two_inputs_first_violate(self):
501
555
  # Construct the XML report
502
556
  file_paths = ["file1.java"]
@@ -620,7 +674,7 @@ class TestCloverXmlCoverageReporterTest:
620
674
  result = coverage.violations("file.java")
621
675
  assert result == set()
622
676
 
623
- def _coverage_xml(self, file_paths, violations, measured):
677
+ def _coverage_xml(self, file_paths, violations, measured, method_lines=None):
624
678
  """
625
679
  Build an XML tree with source files specified by `file_paths`.
626
680
  Each source fill will have the same set of covered and
@@ -630,6 +684,9 @@ class TestCloverXmlCoverageReporterTest:
630
684
  `line_dict` is a dictionary with keys that are line numbers
631
685
  and values that are True/False indicating whether the line
632
686
  is covered
687
+ `method_lines` is an optional set of line numbers to emit as
688
+ `type="method"` instead of `type="stmt"`, the way PHPUnit's Clover
689
+ writer marks the declaration line of a measured function
633
690
 
634
691
  This leaves out some attributes of the Cobertura format,
635
692
  but includes all the elements.
@@ -654,6 +711,12 @@ class TestCloverXmlCoverageReporterTest:
654
711
  line.set("count", str(hits))
655
712
  line.set("num", str(line_num))
656
713
  line.set("type", "stmt")
714
+
715
+ for line_num in method_lines or ():
716
+ line = etree.SubElement(src_node, "line")
717
+ line.set("count", "1")
718
+ line.set("num", str(line_num))
719
+ line.set("type", "method")
657
720
  return root
658
721
 
659
722
 
@@ -1,12 +0,0 @@
1
- import pluggy
2
-
3
- hookspec = pluggy.HookspecMarker("diff_cover")
4
-
5
-
6
- @hookspec
7
- def diff_cover_report_quality():
8
- """
9
- Return a 2-part tuple:
10
- - Quality plugin name
11
- - Object that implements the BaseViolationReporter protocol
12
- """
File without changes
File without changes