opencrbasic 1.0.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.
- opencrbasic/__init__.py +3 -0
- opencrbasic/__main__.py +5 -0
- opencrbasic/analysis.py +184 -0
- opencrbasic/bindings.py +1034 -0
- opencrbasic/catalog.py +173 -0
- opencrbasic/checkconfig.py +144 -0
- opencrbasic/checker/__init__.py +977 -0
- opencrbasic/checker/cr3xx.py +3118 -0
- opencrbasic/checker/ess.py +81 -0
- opencrbasic/checker/expressions.py +3581 -0
- opencrbasic/checker/instr_data.py +1802 -0
- opencrbasic/checker/instr_measure.py +1547 -0
- opencrbasic/checker/instr_rules.py +3540 -0
- opencrbasic/checker/instructions.py +17 -0
- opencrbasic/checker/intervals.py +678 -0
- opencrbasic/checker/longconv.py +133 -0
- opencrbasic/checker/scan_time.py +580 -0
- opencrbasic/checker/storage.py +456 -0
- opencrbasic/checker/structs.py +240 -0
- opencrbasic/checker/structure.py +6213 -0
- opencrbasic/checker/symbols.py +609 -0
- opencrbasic/checker/terminals.py +249 -0
- opencrbasic/checker/unused.py +186 -0
- opencrbasic/checker/var_types.py +564 -0
- opencrbasic/checker/warnings.py +661 -0
- opencrbasic/checks.py +491 -0
- opencrbasic/cli.py +391 -0
- opencrbasic/commands.py +128 -0
- opencrbasic/compiler.py +320 -0
- opencrbasic/consteval.py +327 -0
- opencrbasic/data/background_calibration.json +4 -0
- opencrbasic/data/before_beginprog.json +99 -0
- opencrbasic/data/cr1000x/background_calibration.json +4 -0
- opencrbasic/data/cr1000x/before_beginprog.json +138 -0
- opencrbasic/data/cr1000x/ess_variables.json +1087 -0
- opencrbasic/data/cr1000x/fnotch.json +29 -0
- opencrbasic/data/cr1000x/functions.json +1337 -0
- opencrbasic/data/cr1000x/instructions.json +4836 -0
- opencrbasic/data/cr1000x/measure.json +121 -0
- opencrbasic/data/cr1000x/mode.json +111 -0
- opencrbasic/data/cr1000x/pipeline_tasks.json +88 -0
- opencrbasic/data/cr1000x/settings_fields.json +321 -0
- opencrbasic/data/cr1000x/status_members.json +318 -0
- opencrbasic/data/cr1000x/symbols.json +872 -0
- opencrbasic/data/cr1000x/terminals.json +225 -0
- opencrbasic/data/cr300/before_beginprog.json +42 -0
- opencrbasic/data/cr300/ess_variables.json +1087 -0
- opencrbasic/data/cr300/fnotch.json +22 -0
- opencrbasic/data/cr300/functions.json +1261 -0
- opencrbasic/data/cr300/instructions.json +2837 -0
- opencrbasic/data/cr300/measure.json +33 -0
- opencrbasic/data/cr300/mode.json +121 -0
- opencrbasic/data/cr300/settings_fields.json +259 -0
- opencrbasic/data/cr300/status_members.json +303 -0
- opencrbasic/data/cr300/symbols.json +584 -0
- opencrbasic/data/cr300/terminals.json +193 -0
- opencrbasic/data/cr350/before_beginprog.json +44 -0
- opencrbasic/data/cr350/ess_variables.json +1087 -0
- opencrbasic/data/cr350/fnotch.json +22 -0
- opencrbasic/data/cr350/functions.json +1262 -0
- opencrbasic/data/cr350/instructions.json +2848 -0
- opencrbasic/data/cr350/measure.json +35 -0
- opencrbasic/data/cr350/mode.json +121 -0
- opencrbasic/data/cr350/settings_fields.json +292 -0
- opencrbasic/data/cr350/status_members.json +303 -0
- opencrbasic/data/cr350/symbols.json +572 -0
- opencrbasic/data/cr350/terminals.json +190 -0
- opencrbasic/data/ess_variables.json +1087 -0
- opencrbasic/data/fnotch.json +34 -0
- opencrbasic/data/functions.json +1337 -0
- opencrbasic/data/instructions.json +4965 -0
- opencrbasic/data/measure.json +123 -0
- opencrbasic/data/mode.json +121 -0
- opencrbasic/data/notices_cr6.json +890 -0
- opencrbasic/data/pipeline_tasks.json +84 -0
- opencrbasic/data/settings_fields.json +322 -0
- opencrbasic/data/status_members.json +319 -0
- opencrbasic/data/status_table.json +684 -0
- opencrbasic/data/symbols.json +902 -0
- opencrbasic/data/terminals.json +195 -0
- opencrbasic/data/type_codes.json +249 -0
- opencrbasic/diagnostics.py +196 -0
- opencrbasic/editing.py +827 -0
- opencrbasic/formatting.py +310 -0
- opencrbasic/helpdocs.py +677 -0
- opencrbasic/hover.py +667 -0
- opencrbasic/inlay.py +169 -0
- opencrbasic/lexer.py +183 -0
- opencrbasic/lsp/__init__.py +5 -0
- opencrbasic/lsp/__main__.py +7 -0
- opencrbasic/lsp/config.py +202 -0
- opencrbasic/lsp/features.py +602 -0
- opencrbasic/lsp/server.py +741 -0
- opencrbasic/lsp/session.py +1189 -0
- opencrbasic/lsp/worker.py +128 -0
- opencrbasic/mode.py +143 -0
- opencrbasic/navigation.py +401 -0
- opencrbasic/parser.py +3264 -0
- opencrbasic/positions.py +274 -0
- opencrbasic/preprocess.py +269 -0
- opencrbasic/profiles.py +266 -0
- opencrbasic/provenance.py +17 -0
- opencrbasic/quickfix.py +331 -0
- opencrbasic/rename.py +662 -0
- opencrbasic/signature.py +33 -0
- opencrbasic/source.py +600 -0
- opencrbasic/sources.py +125 -0
- opencrbasic/syntax.py +337 -0
- opencrbasic/tables.py +2173 -0
- opencrbasic/tables_hook.py +14 -0
- opencrbasic/tdf.py +142 -0
- opencrbasic/timing.py +565 -0
- opencrbasic-1.0.0.dist-info/METADATA +223 -0
- opencrbasic-1.0.0.dist-info/RECORD +117 -0
- opencrbasic-1.0.0.dist-info/WHEEL +4 -0
- opencrbasic-1.0.0.dist-info/entry_points.txt +3 -0
- opencrbasic-1.0.0.dist-info/licenses/LICENSE +21 -0
opencrbasic/catalog.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Instruction catalog: the names a CRBasic program can use, per logger profile.
|
|
2
|
+
|
|
3
|
+
Built only from `profiles.get(name)` (`instructions`, `functions`, `symbols`, the tables extracted
|
|
4
|
+
from each logger's compiler by `tools/extract_tables.py`). There is no second instruction list.
|
|
5
|
+
`docs/lsp-api.md` ("Instruction catalog") describes the kinds, the overlaps and the keyword rule.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import functools
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from . import profiles
|
|
13
|
+
|
|
14
|
+
KINDS = ("instruction", "function", "symbol", "keyword")
|
|
15
|
+
|
|
16
|
+
# Words that structure a program or a declaration. Each is also a row of the instruction table of
|
|
17
|
+
# the profiles that have it (the parser dispatches on it in `Parser._parse_statement`, or reads it
|
|
18
|
+
# as an ordinary instruction row such as `NextScan`, `EndTable`). A profile gets the entry only
|
|
19
|
+
# when its own instruction table has the word, with the table's spelling.
|
|
20
|
+
TABLE_KEYWORDS = (
|
|
21
|
+
"If",
|
|
22
|
+
"ElseIf",
|
|
23
|
+
"Else",
|
|
24
|
+
"EndIf",
|
|
25
|
+
"Select",
|
|
26
|
+
"Case",
|
|
27
|
+
"EndSelect",
|
|
28
|
+
"For",
|
|
29
|
+
"Next",
|
|
30
|
+
"Do",
|
|
31
|
+
"Loop",
|
|
32
|
+
"While",
|
|
33
|
+
"WEnd",
|
|
34
|
+
"Exit",
|
|
35
|
+
"ExitFor",
|
|
36
|
+
"ExitDo",
|
|
37
|
+
"ExitSub",
|
|
38
|
+
"ExitFunction",
|
|
39
|
+
"ExitScan",
|
|
40
|
+
"ContinueScan",
|
|
41
|
+
"Return",
|
|
42
|
+
"Sub",
|
|
43
|
+
"EndSub",
|
|
44
|
+
"Function",
|
|
45
|
+
"EndFunction",
|
|
46
|
+
"Call",
|
|
47
|
+
"End",
|
|
48
|
+
"Const",
|
|
49
|
+
"Public",
|
|
50
|
+
"Dim",
|
|
51
|
+
"Alias",
|
|
52
|
+
"Units",
|
|
53
|
+
"Include",
|
|
54
|
+
"IncludeSection",
|
|
55
|
+
"BeginProg",
|
|
56
|
+
"EndProg",
|
|
57
|
+
"Scan",
|
|
58
|
+
"SubScan",
|
|
59
|
+
"NextScan",
|
|
60
|
+
"NextSubScan",
|
|
61
|
+
"DataTable",
|
|
62
|
+
"EndTable",
|
|
63
|
+
"ConstTable",
|
|
64
|
+
"EndConstTable",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Language words the instruction table lacks. Each is matched by name in the parser
|
|
68
|
+
# (`parser.py`): `Then` (`_parse_if`, `_parse_elseif`), `Step` (`_parse_for`), `As` (declarations,
|
|
69
|
+
# `_parse_subdef`, `_parse_param`), `Optional` (`_parse_param`). They are the same on every profile.
|
|
70
|
+
EXTRA_KEYWORDS = ("Then", "Step", "As", "Optional")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class CatalogEntry:
|
|
75
|
+
name: str # the profile's spelling
|
|
76
|
+
kind: str # one of KINDS
|
|
77
|
+
profile: str # profile name ("cr6", ...)
|
|
78
|
+
source: str # "instructions.json" | "functions.json" | "symbols.json" |
|
|
79
|
+
# "keyword:table" | "keyword:parser"
|
|
80
|
+
params: tuple[int, ...] = () # instruction: type codes (docs/type-codes.md)
|
|
81
|
+
value: int | None = None # symbol: the integer value
|
|
82
|
+
code: int | None = None # function: table `code`
|
|
83
|
+
func_kind: int | None = None # function: table `kind`
|
|
84
|
+
argspec: int | None = None # function: table `argspec`
|
|
85
|
+
nargs: int | None = None # function: present only for some rows
|
|
86
|
+
argtypes: tuple[int, ...] | None = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _build(name: str) -> tuple[CatalogEntry, ...]:
|
|
90
|
+
prof = profiles.get(name)
|
|
91
|
+
out: list[CatalogEntry] = []
|
|
92
|
+
for row in prof.instructions.values():
|
|
93
|
+
out.append(
|
|
94
|
+
CatalogEntry(
|
|
95
|
+
row["name"], "instruction", prof.name, "instructions.json", params=tuple(row.get("params", ()))
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
for row in prof.functions.values():
|
|
99
|
+
argtypes = row.get("argtypes")
|
|
100
|
+
out.append(
|
|
101
|
+
CatalogEntry(
|
|
102
|
+
row["name"],
|
|
103
|
+
"function",
|
|
104
|
+
prof.name,
|
|
105
|
+
"functions.json",
|
|
106
|
+
code=row.get("code"),
|
|
107
|
+
func_kind=row.get("kind"),
|
|
108
|
+
argspec=row.get("argspec"),
|
|
109
|
+
nargs=row.get("nargs"),
|
|
110
|
+
argtypes=None if argtypes is None else tuple(argtypes),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
for row in prof.symbols.values():
|
|
114
|
+
out.append(CatalogEntry(row["name"], "symbol", prof.name, "symbols.json", value=row.get("value")))
|
|
115
|
+
for word in TABLE_KEYWORDS:
|
|
116
|
+
row = prof.instructions.get(word.upper())
|
|
117
|
+
if row is not None:
|
|
118
|
+
out.append(CatalogEntry(row["name"], "keyword", prof.name, "keyword:table"))
|
|
119
|
+
for word in EXTRA_KEYWORDS:
|
|
120
|
+
out.append(CatalogEntry(word, "keyword", prof.name, "keyword:parser"))
|
|
121
|
+
return tuple(out)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class InstructionCatalog:
|
|
125
|
+
"""Every name of one profile, by kind. Lookup is case-insensitive and keeps the profile's
|
|
126
|
+
spelling. A name may exist under several kinds (`If` is an instruction row, a symbol and a
|
|
127
|
+
keyword); `get` returns one entry by the priority instruction, function, symbol, keyword and
|
|
128
|
+
`entries` returns all of them."""
|
|
129
|
+
|
|
130
|
+
def __init__(self, profile: str):
|
|
131
|
+
self.profile = profiles.get(profile).name
|
|
132
|
+
self._entries = _build(self.profile)
|
|
133
|
+
self._by_name: dict[str, list[CatalogEntry]] = {}
|
|
134
|
+
for entry in self._entries:
|
|
135
|
+
self._by_name.setdefault(entry.name.upper(), []).append(entry)
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def for_profile(cls, name: str) -> InstructionCatalog:
|
|
139
|
+
"""The cached catalog of a profile name (`ValueError` for an unknown one)."""
|
|
140
|
+
return _cached(profiles.get(name).name)
|
|
141
|
+
|
|
142
|
+
def entries(self, name: str) -> tuple[CatalogEntry, ...]:
|
|
143
|
+
return tuple(self._by_name.get(name.upper(), ()))
|
|
144
|
+
|
|
145
|
+
def get(self, name: str, kind: str | None = None) -> CatalogEntry | None:
|
|
146
|
+
found = self._by_name.get(name.upper(), ())
|
|
147
|
+
if kind is not None:
|
|
148
|
+
return next((e for e in found if e.kind == kind), None)
|
|
149
|
+
for k in KINDS:
|
|
150
|
+
for e in found:
|
|
151
|
+
if e.kind == k:
|
|
152
|
+
return e
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
def names(self, kind: str | None = None) -> tuple[str, ...]:
|
|
156
|
+
"""Profile spellings of one kind (all kinds when None), sorted case-insensitively and
|
|
157
|
+
without repeats."""
|
|
158
|
+
seen = {e.name for e in self._entries if kind is None or e.kind == kind}
|
|
159
|
+
return tuple(sorted(seen, key=lambda n: (n.upper(), n)))
|
|
160
|
+
|
|
161
|
+
def __iter__(self) -> Iterator[CatalogEntry]:
|
|
162
|
+
return iter(self._entries)
|
|
163
|
+
|
|
164
|
+
def __len__(self) -> int:
|
|
165
|
+
return len(self._entries)
|
|
166
|
+
|
|
167
|
+
def __contains__(self, name: object) -> bool:
|
|
168
|
+
return isinstance(name, str) and name.upper() in self._by_name
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@functools.cache
|
|
172
|
+
def _cached(name: str) -> InstructionCatalog:
|
|
173
|
+
return InstructionCatalog(name)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""`opencrbasic.toml`: check severities and scoped suppressions (`PLAN.md` "Configuration",
|
|
2
|
+
`docs/checker.md` "Check configuration and report file"). Configuration never touches the reference
|
|
3
|
+
channel: every problem is a `ConfigError` that the caller reports on stderr."""
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
import os
|
|
7
|
+
import tomllib
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from . import profiles
|
|
12
|
+
|
|
13
|
+
FILE_NAME = "opencrbasic.toml"
|
|
14
|
+
SEVERITIES = ("error", "warning", "info", "off")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigError(Exception):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Suppression:
|
|
23
|
+
code: str
|
|
24
|
+
file: str # glob, relative to the configuration file, case-insensitive
|
|
25
|
+
line: int | None
|
|
26
|
+
reason: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Config:
|
|
31
|
+
path: Path | None = None # None: no file, built-in defaults
|
|
32
|
+
checks: dict[str, str] = field(default_factory=dict)
|
|
33
|
+
profile: dict[str, dict[str, str]] = field(default_factory=dict)
|
|
34
|
+
entry: dict[str, dict[str, str]] = field(default_factory=dict) # key: normalized relative path
|
|
35
|
+
suppress: list[Suppression] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def root(self) -> Path | None:
|
|
39
|
+
return self.path.parent if self.path else None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def norm(path: str) -> str:
|
|
43
|
+
return Path(os.path.normpath(path)).as_posix().lower()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def find(entry: Path) -> Path | None:
|
|
47
|
+
"""The workspace root: the nearest ancestor directory of the entry file holding the file."""
|
|
48
|
+
for directory in entry.resolve().parents:
|
|
49
|
+
if (directory / FILE_NAME).is_file():
|
|
50
|
+
return directory / FILE_NAME
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _severities(table: object, codes, where: str) -> dict[str, str]:
|
|
55
|
+
if not isinstance(table, dict):
|
|
56
|
+
raise ConfigError(f"{where} must be a table")
|
|
57
|
+
out = {}
|
|
58
|
+
for code, value in table.items():
|
|
59
|
+
if code not in codes:
|
|
60
|
+
raise ConfigError(f"{where}: unknown check code {code}")
|
|
61
|
+
if value not in SEVERITIES:
|
|
62
|
+
raise ConfigError(f"{where}: unknown severity {value!r} for {code}; choose from {', '.join(SEVERITIES)}")
|
|
63
|
+
out[code] = value
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_overrides(items: list[str], codes) -> dict[str, str]:
|
|
68
|
+
"""`CODE=severity` strings (`--check`, an LSP setting) as a severity map."""
|
|
69
|
+
out = {}
|
|
70
|
+
for item in items:
|
|
71
|
+
code, sep, value = item.partition("=")
|
|
72
|
+
if not sep:
|
|
73
|
+
raise ConfigError(f"--check {item}: expected CODE=SEVERITY")
|
|
74
|
+
out.update(_severities({code: value}, codes, f"--check {item}"))
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load(path: Path, codes) -> Config:
|
|
79
|
+
try:
|
|
80
|
+
text = path.read_text(encoding="utf-8")
|
|
81
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
82
|
+
raise ConfigError(f"{path}: cannot read: {e}") from None
|
|
83
|
+
return loads(text, path, codes)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def loads(text: str, path: Path, codes) -> Config:
|
|
87
|
+
"""`load` of a file's text already read (the language server reads it through its sources)."""
|
|
88
|
+
try:
|
|
89
|
+
data = tomllib.loads(text)
|
|
90
|
+
except tomllib.TOMLDecodeError as e:
|
|
91
|
+
raise ConfigError(f"{path}: {e}") from None
|
|
92
|
+
name = str(path)
|
|
93
|
+
if unknown := set(data) - {"checks", "profile", "entry", "suppress"}:
|
|
94
|
+
raise ConfigError(f"{name}: unknown key {sorted(unknown)[0]}")
|
|
95
|
+
cfg = Config(path=path.resolve())
|
|
96
|
+
cfg.checks = _severities(data.get("checks", {}), codes, f"{name} [checks]")
|
|
97
|
+
for logger, table in _tables(data, "profile", name).items():
|
|
98
|
+
if logger not in profiles.PROFILES:
|
|
99
|
+
raise ConfigError(f"{name} [profile.{logger}]: unknown logger profile")
|
|
100
|
+
cfg.profile[logger] = _severities(table.get("checks", {}), codes, f"{name} [profile.{logger}.checks]")
|
|
101
|
+
for entry, table in _tables(data, "entry", name).items():
|
|
102
|
+
cfg.entry[norm(entry)] = _severities(table.get("checks", {}), codes, f'{name} [entry."{entry}".checks]')
|
|
103
|
+
suppress = data.get("suppress", [])
|
|
104
|
+
if not isinstance(suppress, list):
|
|
105
|
+
raise ConfigError(f"{name}: suppress must be an array of tables ([[suppress]])")
|
|
106
|
+
for i, s in enumerate(suppress, 1):
|
|
107
|
+
where = f"{name} [[suppress]] #{i}"
|
|
108
|
+
if not isinstance(s, dict) or set(s) - {"code", "file", "line", "reason"}:
|
|
109
|
+
raise ConfigError(f"{where}: keys are code, file, line, reason")
|
|
110
|
+
if not isinstance(s.get("code"), str) or s["code"] not in codes:
|
|
111
|
+
raise ConfigError(f"{where}: unknown check code {s.get('code')}")
|
|
112
|
+
if not isinstance(s.get("file"), str) or not s["file"]:
|
|
113
|
+
raise ConfigError(f"{where}: file (a glob) is required")
|
|
114
|
+
if not isinstance(s.get("reason"), str) or not s["reason"].strip():
|
|
115
|
+
raise ConfigError(f"{where}: reason is required")
|
|
116
|
+
line = s.get("line")
|
|
117
|
+
if line is not None and (not isinstance(line, int) or isinstance(line, bool) or line < 1):
|
|
118
|
+
raise ConfigError(f"{where}: line must be a positive integer")
|
|
119
|
+
cfg.suppress.append(Suppression(s["code"], s["file"].lower(), line, s["reason"]))
|
|
120
|
+
return cfg
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _tables(data: dict, key: str, name: str) -> dict[str, dict]:
|
|
124
|
+
tables = data.get(key, {})
|
|
125
|
+
if not isinstance(tables, dict) or not all(isinstance(t, dict) for t in tables.values()):
|
|
126
|
+
raise ConfigError(f"{name}: {key} must hold tables ([{key}.<name>.checks])")
|
|
127
|
+
return tables
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def severity(
|
|
131
|
+
cfg: Config, code: str, default: str, logger: str, entry: str, cli: dict[str, str], lsp: dict[str, str]
|
|
132
|
+
) -> str:
|
|
133
|
+
"""Highest layer that names `code`: CLI, LSP, entry, profile, `[checks]`, built-in default."""
|
|
134
|
+
for layer in (cli, lsp, cfg.entry.get(entry, {}), cfg.profile.get(logger, {}), cfg.checks):
|
|
135
|
+
if code in layer:
|
|
136
|
+
return layer[code]
|
|
137
|
+
return default
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def suppression(cfg: Config, code: str, file: str, line: int) -> Suppression | None:
|
|
141
|
+
return next(
|
|
142
|
+
(s for s in cfg.suppress if s.code == code and fnmatch.fnmatchcase(file, s.file) and s.line in (None, line)),
|
|
143
|
+
None,
|
|
144
|
+
)
|