pyright-to-gitlab 1.2.0__tar.gz → 1.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyright-to-gitlab
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Convert Pyright JSON output to GitLab CI/CD format
5
5
  Project-URL: homepage, https://github.com/schollm/pyright-to-gitlab/
6
6
  Project-URL: repository, https://github.com/schollm/pyright-to-gitlab/
@@ -52,12 +52,12 @@ $ pip install pyright-to-gitlab
52
52
  $ pyright . --outputjson | python -m pyright_to_gitlab > code-quality.json
53
53
  ```
54
54
  ### Custom path prefix
55
- The `--prefix` option adds a custom prefix to the file paths in the output. This is
56
- useful if the paths in the pyright output are not relative to the root of the repository.
55
+ The `--prefix` option adds a custom prefix path to the file paths in the output. This is
56
+ useful for mono-repos, where the paths in the pyright output is not the repository root.
57
57
 
58
58
 
59
59
  ```shell
60
- $ pyright . --outputjson | pyright-to-gitlab --prefix my-app/ > code-quality.json
60
+ $ pyright . --outputjson | pyright-to-gitlab --prefix my-app > code-quality.json
61
61
  ```
62
62
 
63
63
  ## Testing
@@ -24,12 +24,12 @@ $ pip install pyright-to-gitlab
24
24
  $ pyright . --outputjson | python -m pyright_to_gitlab > code-quality.json
25
25
  ```
26
26
  ### Custom path prefix
27
- The `--prefix` option adds a custom prefix to the file paths in the output. This is
28
- useful if the paths in the pyright output are not relative to the root of the repository.
27
+ The `--prefix` option adds a custom prefix path to the file paths in the output. This is
28
+ useful for mono-repos, where the paths in the pyright output is not the repository root.
29
29
 
30
30
 
31
31
  ```shell
32
- $ pyright . --outputjson | pyright-to-gitlab --prefix my-app/ > code-quality.json
32
+ $ pyright . --outputjson | pyright-to-gitlab --prefix my-app > code-quality.json
33
33
  ```
34
34
 
35
35
  ## Testing
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "pyright-to-gitlab"
3
- version = "1.2.0"
3
+ version = "1.3.0"
4
4
  description = "Convert Pyright JSON output to GitLab CI/CD format"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.8"
7
7
  authors = [{ name = "Micha Schöll", email = "" }]
8
8
  license = "MIT"
9
9
  license-files = ["LICENSE"]
