refdef 0.1.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.
refdef/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ """Reference designations as strict, typed, immutable values."""
2
+
3
+ from refdef._check import check
4
+ from refdef._parse import parse
5
+ from refdef._render import render
6
+ from refdef.designation import Designation
7
+ from refdef.issues import Issue, IssueCode, Parsed
8
+ from refdef.level import Aspect, Level
9
+ from refdef.numbering import allocate, number_group
10
+ from refdef.order import natural_key, sort_key
11
+ from refdef.scheme import STANDARD, ClassEntry, Scheme
12
+ from refdef.terminal import Signal, Terminal
13
+
14
+ __all__ = [
15
+ "STANDARD",
16
+ "Aspect",
17
+ "ClassEntry",
18
+ "Designation",
19
+ "Issue",
20
+ "IssueCode",
21
+ "Level",
22
+ "Parsed",
23
+ "Scheme",
24
+ "Signal",
25
+ "Terminal",
26
+ "allocate",
27
+ "check",
28
+ "natural_key",
29
+ "number_group",
30
+ "parse",
31
+ "render",
32
+ "sort_key",
33
+ ]
refdef/__main__.py ADDED
@@ -0,0 +1,18 @@
1
+ """Command line: `python -m refdef api` prints the public surface."""
2
+
3
+ import sys
4
+
5
+ from refdef._api import DOORS, render
6
+
7
+
8
+ def main(argv: list[str]) -> int:
9
+ """Run the one command, `api`; any other argument is a usage error (exit 2)."""
10
+ if argv != ["api"]:
11
+ sys.stderr.write("usage: python -m refdef api\n")
12
+ return 2
13
+ sys.stdout.write(render(DOORS))
14
+ return 0
15
+
16
+
17
+ if __name__ == "__main__":
18
+ sys.exit(main(sys.argv[1:]))
refdef/_api.py ADDED
@@ -0,0 +1,42 @@
1
+ """The surface printer: `render` lists every door's `__all__` with signatures and docstrings."""
2
+
3
+ import annotationlib
4
+ import dataclasses
5
+ import enum
6
+ import importlib
7
+ import inspect
8
+
9
+ DOORS: tuple[str, ...] = ("refdef", "refdef.plc")
10
+
11
+
12
+ def _signature(obj: object) -> str:
13
+ """The signature as source text, so 3.14 and 3.15 print the same bytes."""
14
+ return str(inspect.signature(obj, annotation_format=annotationlib.Format.STRING)) # ty: ignore[invalid-argument-type] - obj is callable here
15
+
16
+
17
+ def _head(name: str, obj: object) -> str:
18
+ """The first line of a name's entry."""
19
+ if isinstance(obj, type) and issubclass(obj, enum.Enum):
20
+ return f"class {name}"
21
+ if isinstance(obj, type) and dataclasses.is_dataclass(obj):
22
+ return f"class {name}{_signature(obj)}"
23
+ if callable(obj):
24
+ return f"{name}{_signature(obj)}"
25
+ return f"{name}: {type(obj).__name__}"
26
+
27
+
28
+ def _entry(name: str, obj: object) -> list[str]:
29
+ """A name's head, its docstring indented four spaces, and a blank line."""
30
+ doc = inspect.getdoc(obj if callable(obj) else type(obj)) or ""
31
+ return [_head(name, obj), *(f" {line}".rstrip() for line in doc.splitlines()), ""]
32
+
33
+
34
+ def render(doors: tuple[str, ...]) -> str:
35
+ """Return the public surface of `doors` as text: deterministic, one final newline."""
36
+ lines: list[str] = []
37
+ for door in doors:
38
+ module = importlib.import_module(door)
39
+ lines.append(f"## {door}")
40
+ for name in sorted(module.__all__):
41
+ lines += _entry(name, getattr(module, name))
42
+ return "\n".join(lines).rstrip("\n") + "\n"
refdef/_check.py ADDED
@@ -0,0 +1,55 @@
1
+ """The scheme rules: the one place that reads a scheme (R5)."""
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from refdef.designation import Designation
6
+ from refdef.issues import Issue, IssueCode
7
+ from refdef.level import Level
8
+ from refdef.scheme import Scheme
9
+
10
+
11
+ def _level_issues(level: Level, scheme: Scheme) -> list[Issue]:
12
+ """Aspect rule for every level; letter and class rules for coded levels only."""
13
+ subject = level.aspect.value + level.text
14
+ found = []
15
+ if level.aspect not in scheme.aspects:
16
+ found.append(
17
+ Issue(
18
+ IssueCode.ASPECT_OFF, subject, f"aspect {level.aspect.value} is off in the scheme"
19
+ )
20
+ )
21
+ if not level.code:
22
+ return found
23
+ rule = scheme.letters
24
+ if not rule.min_count <= len(level.code) <= rule.max_count:
25
+ found.append(
26
+ Issue(IssueCode.LETTER_COUNT, subject, f"{level.code} has an odd letter count")
27
+ )
28
+ if rule.never & set(level.code):
29
+ found.append(Issue(IssueCode.NEVER_LETTER, subject, f"{level.code} uses a barred letter"))
30
+ if scheme.classes.get(level.code) is None:
31
+ found.append(Issue(IssueCode.UNKNOWN_CLASS, subject, f"{level.code} is not a known class"))
32
+ return found
33
+
34
+
35
+ def check(designations: Iterable[Designation], scheme: Scheme) -> tuple[Issue, ...]:
36
+ """Issues of the designations against `scheme`, in order; duplicates first for each one.
37
+
38
+ Terminals and signals are not checked: pass their `of`.
39
+ """
40
+ if not isinstance(scheme, Scheme):
41
+ msg = "scheme must be a Scheme"
42
+ raise TypeError(msg)
43
+ found: list[Issue] = []
44
+ seen: set[str] = set()
45
+ for design in designations:
46
+ if not isinstance(design, Designation):
47
+ msg = "designations must hold Designation"
48
+ raise TypeError(msg)
49
+ text = str(design)
50
+ if text in seen:
51
+ found.append(Issue(IssueCode.DUPLICATE, text, "designation used twice"))
52
+ seen.add(text)
53
+ for level in design.levels:
54
+ found += _level_issues(level, scheme)
55
+ return tuple(found)
refdef/_heads.py ADDED
@@ -0,0 +1,70 @@
1
+ """Helpers on tuples of levels; no Designation import, so no import cycle."""
2
+
3
+ from refdef.level import Aspect, Level
4
+
5
+
6
+ def only(levels: tuple[Level, ...], aspects: tuple[Aspect, ...]) -> tuple[Level, ...]:
7
+ """The levels whose aspect is listed, in order."""
8
+ return tuple(level for level in levels if level.aspect in aspects)
9
+
10
+
11
+ def without_last(levels: tuple[Level, ...], aspect: Aspect | None) -> tuple[Level, ...]:
12
+ """The levels without the last one of `aspect` (of any aspect if None); unchanged if none."""
13
+ for index in range(len(levels) - 1, -1, -1):
14
+ if aspect is None or levels[index].aspect is aspect:
15
+ return levels[:index] + levels[index + 1 :]
16
+ return levels
17
+
18
+
19
+ def head(levels: tuple[Level, ...], aspect: Aspect) -> tuple[Level, ...]:
20
+ """The levels of `aspect`, in order."""
21
+ return tuple(level for level in levels if level.aspect is aspect)
22
+
23
+
24
+ def _aspects(levels: tuple[Level, ...]) -> tuple[Aspect, ...]:
25
+ """The aspects present in `levels`, in order of first appearance."""
26
+ return tuple(dict.fromkeys(level.aspect for level in levels))
27
+
28
+
29
+ def _shared(mine: tuple[Level, ...], other: tuple[Level, ...]) -> int:
30
+ """The length of the common prefix of two level tuples."""
31
+ count = 0
32
+ for left, right in zip(mine, other, strict=False):
33
+ if left != right:
34
+ break
35
+ count += 1
36
+ return count
37
+
38
+
39
+ def within(levels: tuple[Level, ...], root: tuple[Level, ...]) -> bool:
40
+ """True when the root head is a prefix of ours per shared aspect; a root sharing none fails."""
41
+ shared = [a for a in _aspects(root) if a in _aspects(levels)]
42
+ if root and not shared:
43
+ return False
44
+ return all(_shared(head(levels, a), head(root, a)) == len(head(root, a)) for a in shared)
45
+
46
+
47
+ def drop_heads(levels: tuple[Level, ...], root: tuple[Level, ...]) -> tuple[Level, ...]:
48
+ """The levels without, per aspect, as many leading ones as the root's head holds."""
49
+ left = {a: len(head(root, a)) for a in _aspects(root)}
50
+ kept = []
51
+ for level in levels:
52
+ if left.get(level.aspect, 0) > 0:
53
+ left[level.aspect] -= 1
54
+ else:
55
+ kept.append(level)
56
+ return tuple(kept)
57
+
58
+
59
+ def common(first: tuple[Level, ...], *others: tuple[Level, ...]) -> tuple[Level, ...]:
60
+ """Per aspect of `first`, the shortest head all share with it, in the order of `first`."""
61
+ keep = {
62
+ a: min([len(head(first, a)), *(_shared(head(first, a), head(o, a)) for o in others)])
63
+ for a in _aspects(first)
64
+ }
65
+ kept = []
66
+ for level in first:
67
+ if keep[level.aspect] > 0:
68
+ keep[level.aspect] -= 1
69
+ kept.append(level)
70
+ return tuple(kept)
refdef/_parse.py ADDED
@@ -0,0 +1,177 @@
1
+ """The lenient reader: text in, a value and the issues of every repair out."""
2
+
3
+ import re
4
+
5
+ from refdef.designation import Designation
6
+ from refdef.issues import Issue, IssueCode, Parsed
7
+ from refdef.level import Aspect, Level, coded_split
8
+ from refdef.terminal import Signal, Terminal
9
+
10
+ _SIGNS = re.compile(r"(==|\+\+|=|-|\+|%|#|\.)")
11
+ _DIGITS = re.compile(r"[A-Za-z]*([0-9]+)")
12
+ _BY_SIGN = {aspect.value: aspect for aspect in Aspect}
13
+
14
+
15
+ def _issue(issues: list[Issue], code: IssueCode, subject: str, message: str) -> None:
16
+ issues.append(Issue(code, subject, message))
17
+
18
+
19
+ def _clean(text: str) -> str:
20
+ """Outer whitespace off a piece of text; pins and names reach this as plain `str`."""
21
+ return text.strip()
22
+
23
+
24
+ def _split_suffix(text: str) -> tuple[str, str, str]:
25
+ """The text before the first of `: ; &`, that sign, and the rest (sign empty if none)."""
26
+ cuts = [text.index(sign) for sign in ":;&" if sign in text]
27
+ if not cuts:
28
+ return text, "", ""
29
+ cut = min(cuts)
30
+ return text[:cut], text[cut], text[cut + 1 :]
31
+
32
+
33
+ def _take_top(head: str, issues: list[Issue]) -> tuple[str, str]:
34
+ """The top node and the text after it; a head with no `<...>` at its start has no top."""
35
+ if not (head.startswith("<") and ">" in head):
36
+ return "", head
37
+ end = head.index(">")
38
+ raw, rest = head[1:end], head[end + 1 :]
39
+ top = _clean(raw)
40
+ if not top:
41
+ _issue(issues, IssueCode.EMPTY_TOP, raw, "the top node is empty; dropped")
42
+ elif top != raw:
43
+ _issue(issues, IssueCode.WHITESPACE, raw, "outer whitespace of the top node removed")
44
+ return top, rest
45
+
46
+
47
+ def _label(
48
+ aspect: Aspect, body: str, why: tuple[IssueCode, str], issues: list[Issue]
49
+ ) -> Level | None:
50
+ """A label level for text that is not a code, with the issue `why`; None if Level refuses it."""
51
+ try:
52
+ level = Level(aspect, label=body)
53
+ except TypeError:
54
+ _issue(issues, IssueCode.BAD_LABEL, body, "the label holds a reserved character; dropped")
55
+ return None
56
+ _issue(issues, why[0], body, why[1])
57
+ return level
58
+
59
+
60
+ def _not_code(aspect: Aspect, body: str, issues: list[Issue]) -> Level | None:
61
+ """A level for a body that `coded_split` refused: a zero or oversize number, or free text."""
62
+ shape = _DIGITS.fullmatch(body)
63
+ why = (IssueCode.FREE_LABEL, "not letters and a number; kept as a label")
64
+ if shape is not None and shape.group(1).strip("0"):
65
+ why = (IssueCode.NUMBER_TOO_LARGE, "the number is over nine digits; kept as a label")
66
+ elif shape is not None:
67
+ why = (IssueCode.ZERO_NUMBER, "zero is not a number here; kept as a label")
68
+ return _label(aspect, body, why, issues)
69
+
70
+
71
+ def _level(aspect: Aspect, raw: str, issues: list[Issue]) -> Level | None:
72
+ """One body read as a level, with an issue per repair; None if nothing is left of it."""
73
+ body = _clean(raw)
74
+ if body != raw:
75
+ _issue(issues, IssueCode.WHITESPACE, raw, "outer whitespace of the level removed")
76
+ if not body:
77
+ _issue(issues, IssueCode.EMPTY_LEVEL, raw, "a sign with nothing after it; dropped")
78
+ return None
79
+ split = coded_split(body)
80
+ if split is None:
81
+ return _not_code(aspect, body, issues)
82
+ letters, digits = split
83
+ if len(digits) > 1 and digits.startswith("0"):
84
+ _issue(issues, IssueCode.LEADING_ZERO, body, "leading zero dropped")
85
+ if letters != letters.upper():
86
+ _issue(issues, IssueCode.LOWER_CASE, body, "letters raised to upper case")
87
+ return Level(aspect, letters.upper(), int(digits))
88
+
89
+
90
+ def _signed(
91
+ sign: str, body: str, levels: list[Level], aspect: Aspect, issues: list[Issue]
92
+ ) -> Aspect:
93
+ """Read one signed body onto `levels`; a dot takes the aspect of the previous level (M6)."""
94
+ if sign == ".":
95
+ _issue(issues, IssueCode.DOT_LEVEL, body, "a dot read as a level of the same aspect")
96
+ aspect = levels[-1].aspect if levels else aspect
97
+ else:
98
+ aspect = _BY_SIGN[sign]
99
+ level = _level(aspect, body, issues)
100
+ if level is not None:
101
+ levels.append(level)
102
+ return aspect
103
+
104
+
105
+ def _levels(head: str, bare: Aspect, issues: list[Issue]) -> tuple[Level, ...]:
106
+ """Split the head on signs and read each body; a dot repeats the aspect before it."""
107
+ parts = _SIGNS.split(head)
108
+ levels: list[Level] = []
109
+ aspect = bare
110
+ if _clean(parts[0]):
111
+ _issue(issues, IssueCode.NO_SIGN, parts[0], f"no sign; read as {bare.name.lower()}")
112
+ first = _level(aspect, parts[0], issues)
113
+ if first is not None:
114
+ levels.append(first)
115
+ elif parts[0]:
116
+ _issue(issues, IssueCode.WHITESPACE, parts[0], "whitespace before the first sign removed")
117
+ for sign, body in zip(parts[1::2], parts[2::2], strict=True):
118
+ aspect = _signed(sign, body, levels, aspect, issues)
119
+ return tuple(levels)
120
+
121
+
122
+ def _pins(rest: str, issues: list[Issue]) -> tuple[str, ...]:
123
+ """The pins of a terminal: split on `:`, strip each, drop the empty ones."""
124
+ pins = []
125
+ for raw in rest.split(":"):
126
+ pin = _clean(raw)
127
+ if pin != raw:
128
+ _issue(issues, IssueCode.WHITESPACE, raw, "outer whitespace of the pin removed")
129
+ if pin:
130
+ pins.append(pin)
131
+ else:
132
+ _issue(issues, IssueCode.EMPTY_PIN, raw, "an empty pin; dropped")
133
+ return tuple(pins)
134
+
135
+
136
+ def _signal(design: Designation, rest: str, issues: list[Issue]) -> Designation | Signal:
137
+ """A signal named by the rest; a bare designation if the name is empty."""
138
+ name = _clean(rest)
139
+ if name != rest:
140
+ _issue(issues, IssueCode.WHITESPACE, rest, "outer whitespace of the name removed")
141
+ if not name:
142
+ _issue(issues, IssueCode.EMPTY_NAME, rest, "an empty signal name; dropped")
143
+ return design
144
+ return Signal(design if design.levels or design.top else None, name)
145
+
146
+
147
+ def _finish(
148
+ design: Designation, sign: str, rest: str, issues: list[Issue]
149
+ ) -> Designation | Terminal | Signal:
150
+ """Attach the suffix: pins make a terminal, a name a signal, `&` is dropped with an issue."""
151
+ if sign == ":":
152
+ pins = _pins(rest, issues)
153
+ return Terminal(design, pins) if pins else design
154
+ if sign == ";":
155
+ return _signal(design, rest, issues)
156
+ if sign == "&":
157
+ _issue(issues, IssueCode.UNSUPPORTED_SUFFIX, sign + rest, "the document suffix is dropped")
158
+ return design
159
+
160
+
161
+ def parse(text: str, *, bare: Aspect) -> Parsed[Designation | Terminal | Signal]:
162
+ """Read text as a designation, terminal or signal, with an issue for each repair.
163
+
164
+ `bare` is the aspect of text before the first sign. Never raises for content.
165
+ """
166
+ if not isinstance(text, str) or not isinstance(bare, Aspect):
167
+ msg = "parse takes a str and a bare Aspect"
168
+ raise TypeError(msg)
169
+ text = _clean(text)
170
+ issues: list[Issue] = []
171
+ if not text:
172
+ _issue(issues, IssueCode.EMPTY, text, "nothing to read")
173
+ return Parsed(Designation(), tuple(issues))
174
+ head, sign, rest = _split_suffix(text)
175
+ top, head = _take_top(head, issues)
176
+ design = Designation(_levels(head, bare, issues), top)
177
+ return Parsed(_finish(design, sign, rest, issues), tuple(issues))
refdef/_render.py ADDED
@@ -0,0 +1,64 @@
1
+ """The strict printer and its three explicit options."""
2
+
3
+ from collections.abc import Collection, Mapping
4
+
5
+ from refdef.designation import Designation
6
+ from refdef.level import Aspect, Level
7
+
8
+
9
+ def _check(d: object, pad: object, dotted: object, bare: object) -> None:
10
+ if not isinstance(d, Designation):
11
+ msg = "render takes a Designation"
12
+ raise TypeError(msg)
13
+ if pad is not None and not (
14
+ isinstance(pad, Mapping)
15
+ and all(isinstance(k, str) and type(v) is int and v >= 0 for k, v in pad.items())
16
+ ):
17
+ msg = "pad must be a mapping of str to non-negative int, or None"
18
+ raise TypeError(msg)
19
+ if not isinstance(dotted, Collection) or not all(isinstance(a, Aspect) for a in dotted):
20
+ msg = "dotted must be a collection of Aspect"
21
+ raise TypeError(msg)
22
+ if not isinstance(bare, bool):
23
+ msg = "bare must be a bool"
24
+ raise TypeError(msg)
25
+
26
+
27
+ def _is_sub(level: Level, previous: Level | None) -> bool:
28
+ """A number-only level right after a level of the same aspect."""
29
+ return (
30
+ previous is not None
31
+ and previous.aspect is level.aspect
32
+ and level.code == ""
33
+ and level.number is not None
34
+ )
35
+
36
+
37
+ def _text(level: Level, pad: Mapping[str, int]) -> str:
38
+ if level.code and level.code in pad:
39
+ return level.code + str(level.number).zfill(pad[level.code])
40
+ return level.text
41
+
42
+
43
+ def render(
44
+ d: Designation,
45
+ *,
46
+ pad: Mapping[str, int] | None = None,
47
+ dotted: Collection[Aspect] = (),
48
+ bare: bool = False,
49
+ ) -> str:
50
+ """Print a designation; `pad`, `dotted` and `bare` are the explicit dialects, none is `str(d)`.
51
+
52
+ `pad={"W": 4}` gives `-W0001`; `dotted=(P,)` gives `-W25.1`; `bare=True` omits the first sign.
53
+ """
54
+ _check(d, pad, dotted, bare)
55
+ widths = pad or {}
56
+ out = [f"<{d.top}>"] if d.top else []
57
+ previous = None
58
+ for index, level in enumerate(d.levels):
59
+ sign = level.aspect.value
60
+ if _is_sub(level, previous) and level.aspect in dotted:
61
+ sign = "."
62
+ out.append(("" if bare and index == 0 else sign) + _text(level, widths))
63
+ previous = level
64
+ return "".join(out)
refdef/designation.py ADDED
@@ -0,0 +1,93 @@
1
+ """The designation: a tuple of levels and an optional top node."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from refdef import _heads
6
+ from refdef.level import Aspect, Level
7
+
8
+ _NOT_IN_TOP = frozenset(">:;&")
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class Designation:
13
+ """A tuple of levels and an optional top node, built by the constructor and `child` only.
14
+
15
+ Join two through their levels; the empty designation is the root.
16
+ """
17
+
18
+ levels: tuple[Level, ...] = ()
19
+ top: str = ""
20
+
21
+ def __post_init__(self) -> None:
22
+ """Validate the levels and the top; every violation raises TypeError."""
23
+ if not isinstance(self.levels, tuple) or not all(isinstance(v, Level) for v in self.levels):
24
+ msg = "levels must be a tuple of Level"
25
+ raise TypeError(msg)
26
+ if not isinstance(self.top, str):
27
+ msg = "top must be str"
28
+ raise TypeError(msg)
29
+ if self.top != self.top.strip() or _NOT_IN_TOP & set(self.top):
30
+ msg = "top must be stripped and hold none of > : ; &"
31
+ raise TypeError(msg)
32
+
33
+ def __str__(self) -> str:
34
+ """The strict printed form, the same as `render(self)`."""
35
+ from refdef._render import render # render imports this module, so import late
36
+
37
+ return render(self)
38
+
39
+ def child(
40
+ self, aspect: Aspect, code: str = "", number: int | None = None, label: str = ""
41
+ ) -> Designation:
42
+ """This designation with one more level at the end."""
43
+ return Designation((*self.levels, Level(aspect, code, number, label)), self.top)
44
+
45
+ def only(self, *aspects: Aspect) -> Designation:
46
+ """The levels of the listed aspects, in order; the top is kept."""
47
+ if not all(isinstance(a, Aspect) for a in aspects):
48
+ msg = "aspects must be Aspect"
49
+ raise TypeError(msg)
50
+ return Designation(_heads.only(self.levels, aspects), self.top)
51
+
52
+ def parent(self, aspect: Aspect | None = None) -> Designation:
53
+ """Without the last level of `aspect` (of any aspect if None); self if there is none."""
54
+ if aspect is not None and not isinstance(aspect, Aspect):
55
+ msg = "aspect must be an Aspect or None"
56
+ raise TypeError(msg)
57
+ return Designation(_heads.without_last(self.levels, aspect), self.top)
58
+
59
+ @property
60
+ def last(self) -> Level | None:
61
+ """The last level, or None for a designation without levels."""
62
+ return self.levels[-1] if self.levels else None
63
+
64
+ def within(self, root: Designation) -> bool:
65
+ """True when `root` is a head of this designation, aspect by aspect (the top too)."""
66
+ _need_designation(root)
67
+ return root.top in ("", self.top) and _heads.within(self.levels, root.levels)
68
+
69
+ def relative_to(self, root: Designation) -> Designation:
70
+ """This designation without the heads `root` holds; unchanged when it is not within."""
71
+ if not self.within(root):
72
+ return self
73
+ return Designation(
74
+ _heads.drop_heads(self.levels, root.levels), "" if root.top == self.top else self.top
75
+ )
76
+
77
+ @staticmethod
78
+ def common(*designations: Designation) -> Designation:
79
+ """The longest head per aspect all share, ordered as the first; the root for none."""
80
+ for each in designations:
81
+ _need_designation(each)
82
+ if not designations:
83
+ return Designation()
84
+ first, *others = designations
85
+ top = first.top if all(d.top == first.top for d in others) else ""
86
+ return Designation(_heads.common(first.levels, *(d.levels for d in others)), top)
87
+
88
+
89
+ def _need_designation(value: object) -> None:
90
+ """Raise TypeError unless `value` is a Designation."""
91
+ if not isinstance(value, Designation):
92
+ msg = "root and the arguments of common must be Designation"
93
+ raise TypeError(msg)
refdef/issues.py ADDED
@@ -0,0 +1,47 @@
1
+ """Issues found while reading or checking reference designations."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+
7
+ class IssueCode(Enum):
8
+ """The 21 issue codes: the first 16 come from the readers, the last 5 from check."""
9
+
10
+ EMPTY = "empty"
11
+ NO_SIGN = "no_sign"
12
+ EMPTY_LEVEL = "empty_level"
13
+ EMPTY_TOP = "empty_top"
14
+ WHITESPACE = "whitespace"
15
+ LOWER_CASE = "lower_case"
16
+ LEADING_ZERO = "leading_zero"
17
+ ZERO_NUMBER = "zero_number"
18
+ NUMBER_TOO_LARGE = "number_too_large"
19
+ FREE_LABEL = "free_label"
20
+ BAD_LABEL = "bad_label"
21
+ DOT_LEVEL = "dot_level"
22
+ EMPTY_PIN = "empty_pin"
23
+ EMPTY_NAME = "empty_name"
24
+ UNSUPPORTED_SUFFIX = "unsupported_suffix"
25
+ NOT_ADDRESS = "not_address"
26
+ ASPECT_OFF = "aspect_off"
27
+ LETTER_COUNT = "letter_count"
28
+ NEVER_LETTER = "never_letter"
29
+ UNKNOWN_CLASS = "unknown_class"
30
+ DUPLICATE = "duplicate"
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Issue:
35
+ """One finding about a string or a designation: a code, the offending text, a short sentence."""
36
+
37
+ code: IssueCode
38
+ subject: str
39
+ message: str
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class Parsed[T]:
44
+ """A value read from text together with the issues the reader found."""
45
+
46
+ value: T
47
+ issues: tuple[Issue, ...]