python-constricter 0.3.5__py3-none-any.whl → 0.4.1__py3-none-any.whl

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 (113) hide show
  1. constricter/__init__.py +1 -1
  2. constricter/_version.py +4 -0
  3. constricter/cli/collecting.py +8 -1
  4. constricter/cli/command.py +28 -226
  5. constricter/cli/config.py +2 -1
  6. constricter/cli/lsp/__init__.py +2 -0
  7. constricter/cli/{hints.py → lsp/connection.py} +17 -363
  8. constricter/cli/{edits.py → lsp/edits.py} +1 -1
  9. constricter/cli/{guard.py → lsp/guard.py} +5 -1
  10. constricter/cli/lsp/hints.py +391 -0
  11. constricter/cli/lsp/remembered.py +81 -0
  12. constricter/cli/lsp/stored.py +200 -0
  13. constricter/cli/options.py +60 -7
  14. constricter/cli/paths.py +76 -7
  15. constricter/cli/reading.py +36 -0
  16. constricter/cli/report.py +5 -2
  17. constricter/cli/rounds.py +328 -0
  18. constricter/cli/schedule.py +84 -36
  19. constricter/cli/workers.py +78 -8
  20. constricter/empty.py +66 -0
  21. constricter/fix/core/fixes.py +26 -1
  22. constricter/fix/core/imports.py +110 -8
  23. constricter/fix/core/inherited.py +2 -1
  24. constricter/fix/core/known.py +101 -34
  25. constricter/fix/core/signatures.py +14 -7
  26. constricter/fix/index/beyond.py +3 -2
  27. constricter/fix/index/callers.py +3 -2
  28. constricter/fix/index/crossed.py +327 -0
  29. constricter/fix/index/decorated.py +29 -10
  30. constricter/fix/index/loose.py +11 -6
  31. constricter/fix/index/modules.py +90 -192
  32. constricter/fix/index/offers.py +123 -9
  33. constricter/fix/index/origins.py +238 -0
  34. constricter/fix/index/own_methods.py +31 -7
  35. constricter/fix/index/own_overloads.py +2 -2
  36. constricter/fix/index/own_types.py +183 -14
  37. constricter/fix/index/params.py +81 -0
  38. constricter/fix/index/plain.py +109 -13
  39. constricter/fix/index/project.py +86 -273
  40. constricter/fix/index/stubs/__init__.py +2 -0
  41. constricter/fix/index/{atoms.py → stubs/atoms.py} +3 -3
  42. constricter/fix/index/{chained.py → stubs/chained.py} +2 -2
  43. constricter/fix/index/{classnames.py → stubs/classnames.py} +2 -2
  44. constricter/fix/index/{declared.py → stubs/declared.py} +8 -6
  45. constricter/fix/index/{installed.py → stubs/installed.py} +84 -7
  46. constricter/fix/index/stubs/names.py +200 -0
  47. constricter/fix/index/{read.py → stubs/read.py} +2 -2
  48. constricter/fix/index/{stubbed.py → stubs/signatures.py} +28 -372
  49. constricter/fix/index/stubs/stubbed.py +208 -0
  50. constricter/fix/libraries/library.py +60 -14
  51. constricter/fix/libraries/overloads.py +315 -323
  52. constricter/fix/libraries/receivers.py +274 -0
  53. constricter/fix/libraries/stdlib.py +14 -2
  54. constricter/fix/tables/attributes.json +8 -8
  55. constricter/fix/tables/functions.json +15 -0
  56. constricter/fix/tables/method_overloads.json +91 -91
  57. constricter/fix/tables/method_signatures.json +60 -2
  58. constricter/fix/tables/overloads.json +5 -2
  59. constricter/fix/tables/platform.json +37 -0
  60. constricter/fix/tables/source.json +5 -3
  61. constricter/fix/tables/variables.json +3 -0
  62. constricter/fix/values/aliased.py +9 -4
  63. constricter/fix/values/callables.py +4 -2
  64. constricter/fix/values/called.py +89 -1
  65. constricter/fix/values/classvars.py +100 -12
  66. constricter/fix/values/decided.py +39 -6
  67. constricter/fix/values/displays.py +19 -5
  68. constricter/fix/values/doubts.py +21 -6
  69. constricter/fix/values/entered.py +2 -1
  70. constricter/fix/values/fills.py +21 -9
  71. constricter/fix/values/guesses.py +11 -5
  72. constricter/fix/values/hinted.py +175 -9
  73. constricter/fix/values/inference.py +24 -320
  74. constricter/fix/values/loops.py +335 -0
  75. constricter/fix/values/members.py +21 -7
  76. constricter/fix/values/operated.py +10 -8
  77. constricter/fix/values/returned.py +70 -558
  78. constricter/fix/values/shapes.py +192 -32
  79. constricter/fix/values/stored.py +564 -0
  80. constricter/fix/values/targets.py +49 -13
  81. constricter/fix/values/unchecked.py +119 -3
  82. constricter/notebook.py +6 -8
  83. constricter/offences.py +121 -3
  84. constricter/overrides.py +24 -0
  85. constricter/plugins/pylint.py +20 -17
  86. constricter/recording.py +109 -20
  87. constricter/rules/annotations.py +10 -688
  88. constricter/rules/binding.py +14 -8
  89. constricter/rules/calls.py +3 -2
  90. constricter/rules/checker.py +71 -527
  91. constricter/rules/declarations.py +730 -0
  92. constricter/rules/decorators.py +158 -28
  93. constricter/rules/keys.py +1 -1
  94. constricter/rules/late.py +331 -25
  95. constricter/rules/parsed.py +9 -3
  96. constricter/rules/quoted.py +21 -0
  97. constricter/rules/rebinding.py +5 -3
  98. constricter/rules/recorded.py +13 -12
  99. constricter/rules/scope.py +48 -240
  100. constricter/rules/scoped.py +232 -0
  101. constricter/rules/scoping.py +346 -0
  102. constricter/rules/settings.py +189 -0
  103. constricter/rules/tables.py +1 -1
  104. constricter/rules/widened.py +1 -1
  105. constricter/trace.py +5 -9
  106. {python_constricter-0.3.5.dist-info → python_constricter-0.4.1.dist-info}/METADATA +17 -9
  107. python_constricter-0.4.1.dist-info/RECORD +162 -0
  108. {python_constricter-0.3.5.dist-info → python_constricter-0.4.1.dist-info}/licenses/LICENSE.md +1 -1
  109. constricter/cli/remembered.py +0 -52
  110. python_constricter-0.3.5.dist-info/RECORD +0 -140
  111. /constricter/cli/{protocol.py → lsp/protocol.py} +0 -0
  112. {python_constricter-0.3.5.dist-info → python_constricter-0.4.1.dist-info}/WHEEL +0 -0
  113. {python_constricter-0.3.5.dist-info → python_constricter-0.4.1.dist-info}/entry_points.txt +0 -0