10
- keywords = [ "ci/cd", "pyright", 'gitlab' ]
10
+ keywords = [ "ci/cd", "pyright", "gitlab" ]
11
11
  classifiers = [
12
12
  "Development Status :: 5 - Production/Stable",
13
13
  "Intended Audience :: Developers",
@@ -59,7 +59,6 @@ ignore = [
59
59
  "D203", # `incorrect-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible.
60
60
  "COM812", # Missing trailing comma in a single-line list - already applied by ruff format.
61
61
  "D205", # 1 blank line required between summary line and description: ruff format disagrees.
62
-
63
62
  ]
64
63
 
65
64
  [tool.ruff.lint.per-file-ignores]
@@ -68,6 +67,7 @@ ignore = [
68
67
  "S101", # asserts allowed in tests...
69
68
  "ARG", # Unused function args -> fixtures nevertheless are functionally relevant...
70
69
  "FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize()
70
+ "PLR0913", # Too many arguments in test functions are sometimes necessary (e.g. via fixtures)
71
71
  ]
72
72
 
73
73
  [tool.pytest.ini_options]
@@ -78,4 +78,4 @@ addopts = """
78
78
  --cov-report=html:.out/coverage-html
79
79
  --cov-report term-missing
80
80
  --cov-branch
81
- """
81
+ """
@@ -11,25 +11,25 @@ from typing import Literal, TextIO, TypedDict
11
11
 
12
12
 
13
13
  ### Typing for PyRight Issue
14
- class PyrightRangeElement(TypedDict):
14
+ class PyrightRangeElement(TypedDict, total=False):
15
15
  """Pyright Range Element (part of Range)."""
16
16
 
17
17
  line: int
18
18
  character: int
19
19
 
20
20
 
21
- class PyrightRange(TypedDict):
21
+ class PyrightRange(TypedDict, total=False):
22
22
  """Pyright Range (Part of Issue)."""
23
23
 
24
24
  start: PyrightRangeElement
25
25
  end: PyrightRangeElement
26
26
 
27
27
 
28
- class PyrightIssue(TypedDict):
28
+ class PyrightIssue(TypedDict, total=False):
29
29
  """Single Pyright Issue.
30
30
 
31
- Note: 'rule' field is optional in practice but marked as required in type hints.
32
- Runtime code handles this with defensive .get() calls.
31
+ Note: total=False makes all fields optional. Runtime code handles this with
32
+ defensive .get() calls.
33
33
  """
34
34
 
35
35
  file: str
@@ -78,7 +78,7 @@ def _pyright_to_gitlab(input_: TextIO, prefix: str = "") -> str:
78
78
  Line numbers from Pyright are passed through unchanged (0-based per LSP spec).
79
79
  GitLab expects the same format, so no conversion is needed.
80
80
 
81
- :arg prefix: A string to prepend to each file path in the output.
81
+ :arg prefix: A path to prepend to each file path in the output.
82
82
  This is useful if the application is in a subdirectory of the repository.
83
83
  :return: JSON of issues in GitLab Code Quality report format.
84
84
  :raises ValueError: If input is not a JSON object.
@@ -87,6 +87,9 @@ def _pyright_to_gitlab(input_: TextIO, prefix: str = "") -> str:
87
87
  Pyright format at https://github.com/microsoft/pyright/blob/main/docs/command-line.md
88
88
  Gitlab format at https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format
89
89
  """
90
+ if prefix and not prefix.endswith("/"):
91
+ prefix += "/"
92
+
90
93
  try:
91
94
  data = json.load(input_)
92
95
  except json.JSONDecodeError as e:
@@ -116,22 +119,22 @@ def _pyright_issue_to_gitlab(issue: PyrightIssue, prefix: str) -> GitlabIssue:
116
119
  :param prefix: The path prefix.
117
120
  :returns: A gitlab single issue.
118
121
  """
119
- start, end = (
120
- issue.get("range", {}).get("start", {}),
121
- issue.get("range", {}).get("end", {}),
122
- )
122
+ range_ = issue.get("range", {})
123
+ start, end = (range_.get("start", {}), range_.get("end", {}))
123
124
  rule = "pyright: " + issue.get("rule", "unknown")
124
- # Unique fingerprint including file path to prevent collisions across files
125
- fp_str = "--".join([issue.get("file", "<anonymous>"), str(start), str(end), rule])
125
+ # Hash input must contain file, location and rule to generate a unique fingerprint.
126
+ # (This takes advantage of stable dict order).
127
+ fingerprint = f"{issue.get('file', '<anonymous>')}--{range_}--{rule}"
126
128
 
127
129
  return GitlabIssue(
128
130
  description=issue.get("message", ""),
131
+ # Map 'error' to 'major', all others, including empty, to 'minor'
129
132
  severity="major" if issue.get("severity") == "error" else "minor",
130
133
  # Any hash function really works, does not have to be cryptographic.
131
- fingerprint=hashlib.sha3_224(fp_str.encode()).hexdigest(),
134
+ fingerprint=_hash(fingerprint),
132
135
  check_name=rule,
133
136
  location=GitlabIssueLocation(
134
- path=f"{prefix}{issue['file']}" if "file" in issue else "<anonymous>",
137
+ path=f"{prefix}{issue.get('file', '<anonymous>')}",
135
138
  positions=GitlabIssuePositions(
136
139
  begin=GitlabIssuePositionLocation(
137
140
  line=start.get("line", 0), column=start.get("character", 0)
@@ -144,7 +147,16 @@ def _pyright_issue_to_gitlab(issue: PyrightIssue, prefix: str) -> GitlabIssue:
144
147
  )
145
148
 
146
149
 
147
- def main() -> None:
150
+ def _hash(data: str) -> str:
151
+ """Generate an (non-secure) hash of the given data string.
152
+
153
+ :param data: The input string to hash.
154
+ :returns: The hexadecimal representation of the MD5 hash.
155
+ """
156
+ return hashlib.new("md5", data.encode(), usedforsecurity=False).hexdigest()
157
+
158
+
159
+ def cli() -> None:
148
160
  """Parse arguments and call the conversion function."""
149
161
  parser = argparse.ArgumentParser(
150
162
  description=textwrap.dedent("""
@@ -178,12 +190,13 @@ def main() -> None:
178
190
  "--prefix",
179
191
  type=str,
180
192
  default="",
181
- help="Prefix to add to each file entry. This can be used if pyright is run"
193
+ help="Prefix path to add to each file entry. This can be used if pyright is run"
182
194
  " from a subdirectory of the repository. (default: empty string)",
183
195
  )
