stylefix 0.7.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.
- stylefix/__init__.py +3 -0
- stylefix/abbreviations.py +200 -0
- stylefix/accept.txt +94 -0
- stylefix/adjudicate.py +27 -0
- stylefix/apply.py +3 -0
- stylefix/candidates.py +105 -0
- stylefix/changelog.py +66 -0
- stylefix/cli.py +415 -0
- stylefix/context.py +4 -0
- stylefix/conventions.py +172 -0
- stylefix/dict/README-varcon.txt +582 -0
- stylefix/dict/varcon.txt.gz +0 -0
- stylefix/dictionary.py +3 -0
- stylefix/llm.py +4 -0
- stylefix/patterns.py +217 -0
- stylefix/report.py +65 -0
- stylefix/spellcheck.py +68 -0
- stylefix/styleguides.py +153 -0
- stylefix/styleitems.py +221 -0
- stylefix/styles/AP/Numbers.yml +9 -0
- stylefix/styles/AP/Toward.yml +13 -0
- stylefix/styles/APA/Numbers.yml +9 -0
- stylefix/styles/Chicago/Numbers.yml +9 -0
- stylefix/styles/Chicago/Toward.yml +13 -0
- stylefix/styles/GPO/Numbers.yml +9 -0
- stylefix/styles/GPO/Toward.yml +13 -0
- stylefix/styles/Guardian/Numbers.yml +9 -0
- stylefix/styles/Guardian/Towards.yml +10 -0
- stylefix/styles/Oxford/Numbers.yml +9 -0
- stylefix/styles/Oxford/Towards.yml +10 -0
- stylefix/styles/ToUK/Ambiguous.yml +7 -0
- stylefix/styles/ToUK/Ise1.yml +1509 -0
- stylefix/styles/ToUK/Ise2.yml +1509 -0
- stylefix/styles/ToUK/Ise3.yml +1509 -0
- stylefix/styles/ToUK/Ise4.yml +1094 -0
- stylefix/styles/ToUK/Oxford1.yml +1509 -0
- stylefix/styles/ToUK/Oxford2.yml +1509 -0
- stylefix/styles/ToUK/Oxford3.yml +1509 -0
- stylefix/styles/ToUK/Oxford4.yml +1108 -0
- stylefix/styles/ToUK/PatEr.yml +8 -0
- stylefix/styles/ToUK/PatIse.yml +8 -0
- stylefix/styles/ToUK/PatIze.yml +8 -0
- stylefix/styles/ToUK/PatOr.yml +8 -0
- stylefix/styles/ToUK/Variants1.yml +1509 -0
- stylefix/styles/ToUK/Variants2.yml +1509 -0
- stylefix/styles/ToUK/Variants3.yml +1317 -0
- stylefix/styles/ToUS/Ambiguous.yml +7 -0
- stylefix/styles/ToUS/PatIse.yml +8 -0
- stylefix/styles/ToUS/PatLl.yml +8 -0
- stylefix/styles/ToUS/PatOur.yml +8 -0
- stylefix/styles/ToUS/PatRe.yml +8 -0
- stylefix/styles/ToUS/Variants1.yml +1509 -0
- stylefix/styles/ToUS/Variants2.yml +1509 -0
- stylefix/styles/ToUS/Variants3.yml +1509 -0
- stylefix/styles/ToUS/Variants4.yml +1509 -0
- stylefix/styles/ToUS/Variants5.yml +1509 -0
- stylefix/styles/ToUS/Variants6.yml +1509 -0
- stylefix/styles/ToUS/Variants7.yml +697 -0
- stylefix/vale_runner.py +113 -0
- stylefix/varcon.py +170 -0
- stylefix/wordlist.py +145 -0
- stylefix-0.7.0.dist-info/METADATA +290 -0
- stylefix-0.7.0.dist-info/RECORD +67 -0
- stylefix-0.7.0.dist-info/WHEEL +4 -0
- stylefix-0.7.0.dist-info/entry_points.txt +2 -0
- stylefix-0.7.0.dist-info/licenses/LICENSE +21 -0
- stylefix-0.7.0.dist-info/licenses/stylefix/dict/README-varcon.txt +582 -0
stylefix/__init__.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Abbreviation forms the style guides rule on as a class.
|
|
2
|
+
|
|
3
|
+
Two general rules, from the guides themselves:
|
|
4
|
+
|
|
5
|
+
- Chicago 10.4 (and Oxford, Guardian): an abbreviation in full capitals
|
|
6
|
+
takes no periods, whether two letters or more (US, UK, EU, NATO, PhD,
|
|
7
|
+
MA, DC). Applied generally: every period-group of capitals is closed up.
|
|
8
|
+
The one hazard is personal initials ("J.K. Rowling"), which look the
|
|
9
|
+
same; a group followed by a capitalised word is therefore adjudicated,
|
|
10
|
+
as is the sentence-end case ("in the U.S. The ...").
|
|
11
|
+
- AP "abbreviations and acronyms" (and GPO): periods in most two-letter
|
|
12
|
+
abbreviations (U.S., U.N., U.K., B.A., B.C.), with named exceptions (AP,
|
|
13
|
+
GI, ID, EU); none in longer ones (FBI, NATO, USA). AP applies "most" to
|
|
14
|
+
countries, places, degrees and eras, not to AI, IT, PR or TV, so the keep
|
|
15
|
+
rule is applied by category. APA keeps U.S. but writes degrees without
|
|
16
|
+
periods (PhD).
|
|
17
|
+
|
|
18
|
+
Also: clock times (10 a.m. / 10am), vs., and the Latin tags e.g., i.e.,
|
|
19
|
+
etc. And the noun rule of GPO and APA: "U.S." only as an adjective; the
|
|
20
|
+
noun is "the United States". That one is a warning by default and an
|
|
21
|
+
enforcement on request (--abbreviation-nouns enforce), decided per
|
|
22
|
+
occurrence because it is a part-of-speech question."""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
|
|
29
|
+
from adjudicate import Edit, Item, prose_regions, sentence_context
|
|
30
|
+
|
|
31
|
+
# Period-groups of capitals, allowing a lowercase letter after a capital
|
|
32
|
+
# (Ph.D., B.Sc., D.Phil.), two or more parts.
|
|
33
|
+
_GROUP = re.compile(r"(?<![A-Za-z.])(?:[A-Z][a-z]?\.\s?){2,}(?<=\.)(?![A-Za-z])")
|
|
34
|
+
# Categories AP and GPO give periods to.
|
|
35
|
+
COUNTRIES = {"US": "U.S.", "UK": "U.K.", "UN": "U.N.", "DC": "D.C."}
|
|
36
|
+
DEGREES = {"BA": "B.A.", "MA": "M.A.", "PhD": "Ph.D.", "MD": "M.D.", "JD": "J.D.", "BS": "B.S.",
|
|
37
|
+
"MS": "M.S.", "BSc": "B.Sc.", "MSc": "M.Sc.", "LLB": "LL.B.", "LLM": "LL.M.",
|
|
38
|
+
"DPhil": "D.Phil."}
|
|
39
|
+
ERAS = {"BC": "B.C.", "AD": "A.D."}
|
|
40
|
+
KEEP_EXCEPTIONS = {"EU", "AP", "GI", "ID", "USA", "OK"} # AP's named exceptions
|
|
41
|
+
KNOWN = set(COUNTRIES) | set(DEGREES) | set(ERAS) | KEEP_EXCEPTIONS | {
|
|
42
|
+
"NY", "LA", "UAE", "USSR", "NATO", "FBI", "CIA", "NASA", "CEO", "VP", "CE", "BCE", "NYC"}
|
|
43
|
+
|
|
44
|
+
_TIME = re.compile(r"(?<=\d)\s?([aApP])\.?[mM]\.?(?![A-Za-z])")
|
|
45
|
+
_VS = re.compile(r"\bvs\.?(?=[\s,;:)])")
|
|
46
|
+
_EG = re.compile(r"\b[eE]\.?[gG]\.(?=[\s,;:)])|\beg(?=[\s,])")
|
|
47
|
+
_IE = re.compile(r"\b[iI]\.?[eE]\.(?=[\s,;:)])|\bie(?=[\s,])")
|
|
48
|
+
_ETC = re.compile(r"\betc\.?(?![A-Za-z])")
|
|
49
|
+
_NOUN_US = re.compile(r"\b[Tt]he (U\.S\.|US)(?![A-Za-z])")
|
|
50
|
+
# "the US" followed by something that cannot be a modified noun: a verb,
|
|
51
|
+
# a preposition, a conjunction, or punctuation. Used for the warning count.
|
|
52
|
+
_NOUN_TAIL = re.compile(r"\s+(?:and|or|is|has|have|had|will|would|should|could|can|must|may|might|"
|
|
53
|
+
r"to|in|on|at|by|for|with|from|as|than|that|which|who|does|did|do|was|were|"
|
|
54
|
+
r"are|be|also|still|now|then|already|itself|alone|too)\b|[.,;:!?)]")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class Abbreviations:
|
|
59
|
+
caps: str | None # "drop" (Chicago rule) | "keep" (AP/GPO categories) | None
|
|
60
|
+
degrees_periods: bool | None # under keep: Ph.D. (True) or PhD (False)
|
|
61
|
+
time: str | None # "a.m." | "am"
|
|
62
|
+
vs: str | None # "vs." | "vs" | "v"
|
|
63
|
+
latin_periods: bool | None
|
|
64
|
+
noun_rule: bool = False # guide says "U.S." is an adjective only
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
PROFILES = {
|
|
68
|
+
"chicago": Abbreviations("drop", None, "a.m.", "vs.", True),
|
|
69
|
+
"gpo": Abbreviations("keep", True, "a.m.", "vs.", True, noun_rule=True),
|
|
70
|
+
"ap": Abbreviations("keep", True, "a.m.", "vs.", True),
|
|
71
|
+
"apa": Abbreviations("keep", False, "a.m.", "vs.", True, noun_rule=True),
|
|
72
|
+
"oxford": Abbreviations("drop", None, "a.m.", "vs", True),
|
|
73
|
+
"guardian": Abbreviations("drop", None, "am", "v", False),
|
|
74
|
+
"none": Abbreviations(None, None, None, None, None),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
DROP_RULE = ("The style guide writes abbreviations in capitals without periods. This one is followed "
|
|
78
|
+
"by a capitalised word, so it is one of: the last word of its sentence, part of a name "
|
|
79
|
+
"or title ('U.S. Treasury'), or a person's initials ('J.K. Rowling'). The readings:\n"
|
|
80
|
+
" A (sentence ends here): {a}\n B (name or title continues): {b}\n"
|
|
81
|
+
"Choose '{end}' for A, '{bare}' for B. KEEP if these are a person's initials, which keep "
|
|
82
|
+
"their periods.")
|
|
83
|
+
NOUN_RULE = ("{guide} uses '{abbr}' only as an adjective ('{abbr} policy', '{abbr} exports') and writes "
|
|
84
|
+
"the noun out: 'the United States'. Here it follows 'the'. Choose 'United States' if the "
|
|
85
|
+
"abbreviation stands alone as a noun (subject or object: 'the US has', 'in the US', "
|
|
86
|
+
"'the US and its allies'). Choose '{abbr}' if it modifies a following noun ('the US "
|
|
87
|
+
"economy'). KEEP only if neither.")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _line_col(text: str, pos: int) -> tuple[int, int]:
|
|
91
|
+
return text.count("\n", 0, pos) + 1, pos - (text.rfind("\n", 0, pos) + 1) + 1
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _edit(text: str, start: int, end: int, replacement: str) -> Edit:
|
|
95
|
+
line, col = _line_col(text, start)
|
|
96
|
+
return Edit(line, col, col + (end - start) - 1, text[start:end], replacement, "abbreviations")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _item(text: str, s: int, e: int, options: tuple[str, ...], rule: str, next_id: int) -> Item:
|
|
100
|
+
line, col = _line_col(text, s)
|
|
101
|
+
return Item(next_id, text[s:e], options, rule, sentence_context(text, s, e),
|
|
102
|
+
line, col, col + (e - s) - 1, source="abbreviation")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def bare(group: str) -> str:
|
|
106
|
+
return group.replace(".", "").replace(" ", "")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def plan(text: str, guide: str, nouns: str = "default", next_id: int = 1
|
|
110
|
+
) -> tuple[list[Edit], list[Item], list[tuple[int, str]]]:
|
|
111
|
+
"""Deterministic edits, adjudication items, and noun-rule warnings
|
|
112
|
+
(line, phrase). `nouns`: default (warn if the guide has the rule),
|
|
113
|
+
warn, enforce, off."""
|
|
114
|
+
prof = PROFILES.get(guide, PROFILES["none"])
|
|
115
|
+
noun_mode = nouns if nouns != "default" else ("warn" if prof.noun_rule else "off")
|
|
116
|
+
edits: list[Edit] = []
|
|
117
|
+
items: list[Item] = []
|
|
118
|
+
warnings: list[tuple[int, str]] = []
|
|
119
|
+
claimed: set[int] = set() # start offsets already handled by a noun item
|
|
120
|
+
|
|
121
|
+
for rs, re_ in prose_regions(text):
|
|
122
|
+
region = text[rs:re_]
|
|
123
|
+
|
|
124
|
+
# --- the noun rule: "the U.S." / "the US"
|
|
125
|
+
if prof.noun_rule and noun_mode != "off":
|
|
126
|
+
for m in _NOUN_US.finditer(region):
|
|
127
|
+
s, e = rs + m.start(1), rs + m.end(1)
|
|
128
|
+
tail = region[m.end():m.end() + 12]
|
|
129
|
+
if noun_mode == "enforce":
|
|
130
|
+
canonical = COUNTRIES["US"] if prof.caps == "keep" else "US"
|
|
131
|
+
rule = NOUN_RULE.format(guide={"gpo": "GPO", "apa": "APA"}.get(guide, "The guide"),
|
|
132
|
+
abbr=canonical)
|
|
133
|
+
items.append(_item(text, s, e, ("United States", canonical, "KEEP"), rule, next_id))
|
|
134
|
+
next_id += 1
|
|
135
|
+
claimed.add(s)
|
|
136
|
+
elif _NOUN_TAIL.match(tail):
|
|
137
|
+
warnings.append((_line_col(text, s)[0], text[s - 4:e + min(12, len(tail))].strip()))
|
|
138
|
+
|
|
139
|
+
# --- capitals: drop (general) or keep (by category)
|
|
140
|
+
if prof.caps == "drop":
|
|
141
|
+
for m in _GROUP.finditer(region):
|
|
142
|
+
s, e = rs + m.start(), rs + m.end()
|
|
143
|
+
if s in claimed:
|
|
144
|
+
continue
|
|
145
|
+
group = m.group(0)
|
|
146
|
+
canonical = bare(group)
|
|
147
|
+
following = region[m.end():m.end() + 3]
|
|
148
|
+
if re.match(r"\s*$", following) or following.startswith("\n"):
|
|
149
|
+
edits.append(_edit(text, s, e, canonical + ".")) # paragraph end
|
|
150
|
+
elif re.match(r"\s+[A-Z“\"(]", following):
|
|
151
|
+
lo, hi = max(rs, s - 45), min(re_, e + 45)
|
|
152
|
+
before, after = text[lo:s], text[e:hi]
|
|
153
|
+
rule = DROP_RULE.format(a="…" + before + canonical + "." + after + "…",
|
|
154
|
+
b="…" + before + canonical + after + "…",
|
|
155
|
+
end=canonical + ".", bare=canonical)
|
|
156
|
+
items.append(_item(text, s, e, (canonical + ".", canonical, "KEEP"), rule, next_id))
|
|
157
|
+
next_id += 1
|
|
158
|
+
else:
|
|
159
|
+
edits.append(_edit(text, s, e, canonical))
|
|
160
|
+
elif prof.caps == "keep":
|
|
161
|
+
table = dict(COUNTRIES) | dict(ERAS)
|
|
162
|
+
if prof.degrees_periods:
|
|
163
|
+
table |= DEGREES
|
|
164
|
+
else:
|
|
165
|
+
# APA: degrees without periods; U.S. with.
|
|
166
|
+
for k, v in DEGREES.items():
|
|
167
|
+
table[v] = k
|
|
168
|
+
alts = "|".join(re.escape(k) for k in sorted(table, key=len, reverse=True))
|
|
169
|
+
for m in re.finditer(rf"(?<![A-Za-z.])(?:{alts})(?![A-Za-z])", region):
|
|
170
|
+
s, e = rs + m.start(), rs + m.end()
|
|
171
|
+
if s in claimed:
|
|
172
|
+
continue
|
|
173
|
+
want = table[m.group(0)]
|
|
174
|
+
if want.endswith(".") and text[e:e + 1] == "." and text[e:e + 2] != "..":
|
|
175
|
+
e += 1 # absorb the sentence period
|
|
176
|
+
edits.append(_edit(text, s, e, want))
|
|
177
|
+
|
|
178
|
+
# --- times
|
|
179
|
+
if prof.time:
|
|
180
|
+
for m in _TIME.finditer(region):
|
|
181
|
+
letter = m.group(1).lower()
|
|
182
|
+
want = (" " + letter + ".m.") if prof.time == "a.m." else (letter + "m")
|
|
183
|
+
if m.group(0) != want:
|
|
184
|
+
edits.append(_edit(text, rs + m.start(), rs + m.end(), want))
|
|
185
|
+
# --- vs
|
|
186
|
+
if prof.vs:
|
|
187
|
+
for m in _VS.finditer(region):
|
|
188
|
+
if m.group(0) != prof.vs:
|
|
189
|
+
edits.append(_edit(text, rs + m.start(), rs + m.end(), prof.vs))
|
|
190
|
+
# --- e.g., i.e., etc.
|
|
191
|
+
if prof.latin_periods is not None:
|
|
192
|
+
for rx, with_, without in ((_EG, "e.g.", "eg"), (_IE, "i.e.", "ie"), (_ETC, "etc.", "etc")):
|
|
193
|
+
want = with_ if prof.latin_periods else without
|
|
194
|
+
for m in rx.finditer(region):
|
|
195
|
+
if m.group(0) != want:
|
|
196
|
+
e = rs + m.end()
|
|
197
|
+
if want.endswith(".") and text[e:e + 1] == "." and text[e:e + 2] != "..":
|
|
198
|
+
e += 1
|
|
199
|
+
edits.append(_edit(text, rs + m.start(), e, want))
|
|
200
|
+
return edits, items, warnings
|
stylefix/accept.txt
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Terms that Vale's bundled dictionary lacks but that are correct in both
|
|
2
|
+
# US and UK English. Technical, policy and internet vocabulary. One per
|
|
3
|
+
# line; case-insensitive; Vale treats each as a regex, so keep it simple.
|
|
4
|
+
# Users add their own in ~/.config/stylefix/accept.txt and ./.stylefix/accept.txt.
|
|
5
|
+
cybersecurity
|
|
6
|
+
cyber
|
|
7
|
+
cyberattack
|
|
8
|
+
cyberattacks
|
|
9
|
+
cybercrime
|
|
10
|
+
cyberspace
|
|
11
|
+
sandboxing
|
|
12
|
+
sandboxed
|
|
13
|
+
sandbox
|
|
14
|
+
auditable
|
|
15
|
+
auditability
|
|
16
|
+
longterm
|
|
17
|
+
interoperability
|
|
18
|
+
interoperable
|
|
19
|
+
scalable
|
|
20
|
+
scalability
|
|
21
|
+
misinformation
|
|
22
|
+
disinformation
|
|
23
|
+
deepfake
|
|
24
|
+
deepfakes
|
|
25
|
+
chatbot
|
|
26
|
+
chatbots
|
|
27
|
+
blockchain
|
|
28
|
+
cryptocurrency
|
|
29
|
+
cryptocurrencies
|
|
30
|
+
malware
|
|
31
|
+
ransomware
|
|
32
|
+
spyware
|
|
33
|
+
phishing
|
|
34
|
+
botnet
|
|
35
|
+
botnets
|
|
36
|
+
exfiltrate
|
|
37
|
+
exfiltration
|
|
38
|
+
containerization
|
|
39
|
+
containerized
|
|
40
|
+
virtualization
|
|
41
|
+
hyperscaler
|
|
42
|
+
hyperscalers
|
|
43
|
+
preprint
|
|
44
|
+
preprints
|
|
45
|
+
misalignment
|
|
46
|
+
misaligned
|
|
47
|
+
superintelligence
|
|
48
|
+
superintelligent
|
|
49
|
+
multimodal
|
|
50
|
+
transformer
|
|
51
|
+
transformers
|
|
52
|
+
finetune
|
|
53
|
+
finetuned
|
|
54
|
+
finetuning
|
|
55
|
+
pretraining
|
|
56
|
+
pretrained
|
|
57
|
+
inference
|
|
58
|
+
agentic
|
|
59
|
+
autonomously
|
|
60
|
+
onboarding
|
|
61
|
+
offboarding
|
|
62
|
+
roadmap
|
|
63
|
+
roadmaps
|
|
64
|
+
workstream
|
|
65
|
+
workstreams
|
|
66
|
+
stakeholder
|
|
67
|
+
stakeholders
|
|
68
|
+
geopolitical
|
|
69
|
+
geopolitics
|
|
70
|
+
geo-strategic
|
|
71
|
+
whistleblower
|
|
72
|
+
whistleblowers
|
|
73
|
+
open-source
|
|
74
|
+
open-sourced
|
|
75
|
+
opensource
|
|
76
|
+
frontier
|
|
77
|
+
biosecurity
|
|
78
|
+
bioweapon
|
|
79
|
+
bioweapons
|
|
80
|
+
dual-use
|
|
81
|
+
zero-day
|
|
82
|
+
API
|
|
83
|
+
APIs
|
|
84
|
+
GPU
|
|
85
|
+
GPUs
|
|
86
|
+
TPU
|
|
87
|
+
TPUs
|
|
88
|
+
LLM
|
|
89
|
+
LLMs
|
|
90
|
+
AGI
|
|
91
|
+
NIST
|
|
92
|
+
CISA
|
|
93
|
+
OSTP
|
|
94
|
+
DARPA
|
stylefix/adjudicate.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""stylefix's adjudication task on top of the adjudicate library."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from adjudicate.core import (Cache, Decision, Item, decide_batch, parse_response, # noqa: F401
|
|
6
|
+
render_batch, system_prompt)
|
|
7
|
+
from adjudicate.core import adjudicate as _adjudicate
|
|
8
|
+
from adjudicate.llm import Endpoint
|
|
9
|
+
|
|
10
|
+
VARIETY = {"us": "American English", "uk": "British English"}
|
|
11
|
+
|
|
12
|
+
TASK = "You are a meticulous copy editor regularizing a document to {variety}."
|
|
13
|
+
KEEP_RULE = ("Choose KEEP when the marked text is part of a proper name, an organisation name, "
|
|
14
|
+
"the title of a published work, a direct quotation from a source written in another "
|
|
15
|
+
"variety, a URL or code identifier, or is already correct for {variety} in this "
|
|
16
|
+
"context. Otherwise choose the option that is correct for {variety}.")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def system_for(target: str) -> str:
|
|
20
|
+
v = VARIETY[target]
|
|
21
|
+
return system_prompt(TASK.format(variety=v), KEEP_RULE.format(variety=v))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def adjudicate(ep: Endpoint | None, target: str, items: list[Item], cache: Cache,
|
|
25
|
+
batch_size: int = 20, parallel: int = 4, verbose: bool = False) -> list[Decision]:
|
|
26
|
+
return _adjudicate(ep, target, items, cache, batch_size, parallel, verbose,
|
|
27
|
+
system=system_for(target), log_name="stylefix")
|
stylefix/apply.py
ADDED
stylefix/candidates.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Ambiguous words: the options the adjudicator may choose between."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Candidate:
|
|
10
|
+
options: tuple[str, ...] # first option is the variety-specific form
|
|
11
|
+
rule: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _pair(uk_or_us: str, same: str, rule: str, plural: str | None = None,
|
|
15
|
+
plural_same: str | None = None) -> dict[str, Candidate]:
|
|
16
|
+
out = {same: Candidate((uk_or_us, same, "KEEP"), rule)}
|
|
17
|
+
if plural:
|
|
18
|
+
out[plural_same or same + "s"] = Candidate((plural, plural_same or same + "s", "KEEP"), rule)
|
|
19
|
+
return out
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
TO_UK: dict[str, Candidate] = {}
|
|
23
|
+
TO_UK.update(_pair("licence", "license",
|
|
24
|
+
"British: 'licence' is the noun (a driving licence); 'license' is the verb (to license software).",
|
|
25
|
+
"licences"))
|
|
26
|
+
TO_UK.update(_pair("practise", "practice",
|
|
27
|
+
"British: 'practice' is the noun (medical practice, in practice); 'practise' is the verb (to practise the piano).",
|
|
28
|
+
"practises"))
|
|
29
|
+
TO_UK.update(_pair("programme", "program",
|
|
30
|
+
"British: 'programme' for broadcasts, events, plans and schedules; 'program' only for computer software.",
|
|
31
|
+
"programmes"))
|
|
32
|
+
TO_UK.update(_pair("cheque", "check",
|
|
33
|
+
"British: 'cheque' only for the bank instrument; 'check' for verifying, restraining, chess, patterns.",
|
|
34
|
+
"cheques"))
|
|
35
|
+
TO_UK.update(_pair("storey", "story",
|
|
36
|
+
"British: 'storey' is a floor of a building; 'story' is a narrative.",
|
|
37
|
+
"storeys", "stories"))
|
|
38
|
+
TO_UK.update(_pair("tyre", "tire",
|
|
39
|
+
"British: 'tyre' is the rubber ring on a wheel; 'tire' is the verb (to grow weary).",
|
|
40
|
+
"tyres"))
|
|
41
|
+
TO_UK.update(_pair("metre", "meter",
|
|
42
|
+
"British: 'metre' is the unit of length (and poetic metre); 'meter' is a measuring device (gas meter).",
|
|
43
|
+
"metres"))
|
|
44
|
+
TO_UK.update(_pair("kerb", "curb",
|
|
45
|
+
"British: 'kerb' is the edge of a pavement; 'curb' is the verb (to restrain).",
|
|
46
|
+
"kerbs"))
|
|
47
|
+
TO_UK.update(_pair("enquiry", "inquiry",
|
|
48
|
+
"British: 'enquiry' is an ordinary question or request; 'inquiry' is a formal investigation.",
|
|
49
|
+
"enquiries", "inquiries"))
|
|
50
|
+
TO_UK.update(_pair("draught", "draft",
|
|
51
|
+
"British: 'draught' for air currents, beer, ships' depth, draught animals, and the game; 'draft' for preliminary versions, banking, and conscription.",
|
|
52
|
+
"draughts"))
|
|
53
|
+
TO_UK.update(_pair("judgement", "judgment",
|
|
54
|
+
"British: 'judgement' in general use; 'judgment' for a court's formal decision.",
|
|
55
|
+
"judgements"))
|
|
56
|
+
TO_UK.update(_pair("learnt", "learned",
|
|
57
|
+
"British: 'learnt' as the past tense/participle of learn; 'learned' (two syllables) as the adjective meaning scholarly."))
|
|
58
|
+
TO_UK.update(_pair("disc", "disk",
|
|
59
|
+
"British: 'disc' for optical discs, brake discs, anatomy and flat round objects; 'disk' for magnetic computer disks.",
|
|
60
|
+
"discs"))
|
|
61
|
+
TO_UK.update(_pair("tonne", "ton",
|
|
62
|
+
"British: 'tonne' is the metric unit (1000 kg); 'ton' is the imperial unit or figurative ('tons of').",
|
|
63
|
+
"tonnes"))
|
|
64
|
+
TO_UK.update(_pair("whisky", "whiskey",
|
|
65
|
+
"British: 'whisky' for Scotch and Canadian; 'whiskey' for Irish and American.",
|
|
66
|
+
"whiskies", "whiskeys"))
|
|
67
|
+
TO_UK.update(_pair("mum", "mom",
|
|
68
|
+
"British: 'mum' for mother; keep 'mom' only inside a quotation or a name.",
|
|
69
|
+
"mums"))
|
|
70
|
+
|
|
71
|
+
TO_US: dict[str, Candidate] = {}
|
|
72
|
+
TO_US.update(_pair("disk", "disc",
|
|
73
|
+
"American: 'disk' for computer disks and general use; 'disc' for optical discs (compact disc), brake discs and anatomy.",
|
|
74
|
+
"disks"))
|
|
75
|
+
TO_US.update(_pair("metric ton", "tonne",
|
|
76
|
+
"American: 'metric ton' for 1000 kg; keep 'tonne' only inside a quotation or a proper name.",
|
|
77
|
+
"metric tons"))
|
|
78
|
+
TO_US.update(_pair("whiskey", "whisky",
|
|
79
|
+
"American: 'whiskey' in general; 'whisky' stays for Scotch and Canadian.",
|
|
80
|
+
"whiskeys", "whiskys"))
|
|
81
|
+
TO_US.update(_pair("mom", "mum",
|
|
82
|
+
"American: 'mom' for mother; keep 'mum' when it means silent ('keep mum') or the flower.",
|
|
83
|
+
"moms"))
|
|
84
|
+
|
|
85
|
+
TABLES = {"uk": TO_UK, "us": TO_US}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
VARIETY_NAME = {"us": "American", "uk": "British", "oxford": "British (Oxford)"}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def lookup(target: str, word: str, oxford: bool = False) -> Candidate | None:
|
|
92
|
+
"""Hand-written entry first; otherwise one built from VarCon's glosses."""
|
|
93
|
+
hit = TABLES[target].get(word.lower())
|
|
94
|
+
if hit:
|
|
95
|
+
return hit
|
|
96
|
+
from . import varcon
|
|
97
|
+
key = "oxford" if (target == "uk" and oxford) else target
|
|
98
|
+
m = varcon.maps(key)
|
|
99
|
+
forms = m.ambiguous.get(word.lower())
|
|
100
|
+
if not forms:
|
|
101
|
+
return None
|
|
102
|
+
rule = m.rule(word.lower(), VARIETY_NAME[key]) or (
|
|
103
|
+
f"{VARIETY_NAME[key]}: both spellings exist with different meanings; choose the one "
|
|
104
|
+
"that fits the sentence.")
|
|
105
|
+
return Candidate(tuple(sorted(forms, key=lambda f: (f == word.lower(), f))) + ("KEEP",), rule)
|
stylefix/changelog.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Plain-text change log: one line per edit, for --changes and for callers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
from .adjudicate import Decision
|
|
8
|
+
from .apply import Edit, match_case
|
|
9
|
+
from .conventions import Stats
|
|
10
|
+
from .vale_runner import Alert
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _context(text: str, edit: Edit, width: int = 28) -> str:
|
|
14
|
+
lines = text.split("\n")
|
|
15
|
+
line = lines[edit.line - 1] if edit.line - 1 < len(lines) else ""
|
|
16
|
+
s, e = edit.start - 1, edit.end
|
|
17
|
+
left = line[max(0, s - width):s]
|
|
18
|
+
right = line[e:e + width]
|
|
19
|
+
return f"{'…' if s > width else ''}{left}[{line[s:e]}]{right}{'…' if len(line) > e + width else ''}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def render(*, text: str, target: str, applied: list[Edit], decisions: list[Decision],
|
|
23
|
+
stats: Stats, skipped: list[Edit] | None = None,
|
|
24
|
+
review: list[Alert] | None = None) -> str:
|
|
25
|
+
"""Every change, grouped: word edits in document order, then punctuation
|
|
26
|
+
counts, then adjudications that were kept or left undecided."""
|
|
27
|
+
by_pos = {(d.item.line, d.item.start): d for d in decisions}
|
|
28
|
+
out = [f"stylefix changes (target {target.upper()}): {len(applied)} word edit(s)"]
|
|
29
|
+
for e in sorted(applied, key=lambda e: (e.line, e.start)):
|
|
30
|
+
d = by_pos.get((e.line, e.start))
|
|
31
|
+
how = "auto"
|
|
32
|
+
if e.source == "adjudicated" and d:
|
|
33
|
+
how = f"model {d.confidence:.2f}: {d.reason}"
|
|
34
|
+
if d.item.source == "style":
|
|
35
|
+
how = "style, " + how
|
|
36
|
+
elif e.source == "adjudicated":
|
|
37
|
+
how = "model"
|
|
38
|
+
shown = match_case(e.original, e.replacement).strip() or repr(e.replacement)
|
|
39
|
+
out.append(f" L{e.line:<4} {e.original.strip()} -> {shown:<14} {how}")
|
|
40
|
+
out.append(f" {_context(text, e)}")
|
|
41
|
+
kept = [d for d in decisions if d.choice == "KEEP"
|
|
42
|
+
or (d.choice and d.choice.lower() == d.item.word.lower())]
|
|
43
|
+
if kept:
|
|
44
|
+
out.append(f"kept after adjudication: {len(kept)}")
|
|
45
|
+
for d in kept:
|
|
46
|
+
out.append(f" L{d.item.line:<4} {d.item.word:<20} {d.confidence:.2f}: {d.reason}")
|
|
47
|
+
undecided = [d for d in decisions if d.choice is None]
|
|
48
|
+
if undecided:
|
|
49
|
+
out.append(f"undecided (left as is, see report): {len(undecided)}")
|
|
50
|
+
for d in undecided:
|
|
51
|
+
out.append(f" L{d.item.line:<4} {d.item.word:<20} {d.reason}")
|
|
52
|
+
if skipped:
|
|
53
|
+
out.append(f"skipped (text moved): {len(skipped)}")
|
|
54
|
+
punct = stats.as_dict()
|
|
55
|
+
if punct:
|
|
56
|
+
out.append("punctuation: " + ", ".join(f"{k.replace('_', ' ')} {v}" for k, v in punct.items()))
|
|
57
|
+
if review:
|
|
58
|
+
out.append(f"for review (not changed): {len(review)}")
|
|
59
|
+
by_check: dict[str, list[Alert]] = defaultdict(list)
|
|
60
|
+
for a in review:
|
|
61
|
+
by_check[a.check].append(a)
|
|
62
|
+
for check, items in sorted(by_check.items()):
|
|
63
|
+
out.append(f" {check}")
|
|
64
|
+
for a in items:
|
|
65
|
+
out.append(f" L{a.line:<4} {a.message}")
|
|
66
|
+
return "\n".join(out) + "\n"
|