constricter/__init__.py CHANGED
@@ -1,6 +1,7 @@
1
1
  # SPDX-License-Identifier: MIT
2
2
  """Every local variable typed where it's first bound."""
3
3
 
4
+ from constricter._version import __version__ # written by the build, from the git tag
4
5
  from constricter.offences import (
5
6
  COMMENT_TYPED_TARGET,
6
7
  DEFAULT_CHECKS,
@@ -25,7 +26,6 @@ from constricter.offences import (
25
26
  )
26
27
  from constricter.rules.checker import Coverage, annotation_coverage, check_source, check_tree
27
28
 
28
- __version__ = "0.3.5"
29
29
  __all__ = [
30
30
  "COMMENT_TYPED_TARGET",
31
31
  "DEFAULT_CHECKS",
@@ -0,0 +1,4 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """The version this was built as, from its git tag (written by the build, not tracked)."""
3
+
4
+ __version__ = "0.4.1"
@@ -6,15 +6,21 @@ cyclic. Left to itself, the collector looked through the kept trees again and ag
6
6
  allocated: a third of the standard library's check (34s, 21s without). So while the command runs,
7
7
  it's off: what indexing built is frozen (never looked through again), and after each file, only
8
8
  what's been allocated since (the young generations) is collected, which bounds what a cycle can hold.
9
+ Not on a free-threaded build, whose collector has no generations: each collection there looks through
10
+ everything, and one after every file made a check several times as long. Its cycles wait for the run's
11
+ end (a check of pandas held 2.9 GB so, to 2.7 GB).
9
12
  """
10
13
 
11
14
  import contextlib
12
15
  import gc
16
+ import sysconfig
13
17
  from collections.abc import Generator
14
18
  from typing import Final
15
19
 
16
20
  _YOUNG: Final = 1 # the generations collected after each file: 0 and 1, never the frozen or the old
17
21
  _FREEZES: Final = hasattr(gc, "freeze") # PyPy's collector has no freezing
22
+ # Whether the collector has generations: a free-threaded build's has none, its GIL on or off.
23
+ _YOUNGER: Final = not sysconfig.get_config_var("Py_GIL_DISABLED")
18
24
 
19
25
 
20
26
  @contextlib.contextmanager
@@ -44,4 +50,5 @@ def indexed() -> None:
44
50
 
45
51
  def sweep() -> None:
46
52
  """Collect what the file just checked left behind (see the module docstring)."""
47
- _ = gc.collect(_YOUNG)
53
+ if _YOUNGER:
54
+ _ = gc.collect(_YOUNG)
@@ -5,27 +5,26 @@ import codecs
5
5
  import contextlib
6
6
  import difflib
7
7
  import importlib
8
- import io
9
8
  import json
10
9
  import sys
11
- import tokenize
12
- from collections.abc import Callable, Mapping, Sequence
10
+ from collections.abc import Callable, Sequence
13
11
  from dataclasses import replace
14
12
  from functools import partial
15
13
  from pathlib import Path
16
14
  from typing import TYPE_CHECKING, Final, NamedTuple, TextIO, TypeAlias, cast
17
15
 
18
16
  from constricter import notebook
19
- from constricter.cli import baseline, collecting, schedule, traced
17
+ from constricter.cli import baseline, collecting, schedule
18
+ from constricter.cli.lsp.protocol import HintError
20
19
  from constricter.cli.options import Mode, Options, Output
21
20
  from constricter.cli.paths import STDIN, python_files
22
- from constricter.cli.protocol import HintError
21
+ from constricter.cli.reading import encoding_of, read_text
23
22
  from constricter.cli.report import Format, Result, fix_reasons, render, statistics
24
- from constricter.cli.runs import Added, BaselineRun, CheckRun, CoverageRun, FileRun, merged
25
- from constricter.cli.workers import Workers, kept, started
23
+ from constricter.cli.rounds import checked_all, settled
24
+ from constricter.cli.runs import Added, BaselineRun, CheckRun, CoverageRun, FileRun
25
+ from constricter.cli.workers import kept
26
26
  from constricter.fix.core import fixes
27
- from constricter.fix.core.known import Callee, Hints, Outside, Returns
28
- from constricter.fix.index import callers, decorated, installed, loose, plain, project
27
+ from constricter.fix.core.known import Outside, Returns
29
28
  from constricter.noqa import lines, unsuppressed
30
29
  from constricter.offences import (
31
30
  DEFAULT_CHECKS,
@@ -37,39 +36,12 @@ from constricter.rules import parsed
37
36
  from constricter.rules.checker import Checked, Coverage, annotation_coverage, checked_source
38
37
 
39
38
  if TYPE_CHECKING: # slow to import, and only needed for `--infer-with`
40
- from constricter.cli import hints
39
+ from constricter.cli.lsp import hints
41
40
 
42
41
  EXIT_CLEAN: Final = 0
43
42
  EXIT_FOUND: Final = 1
44
43
  EXIT_ERROR: Final = 2
45
44
  _ALL: Final = 100 # percent
46
- _PY: Final = ".py" # a module the index names (not a notebook)
47
-
48
-
49
- def _encoding(path: Path, data: bytes) -> str:
50
- """Find the encoding of `path`, whose bytes are `data`: a notebook's is UTF-8 (it's JSON).
51
-
52
- Returns:
53
- A module's, as Python finds it: a PEP 263 declaration (`# -*- coding: latin-1 -*-`), a BOM
54
- (`utf-8-sig`), or UTF-8. Raises `SyntaxError` for an unknown or contradictory one.
55
-
56
- """
57
- return (
58
- "utf-8" if path.suffix == notebook.SUFFIX else tokenize.detect_encoding(io.BytesIO(data).readline)[0]
59
- )
60
-
61
-
62
- def _read(path: Path) -> str:
63
- """Read `path`, in its encoding; `-` is standard input.
64
-
65
- Returns:
66
- Its text.
67
-
68
- """
69
- if path == STDIN:
70
- return sys.stdin.read()
71
- data: bytes = path.read_bytes()
72
- return data.decode(_encoding(path, data))
73
45
 
74
46
 
75
47
  def _source(raw: str, name: Path) -> tuple[str, list[notebook.Line]]:
@@ -127,7 +99,6 @@ def _placed(offence: Offence, where: list[notebook.Line]) -> Offence:
127
99
 
128
100
 
129
101
  # One changed part of a fixed file (the file, or a notebook's cell): its label, old and new lines.
130
- _Hinted: TypeAlias = dict[Path, tuple[Hints, ...]] # each file's hints, a checker's each
131
102
  _Change: TypeAlias = tuple[str, list[str], list[str]]
132
103
  # One JSON value a coverage report holds, and one file's row of them.
133
104
  _Scalar: TypeAlias = str | int | float
@@ -179,14 +150,11 @@ def fix_file(path: Path, offences: Sequence[Offence]) -> int:
179
150
  if not (count := sum(1 for o in offences if o.edit is not None)):
180
151
  return 0
181
152
  data: bytes = path.read_bytes()
182
- encoding: str = _encoding(path, data)
153
+ encoding: str = encoding_of(path, data)
183
154
  _ = path.write_bytes(_fixed(data.decode(encoding), path, offences).text.encode(encoding))
184
155
  return count
185
156
 
186
157
 
187
- HINT_ROUNDS: Final = 4 # with `--fix --infer-with`: how many times each file is fixed, at most
188
-
189
-
190
158
  def _shown_lines(raw: str, name: Path) -> dict[tuple[int | None, int], str]:
191
159
  """Map each line of `raw` (a notebook's, in its cells) to its text, as offences are placed.
192
160
 
@@ -210,7 +178,7 @@ def _checked(path: Path, name: Path, checks: Checks, outside: Outside) -> tuple[
210
178
  Its text, and what checking it found.
211
179
 
212
180
  """
213
- raw: str = _read(path)
181
+ raw: str = read_text(path)
214
182
  return raw, checked_text(raw, name, checks, outside=outside)
215
183
 
216
184
 
@@ -288,9 +256,8 @@ def _handled(path: Path, text: tuple[str, Path], offences: list[Offence], option
288
256
  baselined: int
289
257
  offences, baselined = options.filter.unbaselined(name, offences)
290
258
  results: list[Result] = _results(raw, name, offences, options)
291
- unsafe: bool = options.unsafe_fixes
292
259
  fixing: list[Offence] = [
293
- r.offence for r in results if r.offence.edit is not None and (unsafe or not r.offence.unsafe)
260
+ r.offence for r in results if r.offence.edit is not None and options.guesses.applies(r.offence)
294
261
  ]
295
262
  if options.mode is Mode.DIFF:
296
263
  return CheckRun(text=_diff(raw, name, fixing))
@@ -357,7 +324,7 @@ def _cover_path(path: Path, _outside: Outside, options: Options) -> CoverageRun:
357
324
  """
358
325
  name: Path = options.input.name(path)
359
326
  try:
360
- return CoverageRun(annotation_coverage(_source(_read(path), name)[0], options.checks))
327
+ return CoverageRun(annotation_coverage(_source(read_text(path), name)[0], options.checks))
361
328
  except (OSError, ValueError, SyntaxError) as error:
362
329
  return CoverageRun(error=f"{name}: error: {error}")
363
330
 
@@ -369,6 +336,7 @@ def _check_all(options: Options) -> tuple[list[Path], list[FileRun]]:
369
336
  The names, and what each file found.
370
337
 
371
338
  """
339
+ schedule.forget()
372
340
  with kept():
373
341
  return _check_rounds(options)
374
342
 
@@ -378,15 +346,16 @@ def _check_rounds(options: Options) -> tuple[list[Path], list[FileRun]]:
378
346
 
379
347
  With `--infer-with`, the type checker's server runs throughout: it's asked for every file's
380
348
  hints first, and with `--fix`, each file a round changed is asked again and checked again, as its
381
- new annotations change what the checker infers, until a round changes nothing (at most
382
- `HINT_ROUNDS`). A baseline records offences, and coverage counts annotations: hints change
383
- neither, only what `--fix` offers.
349
+ new annotations change what the checker infers (see `settled`). A baseline records offences,
350
+ and coverage counts annotations: hints change neither, only what `--fix` offers.
384
351
 
385
352
  Returns:
386
353
  The names, and what each file found.
387
354
 
388
355
  """
389
- paths: list[Path] = list(python_files(options.input.paths, options.input.exclude))
356
+ paths: list[Path] = list(
357
+ python_files(options.input.paths, options.input.exclude, tracked=options.input.gitignore),
358
+ )
390
359
  check: Callable[[Path, Outside], FileRun]
391
360
  if options.mode is Mode.COVERAGE:
392
361
  check = partial(_cover_path, options=options)
@@ -396,47 +365,19 @@ def _check_rounds(options: Options) -> tuple[list[Path], list[FileRun]]:
396
365
  check = partial(_check_path, options=options)
397
366
  names: list[Path] = [options.input.name(path) for path in paths]
398
367
  if options.mode in {Mode.COVERAGE, Mode.WRITE_BASELINE}:
399
- return names, _checked_all(paths, check, options)[0]
368
+ return names, checked_all(paths, check, options)[0]
400
369
  if not options.infer_with:
401
- return names, _called_again(paths, _checked_all(paths, check, options), check, options)[0]
370
+ return names, settled(paths, check, options, None)
402
371
  session: hints.Session
403
372
  # Imported only for `--infer-with`: it's slow to import, for every run's start.
404
- with cast("type[hints.Session]", importlib.import_module("constricter.cli.hints").Session)(
373
+ with cast("type[hints.Session]", importlib.import_module("constricter.cli.lsp.hints").Session)(
405
374
  options.infer_with,
406
375
  Path.cwd(),
407
376
  options.jobs,
408
377
  options.infer_memory,
378
+ kept=not options.infer_fresh,
409
379
  ) as session:
410
- runs: list[FileRun]
411
- modules: project.Index
412
- runs, modules = _called_again(
413
- paths,
414
- _checked_all(paths, check, options, session),
415
- check,
416
- options,
417
- session,
418
- )
419
- # The files the last round changed: only their hints can have changed.
420
- again: list[int] = [
421
- index
422
- for index, run in enumerate(runs)
423
- if options.mode is Mode.FIX and isinstance(run, CheckRun) and run.fixed and paths[index] != STDIN
424
- ]
425
- for _ in range(HINT_ROUNDS - 1):
426
- if not again:
427
- break
428
- redone: list[FileRun] = _checked_all(
429
- [paths[i] for i in again],
430
- check,
431
- options,
432
- session,
433
- modules,
434
- )[0]
435
- index: int
436
- run: FileRun
437
- for index, run in zip(again, redone, strict=True):
438
- runs[index] = merged(runs[index], run)
439
- again = [index for index, run in zip(again, redone, strict=True) if cast("CheckRun", run).fixed]
380
+ runs: list[FileRun] = settled(paths, check, options, session)
440
381
  checker: str
441
382
  path: Path
442
383
  for checker, path in session.abandoned:
@@ -445,148 +386,6 @@ def _check_rounds(options: Options) -> tuple[list[Path], list[FileRun]]:
445
386
  return names, runs
446
387
 
447
388
 
448
- def _called_again(
449
- paths: Sequence[Path],
450
- first: tuple[list[FileRun], project.Index],
451
- check: Callable[[Path, Outside], FileRun],
452
- options: Options,
453
- session: "hints.Session | None" = None,
454
- ) -> tuple[list[FileRun], project.Index]:
455
- """Check again the files whose functions' unannotated parameters every call passes one type.
456
-
457
- Knowing those types (see `callers`), as guesses; a file among several of one module name isn't
458
- (which its callers call can't be told). Then the other files calling those functions, knowing
459
- what they now return. With `--fix` a file's rounds are joined; otherwise the last, knowing more,
460
- stands.
461
-
462
- Returns:
463
- What each file found, and the index, with the parameters' types.
464
-
465
- """
466
- runs: list[FileRun]
467
- modules: project.Index
468
- runs, modules = first
469
- found: dict[str, callers.Parameters] = callers.parameters(
470
- (run.calls for run in runs if isinstance(run, CheckRun)),
471
- modules,
472
- )
473
- names: list[str | None] = [project.module_name(path) if path.suffix == _PY else None for path in paths]
474
- again: list[int]
475
- if not (again := [at for at, name in enumerate(names) if name in found and names.count(name) == 1]):
476
- return runs, modules
477
- calling: list[int] = _calling(runs, found, again)
478
- before: project.Index = callers.with_parameters(modules, found)
479
- modules = _checked_more(paths, runs, again, (check, options, session), before)
480
- # A caller is checked again only if a function it imports now returns something else.
481
- calling = [
482
- at
483
- for at in calling
484
- if loose.returned(modules, paths[at], {}) != loose.returned(before, paths[at], {})
485
- ]
486
- return runs, _checked_more(paths, runs, calling, (check, options, session), modules)
487
-
488
-
489
- def _checked_more(
490
- paths: Sequence[Path],
491
- runs: list[FileRun],
492
- which: Sequence[int],
493
- how: "tuple[Callable[[Path, Outside], FileRun], Options, hints.Session | None]",
494
- modules: project.Index,
495
- ) -> project.Index:
496
- """Check the files of `paths` at `which` again, knowing `modules`, into `runs`.
497
-
498
- `how`: the check, the options and the type checkers' session. With `--fix` a file's rounds are
499
- joined; otherwise the later one stands.
500
-
501
- Returns:
502
- The index, with what their functions now return.
503
-
504
- """
505
- redone: list[FileRun]
506
- redone, modules = _checked_all([paths[at] for at in which], how[0], how[1], how[2], modules)
507
- at: int
508
- run: FileRun
509
- for at, run in zip(which, redone, strict=True):
510
- runs[at] = merged(runs[at], run) if how[1].mode is Mode.FIX else run
511
- return modules
512
-
513
-
514
- def _calling(
515
- runs: Sequence[FileRun],
516
- found: Mapping[str, callers.Parameters],
517
- again: Sequence[int],
518
- ) -> list[int]:
519
- """Find the files (not among `again`) calling a function whose parameters `found` types.
520
-
521
- Returns:
522
- Their indexes.
523
-
524
- """
525
- typed: set[Callee] = {(module, function) for module, functions in found.items() for function in functions}
526
- return [
527
- at
528
- for at, run in enumerate(runs)
529
- if at not in again and isinstance(run, CheckRun) and typed & run.calls.calls.keys()
530
- ]
531
-
532
-
533
- def _checked_all(
534
- paths: Sequence[Path],
535
- check: Callable[[Path, Outside], FileRun],
536
- options: Options,
537
- session: "hints.Session | None" = None,
538
- modules: project.Index | None = None,
539
- ) -> tuple[list[FileRun], project.Index]:
540
- """Check `paths` (`--jobs` at a time), with the `session`'s hints, and `modules` (else indexed).
541
-
542
- Returns:
543
- What each file found, and the index of every file's module.
544
-
545
- """
546
- # Before a worker starts: one forked while the servers' threads run could wait on a lock one holds.
547
- hinted: _Hinted = {} if session is None else session.hints(_texts(paths))
548
- if options.input.trace is not None:
549
- hinted = traced.merged(hinted, traced.load(options.input.trace), paths)
550
- coverage: bool = options.mode is Mode.COVERAGE # needs nothing from the other files
551
- pool: Workers | None = started(paths, options.jobs) if options.jobs != 1 and len(paths) > 1 else None
552
- if modules is None and pool:
553
- modules = project.Index({}, []) if coverage else pool.index()
554
- elif modules is None:
555
- modules = project.Index({}, []) if coverage else project.index(paths)
556
- collecting.indexed()
557
- if not coverage:
558
- modules = plain.settled(
559
- decorated.passed(installed.with_installed(modules, installed.search_path())),
560
- options.checks.plain_bases,
561
- )
562
- return schedule.checked(
563
- paths,
564
- check,
565
- pool,
566
- (modules, hinted),
567
- merged if options.mode is Mode.FIX else None,
568
- )
569
-
570
-
571
- def _texts(paths: Sequence[Path]) -> dict[Path, str]:
572
- """Read each file to ask the type checker about (not a notebook, or standard input).
573
-
574
- A file that can't be read is left out: checking it reports that.
575
-
576
- Returns:
577
- Each file's text.
578
-
579
- """
580
- texts: dict[Path, str] = {}
581
- path: Path
582
- for path in paths:
583
- if path != STDIN and path.suffix != notebook.SUFFIX:
584
- # UnicodeDecodeError is a ValueError; a bad encoding declaration, a SyntaxError.
585
- with contextlib.suppress(OSError, ValueError, SyntaxError):
586
- texts[path] = _read(path)
587
- return texts
588
-
589
-
590
389
  def _report(options: Options, runs: Sequence[FileRun], files: int) -> int:
591
390
  """Print the results (`runs` is check/fix/diff mode's: every other mode has its own printing).
592
391
 
@@ -618,7 +417,10 @@ def _report(options: Options, runs: Sequence[FileRun], files: int) -> int:
618
417
  parts.append(f"fixed {sum(run.fixed for run in checked)}")
619
418
  guesses: int
620
419
  if guesses := sum(r.offence.unsafe for r in results):
621
- parts.append(f"{guesses} more with --unsafe-fixes")
420
+ likely: int = sum(r.offence.likely for r in results)
421
+ parts.append(
422
+ f"{guesses} more with --unsafe-fixes" + f" ({likely} with --likely)" * bool(likely),
423
+ )
622
424
  if options.filter.baseline_file is not None:
623
425
  parts.append(f"{sum(run.baselined for run in checked)} baselined")
624
426
  _ = sys.stdout.write("; ".join(parts) + ".\n")
constricter/cli/config.py CHANGED
@@ -8,7 +8,7 @@ from functools import partial
8
8
  from pathlib import Path
9
9
  from typing import TYPE_CHECKING, Final, TypeAlias, cast
10
10
 
11
- from constricter.cli.protocol import SERVERS
11
+ from constricter.cli.lsp.protocol import SERVERS
12
12
  from constricter.jsonc import is_int
13
13
  from constricter.offences import FIX_KINDS, LEVELS, MESSAGES, WIDEN_KINDS
14
14
 
@@ -276,6 +276,7 @@ _READERS: dict[str, Callable[[_Toml], Default | None]] = {
276
276
  "fix-plain-bases": _strings,
277
277
  "type-comments": _flag,
278
278
  "all-scopes": _flag,
279
+ "gitignore": _flag,
279
280
  "per-path-levels": _levels,
280
281
  "per-file-ignores": partial(_lists, read=_codes),
281
282
  "narrower": partial(_lists, read=_strings), # a type hierarchy: each type, and those it's narrower than
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """constricter.cli.lsp."""