196
+ parser.add_argument("--version", action="version", version="%(prog)s 1.3.0")
184
197
  args = parser.parse_args()
185
198
  args.output.write(_pyright_to_gitlab(input_=args.input, prefix=args.prefix))
186
199
 
187
200
 
188
201
  if __name__ == "__main__": # pragma: no cover
189
- main()
202
+ cli()
@@ -9,9 +9,11 @@ from typing import TYPE_CHECKING
9
9
  if TYPE_CHECKING:
10
10
  from pathlib import Path
11
11
 
12
+ from importlib.metadata import version
13
+
12
14
  import pytest
13
15
 
14
- from pyright_to_gitlab import main
16
+ from pyright_to_gitlab import cli
15
17
 
16
18
  PYRIGHT = {
17
19
  "version": "1.1.385",
@@ -50,7 +52,7 @@ GITLAB = [
50
52
  {
51
53
  "check_name": "pyright: reportGeneralTypeIssues",
52
54
  "description": 'Message "foo"',
53
- "fingerprint": "c07588a4b4ee16dee26d14c086857de5a86bb7034461cdad63f6397f",
55
+ "fingerprint": "023610260f7cb68cf03b7f5a1232b566",
54
56
  "location": {
55
57
  "path": "test1.py",
56
58
  "positions": {
@@ -63,7 +65,7 @@ GITLAB = [
63
65
  {
64
66
  "check_name": "pyright: reportInvalidTypeForm",
65
67
  "description": "Message bar",
66
- "fingerprint": "d8bd498be79cb56d504196f52a1ba9bcd4e66635404629eda82e6be4",
68
+ "fingerprint": "2113f1d00f089646663709d55a111100",
67
69
  "location": {
68
70
  "path": "test2.py",
69
71
  "positions": {
@@ -92,7 +94,7 @@ def test(
92
94
  """Test that the pyright.json is converted to GitLab Code Quality report format."""
93
95
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
94
96
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
95
- main()
97
+ cli()
96
98
  captured = capsys.readouterr()
97
99
  assert json.loads(captured.out) == gitlab
98
100
 
@@ -124,10 +126,20 @@ def test_input_output_file(
124
126
  output_file.as_posix(),
125
127
  ],
126
128
  )
127
- main()
129
+ cli()
128
130
  assert json.loads(output_file.read_text("utf-8")) == gitlab
129
131
 
130
132
 
133
+ @pytest.mark.parametrize(
134
+ ("prefix_input", "prefix_expected"),
135
+ [
136
+ ("", ""),
137
+ (".", "./"),
138
+ ("src/", "src/"),
139
+ ("src", "src/"),
140
+ ("..", "../"),
141
+ ],
142
+ )
131
143
  @pytest.mark.parametrize(
132
144
  ("pyright", "gitlab"),
133
145
  [
@@ -138,21 +150,22 @@ def test_input_output_file(
138
150
  def test_prefix(
139
151
  monkeypatch: pytest.MonkeyPatch,
140
152
  capsys: pytest.CaptureFixture,
153
+ prefix_input: str,
154
+ prefix_expected: str,
141
155
  pyright: dict,
142
156
  gitlab: list[dict],
143
157
  ) -> None:
144
158
  """Test that the prefix is added to the file paths in the output."""
145
159
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
146
- monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py", "--prefix", "prefix/"])
147
- prefix = "prefix/"
148
- main()
160
+ monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py", "--prefix", prefix_input])
161
+ cli()
149
162
  captured = capsys.readouterr()
150
163
  gitlab_with_prefix = [
151
164
  {
152
165
  **issue,
153
166
  "location": {
154
167
  **issue["location"],
155
- "path": f"{prefix}{issue['location']['path']}",
168
+ "path": f"{prefix_expected}{issue['location']['path']}",
156
169
  },
157
170
  }
158
171
  for issue in gitlab
@@ -166,7 +179,7 @@ def test_malformed_json(monkeypatch: pytest.MonkeyPatch) -> None:
166
179
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
167
180
 
168
181
  with pytest.raises(ValueError, match="Invalid JSON input"):
169
- main()
182
+ cli()
170
183
 
171
184
 
172
185
  def test_non_dict_json(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -175,18 +188,31 @@ def test_non_dict_json(monkeypatch: pytest.MonkeyPatch) -> None:
175
188
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
176
189
 
177
190
  with pytest.raises(TypeError, match="Input must be a JSON object"):
178
- main()
191
+ cli()
179
192
 
180
193
 
181
- def test_warning_severity(
182
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
194
+ @pytest.mark.parametrize(
195
+ ("severity_input", "severity_expected"),
196
+ [
197
+ ("error", "major"),
198
+ ("warning", "minor"),
199
+ ("information", "minor"),
200
+ ("", "minor"),
201
+ (None, "minor"),
202
+ ],
203
+ )
204
+ def test_severity(
205
+ severity_input: str | None,
206
+ severity_expected: str,
207
+ monkeypatch: pytest.MonkeyPatch,
208
+ capsys: pytest.CaptureFixture,
183
209
  ) -> None:
184
210
  """Test that warning severity is mapped to 'minor'."""
185
211
  pyright = {
186
212
  "generalDiagnostics": [
187
213
  {
188
214
  "file": "test.py",
189
- "severity": "warning",
215
+ "severity": severity_input,
190
216
  "message": "Test warning",
191
217
  "range": {
192
218
  "start": {"line": 1, "character": 0},
@@ -198,38 +224,11 @@ def test_warning_severity(
198
224
  }
199
225
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
200
226
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
201
- main()
202
- captured = capsys.readouterr()
203
- result = json.loads(captured.out)
204
- assert len(result) == 1
205
- assert result[0]["severity"] == "minor"
206
-
207
-
208
- def test_information_severity(
209
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
210
- ) -> None:
211
- """Test that information severity is mapped to 'minor'."""
212
- pyright = {
213
- "generalDiagnostics": [
214
- {
215
- "file": "test.py",
216
- "severity": "information",
217
- "message": "Test info",
218
- "range": {
219
- "start": {"line": 1, "character": 0},
220
- "end": {"line": 1, "character": 5},
221
- },
222
- "rule": "testRule",
223
- }
224
- ]
225
- }
226
- monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
227
- monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
228
- main()
227
+ cli()
229
228
  captured = capsys.readouterr()
230
229
  result = json.loads(captured.out)
231
230
  assert len(result) == 1
232
- assert result[0]["severity"] == "minor"
231
+ assert result[0]["severity"] == severity_expected
233
232
 
234
233
 
235
234
  def test_missing_rule(
@@ -251,7 +250,7 @@ def test_missing_rule(
251
250
  }
252
251
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
253
252
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
254
- main()
253
+ cli()
255
254
  captured = capsys.readouterr()
256
255
  result = json.loads(captured.out)
257
256
  assert len(result) == 1
@@ -265,7 +264,7 @@ def test_empty_diagnostics(
265
264
  pyright = {"generalDiagnostics": []}
266
265
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
267
266
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
268
- main()
267
+ cli()
269
268
  captured = capsys.readouterr()
270
269
  result = json.loads(captured.out)
271
270
  assert result == []
@@ -278,7 +277,7 @@ def test_missing_general_diagnostics(
278
277
  pyright = {"version": "1.0", "summary": {}}
279
278
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
280
279
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
281
- main()
280
+ cli()
282
281
  captured = capsys.readouterr()
283
282
  result = json.loads(captured.out)
284
283
  assert result == []
@@ -301,7 +300,7 @@ def test_missing_range_field(
301
300
  }
302
301
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
303
302
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
304
- main()
303
+ cli()
305
304
  captured = capsys.readouterr()
306
305
  result = json.loads(captured.out)
307
306
  assert len(result) == 1
@@ -331,7 +330,7 @@ def test_missing_start_in_range(
331
330
  }
332
331
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
333
332
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
334
- main()
333
+ cli()
335
334
  captured = capsys.readouterr()
336
335
  result = json.loads(captured.out)
337
336
  assert len(result) == 1
@@ -361,7 +360,7 @@ def test_missing_end_in_range(
361
360
  }
362
361
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
363
362
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
364
- main()
363
+ cli()
365
364
  captured = capsys.readouterr()
366
365
  result = json.loads(captured.out)
367
366
  assert len(result) == 1
@@ -383,10 +382,7 @@ def test_missing_line_in_start(
383
382
  "message": "Test error",
384
383
  "rule": "testRule",
385
384
  "range": {
386
- "start": {
387
- # Missing 'line' field
388
- "character": 10
389
- },
385
+ "start": {"character": 10}, # Missing 'line' field
390
386
  "end": {"line": 10, "character": 20},
391
387
  },
392
388
  }
@@ -394,7 +390,7 @@ def test_missing_line_in_start(
394
390
  }
395
391
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
396
392
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
397
- main()
393
+ cli()
398
394
  captured = capsys.readouterr()
399
395
  result = json.loads(captured.out)
400
396
  assert len(result) == 1
@@ -427,7 +423,7 @@ def test_missing_character_in_end(
427
423
  }
428
424
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
429
425
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
430
- main()
426
+ cli()
431
427
  captured = capsys.readouterr()
432
428
  result = json.loads(captured.out)
433
429
  assert len(result) == 1
@@ -454,7 +450,7 @@ def test_missing_file_field(
454
450
  }
455
451
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
456
452
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
457
- main()
453
+ cli()
458
454
  captured = capsys.readouterr()
459
455
  result = json.loads(captured.out)
460
456
  assert len(result) == 1
@@ -481,7 +477,7 @@ def test_missing_message_field(
481
477
  }
482
478
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
483
479
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
484
- main()
480
+ cli()
485
481
  captured = capsys.readouterr()
486
482
  result = json.loads(captured.out)
487
483
  assert len(result) == 1
@@ -499,14 +495,14 @@ def test_completely_empty_issue(
499
495
  }
500
496
  monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(pyright)))
501
497
  monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py"])
502
- main()
498
+ cli()
503
499
  captured = capsys.readouterr()
504
500
  result = json.loads(captured.out)
505
501
  assert result == [
506
502
  {
507
503
  "check_name": "pyright: unknown",
508
504
  "description": "",
509
- "fingerprint": "751a157014b31820ec789f6f9cce28599122b8b56b628230f339ea2d",
505
+ "fingerprint": "dffaea5ca76e2b8d5ce64c938ce05945",
510
506
  "severity": "minor",
511
507
  "location": {
512
508
  "path": "<anonymous>",
@@ -517,3 +513,14 @@ def test_completely_empty_issue(
517
513
  },
518
514
  }
519
515
  ]
516
+
517
+
518
+ def test_version_flag(
519
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture
520
+ ) -> None:
521
+ """Test that the --version flag outputs the correct version."""
522
+ monkeypatch.setattr("sys.argv", ["pyright_to_gitlab.py", "--version"])
523
+ with pytest.raises(SystemExit):
524
+ cli()
525
+ result = capsys.readouterr().out
526
+ assert result == f"pyright_to_gitlab.py {version('pyright-to-gitlab')}\n"
@@ -435,7 +435,7 @@ wheels = [
435
435
 
436
436
  [[package]]
437
437
  name = "pyright-to-gitlab"
438
- version = "1.2.0"
438
+ version = "1.3.0"
439
439
  source = { editable = "." }
440
440
 
441
441
  [package.dev-dependencies]