python-checks 0.1.1__py3-none-any.whl → 0.2.0__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.
@@ -11,7 +11,7 @@ from py_checks.core._edit import Edit, apply, column
11
11
  from py_checks.core._errors import ParseError, UnknownCheckError
12
12
  from py_checks.core._fixer import fix
13
13
  from py_checks.core._format import reformat
14
- from py_checks.core._markers import MARKER, Marker, complaints, read, surviving
14
+ from py_checks.core._markers import MARKER, Marker, complaints, markers, surviving
15
15
  from py_checks.core._protocols import Check, FileCheck, ProjectCheck, Scope
16
16
  from py_checks.core._registry import Checks, available, get
17
17
  from py_checks.core._report import report
@@ -21,19 +21,19 @@ from py_checks.core._source import ParsedFile
21
21
  from py_checks.core._violation import Violation
22
22
 
23
23
  __all__ = [
24
- "EXIT_OK",
25
- "EXIT_VIOLATION",
26
- "GROUP",
27
- "MARKER",
28
- "SYNTAX",
29
24
  "Check",
30
25
  "Checks",
26
+ "EXIT_OK",
27
+ "EXIT_VIOLATION",
31
28
  "Edit",
32
29
  "FileCheck",
33
- "ProjectCheck",
30
+ "GROUP",
31
+ "MARKER",
34
32
  "Marker",
35
33
  "ParseError",
36
34
  "ParsedFile",
35
+ "ProjectCheck",
36
+ "SYNTAX",
37
37
  "Scope",
38
38
  "SettingsMismatchError",
39
39
  "UnknownCheckError",
@@ -42,15 +42,15 @@ __all__ = [
42
42
  "available",
43
43
  "column",
44
44
  "complaints",
45
- "fix",
46
45
  "examine",
46
+ "fix",
47
47
  "get",
48
48
  "inspect",
49
+ "markers",
49
50
  "python_files",
50
- "read",
51
51
  "reformat",
52
- "surviving",
53
- "settings_as",
54
52
  "report",
53
+ "settings_as",
55
54
  "survey",
55
+ "surviving",
56
56
  ]
@@ -49,33 +49,73 @@ class Marker:
49
49
  column: int
50
50
 
51
51
 
52
- def read(
52
+ def markers(
53
53
  *,
54
54
  line: str,
55
55
  aliases: Mapping[str, frozenset[str]],
56
- ) -> Marker | None:
57
- """Маркер из строки, если он там есть.
56
+ ) -> tuple[Marker, ...]:
57
+ """Маркеры строки: их может быть несколько.
58
+
59
+ Подпись в столбик собирает пометки в одну строку — `# signature-ok: так
60
+ зовёт библиотека # type-ok: сырой ввод`, — и снимать они должны обе.
58
61
 
59
62
  Разбор нарочно не падает на кривой записи: маркер без кода или без причины
60
63
  читается и попадает в `complaints`, иначе о нём никто бы не узнал.
61
64
  """
62
- if MARKER in line:
63
- codes, _, reason = line.split(MARKER, maxsplit=1)[1].partition(":")
64
- if not _named(text=codes):
65
- return None
66
- return Marker(
67
- codes=frozenset(_codes(text=codes)),
68
- reason=reason.strip(),
69
- column=line.index(MARKER) + 1,
65
+ found = sorted(
66
+ _starts(
67
+ line=line,
68
+ aliases=aliases,
70
69
  )
71
- for text, codes in aliases.items():
72
- if text in line:
73
- return Marker(
74
- codes=codes,
75
- reason=line.split(text, maxsplit=1)[1].removeprefix(":").strip(),
76
- column=line.index(text) + 1,
70
+ )
71
+ bounds = [*(start for start, _ in found), len(line)]
72
+ return tuple(
73
+ marker
74
+ for number, (start, word) in enumerate(found)
75
+ if (
76
+ marker := _marker(
77
+ text=line[start : bounds[number + 1]],
78
+ word=word,
79
+ aliases=aliases,
80
+ column=start + 1,
77
81
  )
78
- return None
82
+ )
83
+ )
84
+
85
+
86
+ def _starts(
87
+ *,
88
+ line: str,
89
+ aliases: Mapping[str, frozenset[str]],
90
+ ) -> Iterator[tuple[int, str]]:
91
+ for word in (MARKER, *aliases):
92
+ start = line.find(word)
93
+ if start != -1:
94
+ yield start, word
95
+
96
+
97
+ def _marker(
98
+ *,
99
+ text: str,
100
+ word: str,
101
+ aliases: Mapping[str, frozenset[str]],
102
+ column: int,
103
+ ) -> Marker | None:
104
+ tail = text.removeprefix(word)
105
+ if word != MARKER:
106
+ return Marker(
107
+ codes=aliases[word],
108
+ reason=tail.removeprefix(":").strip(),
109
+ column=column,
110
+ )
111
+ codes, _, reason = tail.partition(":")
112
+ if not _named(text=codes):
113
+ return None
114
+ return Marker(
115
+ codes=frozenset(_codes(text=codes)),
116
+ reason=reason.strip(),
117
+ column=column,
118
+ )
79
119
 
80
120
 
81
121
  def surviving(
@@ -109,18 +149,16 @@ def complaints(
109
149
  нарушение.
110
150
  """
111
151
  for number, line in enumerate(file.lines, start=1):
112
- marker = read(
152
+ for marker in markers(
113
153
  line=line,
114
154
  aliases=aliases,
115
- )
116
- if marker is None:
117
- continue
118
- yield from _wrong(
119
- marker=marker,
120
- path=file.path,
121
- line=number,
122
- known=known,
123
- )
155
+ ):
156
+ yield from _wrong(
157
+ marker=marker,
158
+ path=file.path,
159
+ line=number,
160
+ known=known,
161
+ )
124
162
 
125
163
 
126
164
  def _named(*, text: str) -> bool:
@@ -146,17 +184,17 @@ def _covered(
146
184
  file: ParsedFile,
147
185
  aliases: Mapping[str, frozenset[str]],
148
186
  ) -> bool:
149
- for line in _span(
150
- violation=violation,
151
- file=file,
152
- ):
153
- marker = read(
187
+ return any(
188
+ violation.code in marker.codes
189
+ for line in _span(
190
+ violation=violation,
191
+ file=file,
192
+ )
193
+ for marker in markers(
154
194
  line=line,
155
195
  aliases=aliases,
156
196
  )
157
- if marker is not None and violation.code in marker.codes:
158
- return True
159
- return False
197
+ )
160
198
 
161
199
 
162
200
  def _span(
@@ -56,9 +56,12 @@ def render(
56
56
  seen=set(),
57
57
  )
58
58
  ]
59
+ head = declared.header.strip() or HEADER.format(
60
+ section=f"{prefix(source=config.origin)}{SECTION}"
61
+ )
59
62
  return root / declared.path, "\n".join(
60
63
  [
61
- HEADER.format(section=f"{prefix(source=config.origin)}{SECTION}"),
64
+ f"{head.rstrip()}\n",
62
65
  *blocks,
63
66
  ]
64
67
  )
@@ -22,10 +22,15 @@ class Example(CheckSettings):
22
22
  «ничего не собирать»: проект без настроек из окружения — обычное дело.
23
23
 
24
24
  `path` — куда писать; по умолчанию `.env.example` в корне.
25
+
26
+ `header` — шапка файла со своим текстом, вместе с `#`. Пустая строка
27
+ оставляет шапку библиотеки: проекту, который пишет комментарии на другом
28
+ языке, своя нужна, остальным — нет.
25
29
  """
26
30
 
27
31
  settings: tuple[str, ...] = ()
28
32
  path: str = FILE
33
+ header: str = ""
29
34
 
30
35
 
31
36
  def example(*, config: Config) -> Example:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-checks
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Проверки архитектурных соглашений проекта
5
5
  Author: Armontex
6
6
  Author-email: Armontex <windle1337@gmail.com>
@@ -181,6 +181,13 @@ text("SELECT 1") # db-ok: raw-sql: проба живости, формы ORM н
181
181
  правило. У каждой группы есть своё короткое слово (`# db-ok`, `# type-ok`,
182
182
  `# signature-ok`, …): человек помнит группу, а не двадцать семь кодов.
183
183
 
184
+ Пометок в строке может стоять несколько — подпись в столбик собирает их на
185
+ последней строке:
186
+
187
+ ```python
188
+ ) -> object: # signature-ok: так зовёт pydantic # type-ok: сырой ввод
189
+ ```
190
+
184
191
  Пометка без кода, с опечаткой в коде или без причины — сама нарушение. Молча
185
192
  неработающая пометка выглядит как отключённая проверка, а на деле проверка
186
193
  работает и просто её не видит.
@@ -246,6 +253,10 @@ settings = ["myservice.config.settings:Settings"]
246
253
  докстринга класса и `description` поля, если оно у него есть, — так объяснение
247
254
  живёт рядом с полем, а не в файле, который его переживёт.
248
255
 
256
+ Шапку файла можно написать свою — `header = "# Generated by py-checks."`,
257
+ вместе с `#`. Нужна она проекту, который пишет комментарии на другом языке;
258
+ пустая оставляет шапку библиотеки.
259
+
249
260
  Классы импортируются, а не читаются текстом: имя переменной — значение
250
261
  атрибута, собранное вызовом, и прочитать его исходником значит выполнить этот
251
262
  вызов самому.
@@ -72,14 +72,14 @@ py_checks/contracts/_constants.py,sha256=nKfAs2IJdFdxCxazEG0t_AIeBt6QdAkhhNHNE9d
72
72
  py_checks/contracts/_layout.py,sha256=wlZSk0ioVGHtWcnHTwwSGOV35XDi6to0XuOhqqp6Mx0,1829
73
73
  py_checks/contracts/_render.py,sha256=vMrQuzheihJS-D367tW3KNQvf8jhOZWWptLFLGvO4hE,6598
74
74
  py_checks/contracts/_settings.py,sha256=ItaYTDCZGHEnTaCgPkNwuE0FFlvzM4JABfBpE61dzVI,1556
75
- py_checks/core/__init__.py,sha256=s0VbZarK_R-_ayKzFfKLhFyagN16Huqlu602yVqmmB0,1684
75
+ py_checks/core/__init__.py,sha256=9XspaoOu-6X7hBLVjyXuXNTyghji_ttC_tQqc82JgsA,1690
76
76
  py_checks/core/_constants.py,sha256=G0h499Qoyuqg_PSgfl9El3ObESdoyFbX8BkAt2jvkpE,820
77
77
  py_checks/core/_discovery.py,sha256=e2JEGepp3TZpn-v_U7U28wGCvbcyO2jQJ3VawnYMlcQ,1673
78
78
  py_checks/core/_edit.py,sha256=A0AL6zrWaZK8kb40Ix750jBSO6BZrgVEEaoYSjLnGok,3266
79
79
  py_checks/core/_errors.py,sha256=SsMCn3vVrtfyD56mJ6T_gpeot8H7Ke08YJu-P3AmQ18,794
80
80
  py_checks/core/_fixer.py,sha256=tPC_OEa6wx5TwDlRo-CHu6itjpIqR55wZWrGk1Ccb3M,1783
81
81
  py_checks/core/_format.py,sha256=bo8CFVUiDsky4TtJRyYs5r9Zx8SlUpP93MFLMEEHDhE,1236
82
- py_checks/core/_markers.py,sha256=Ig3k9n1ou5BDm1G_ZNoUWIQqQslrgseLF1PwIjpEeWo,7611
82
+ py_checks/core/_markers.py,sha256=0mNJdQdrrU9A53kGvJ2OjrbJhMi1ok8yRVIQQG4l7kg,8497
83
83
  py_checks/core/_protocols.py,sha256=DosXZvaQ8HkudgS0EXb2qVe7V74TVc7U7lib3_nu56M,3861
84
84
  py_checks/core/_registry.py,sha256=x1ltnTSJ0GOsK8xbaUQhK7SgKUur1MVrEFZUHCZxRdg,3109
85
85
  py_checks/core/_report.py,sha256=6vj8g8jImS1Pl8KmdrdXkTfInC9HVQ6u85BgIpPGq00,1459
@@ -89,13 +89,13 @@ py_checks/core/_source.py,sha256=Y-L5l6ItK08szJu7m2XCrz7O0HrZR6nI7LwR0tyXFtg,343
89
89
  py_checks/core/_violation.py,sha256=o8_SQxjCcwRoIJWskpoMh3A44zwAYmdGDVsleiBXfzI,2702
90
90
  py_checks/environment/__init__.py,sha256=Mq-ItbrQHuo8Y1UhdxhI7dcBv9XbAVisHruaBnF3ZME,891
91
91
  py_checks/environment/_constants.py,sha256=yZm4Aqyx8FJTsSBM0J7g6HKdEntSr2L5_q0rXm4LmhE,205
92
- py_checks/environment/_render.py,sha256=7iW35KTeAhbXpeS14-A96r0dPg92OiI8RDl1agW_L9g,8728
93
- py_checks/environment/_settings.py,sha256=1kWDHkEnZuHPCMdyagVaQAiaJUHshauixQkKBHB6KX8,1426
92
+ py_checks/environment/_render.py,sha256=QK_uqdgRvsuyKesBcTu8E3WKQVFH1fBBiPE9YjJwwS4,8801
93
+ py_checks/environment/_settings.py,sha256=Pr0xE05tt7iagffzK11NncTN22QeoZKtxA2zuCCEmf0,1773
94
94
  py_checks/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
95
  py_checks/sync/__init__.py,sha256=Kg08BC7arCTqdFk2tO3qm95_jG-X0vxK-GMvVYJEkXc,1041
96
96
  py_checks/sync/_sync.py,sha256=KqA65H4zeP6y17LwlRR8svpCuhUdFCY3KIwG3YtuvAk,2254
97
- python_checks-0.1.1.dist-info/licenses/LICENSE,sha256=omcAXq_cAQNMI5X3fRdH7UC6W2Zc8AC-G9xIZ2U1xF0,1065
98
- python_checks-0.1.1.dist-info/WHEEL,sha256=R1d3uUTbmXM1FHXH_itQashbrqrOSVj-hvBCpmkIIGE,81
99
- python_checks-0.1.1.dist-info/entry_points.txt,sha256=59rS_KMwYdFnQ9HuhkBpKQNKySNAALyneIFO_-cVQvw,1679
100
- python_checks-0.1.1.dist-info/METADATA,sha256=ecPuXkHjy6WaZ1HBfIQ2HOYtjAM7PcCYqpIp1Ga_IhE,18869
101
- python_checks-0.1.1.dist-info/RECORD,,
97
+ python_checks-0.2.0.dist-info/licenses/LICENSE,sha256=omcAXq_cAQNMI5X3fRdH7UC6W2Zc8AC-G9xIZ2U1xF0,1065
98
+ python_checks-0.2.0.dist-info/WHEEL,sha256=R1d3uUTbmXM1FHXH_itQashbrqrOSVj-hvBCpmkIIGE,81
99
+ python_checks-0.2.0.dist-info/entry_points.txt,sha256=59rS_KMwYdFnQ9HuhkBpKQNKySNAALyneIFO_-cVQvw,1679
100
+ python_checks-0.2.0.dist-info/METADATA,sha256=BkUvC0fGjPHRTS1wn_38b4Zi4xn3ma0LDfDu-8F5i7c,19452
101
+ python_checks-0.2.0.dist-info/RECORD,,