Skip to content

AutoCorrect2 integration

integration

Manage custom hotstring includes in AutoCorrect2's autocorrection context.

AutoCorrect2IntegrationError

Bases: RuntimeError

Raised when AutoCorrect2's include structure is inconsistent or ambiguous.

Source code in hotstring\autocorrect2\integration.py
37
38
class AutoCorrect2IntegrationError(RuntimeError):
    """Raised when AutoCorrect2's include structure is inconsistent or ambiguous."""

_IncludeState dataclass

Store validated source state for one custom include.

Attributes:

Name Type Description
source_path Path

AutoCorrect2 main script being inspected.

lines list[str]

Main-script source lines including newline terminators.

hotif_start int

Index of the target #HotIf directive.

hotif_end int

Index of the next #HotIf directive terminating the context.

custom_include_index int | None

Existing custom include line, or None when absent.

Source code in hotstring\autocorrect2\integration.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@dataclass(frozen=True, slots=True)
class _IncludeState:
    """Store validated source state for one custom include.

    Attributes:
        source_path:
            AutoCorrect2 main script being inspected.
        lines:
            Main-script source lines including newline terminators.
        hotif_start:
            Index of the target `#HotIf` directive.
        hotif_end:
            Index of the next `#HotIf` directive terminating the context.
        custom_include_index:
            Existing custom include line, or `None` when absent.
    """

    source_path: Path
    lines: list[str]
    hotif_start: int
    hotif_end: int
    custom_include_index: int | None

has_custom_hotstring_include

has_custom_hotstring_include(
    custom_file_path: str | Path, *, project_dir: str | Path
) -> bool

Return whether a custom file is included in AutoCorrect2's correction context.

Parameters:

Name Type Description Default
custom_file_path str | Path

Custom file to look for. A relative path is interpreted relative to the AutoCorrect2 project directory.

required
project_dir str | Path

AutoCorrect2 project directory.

required

Returns:

Type Description
bool

True when exactly one include exists inside the expected

bool

#HotIf AutoCorrectionsActivelyRunning() context; otherwise False

bool

when the include is completely absent.

Raises:

Type Description
FileNotFoundError

If AutoCorrect2's main source file does not exist.

ValueError

If the custom path refers to a protected integration source.

AutoCorrect2IntegrationError

If the expected include/context structure is ambiguous.

OSError

If AutoCorrect2's main source file cannot be read.

Source code in hotstring\autocorrect2\integration.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def has_custom_hotstring_include(
    custom_file_path: str | Path,
    *,
    project_dir: str | Path,
) -> bool:
    """Return whether a custom file is included in AutoCorrect2's correction context.

    Args:
        custom_file_path:
            Custom file to look for. A relative path is interpreted relative
            to the AutoCorrect2 project directory.
        project_dir:
            AutoCorrect2 project directory.

    Returns:
        `True` when exactly one include exists inside the expected
        `#HotIf AutoCorrectionsActivelyRunning()` context; otherwise `False`
        when the include is completely absent.

    Raises:
        FileNotFoundError:
            If AutoCorrect2's main source file does not exist.
        ValueError:
            If the custom path refers to a protected integration source.
        AutoCorrect2IntegrationError:
            If the expected include/context structure is ambiguous.
        OSError:
            If AutoCorrect2's main source file cannot be read.
    """
    return (
        _inspect_include_state(Path(custom_file_path), Path(project_dir)).custom_include_index
        is not None
    )

add_custom_hotstring_include

add_custom_hotstring_include(
    custom_file_path: str | Path, *, project_dir: str | Path
) -> bool

Add a custom file to AutoCorrect2's autocorrection context if absent.

Parameters:

Name Type Description Default
custom_file_path str | Path

Custom file to include. A relative path is interpreted relative to the AutoCorrect2 project directory.

required
project_dir str | Path

AutoCorrect2 project directory.

required

Returns:

Type Description
bool

True if the source file was modified, or False if the include was

bool

already correctly present.

Raises:

Type Description
FileNotFoundError

If AutoCorrect2's main source file does not exist.

ValueError

If the path is invalid or cannot be represented safely.

AutoCorrect2IntegrationError

If the expected include/context structure is ambiguous.

OSError

If the source file cannot be read or written.

Source code in hotstring\autocorrect2\integration.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def add_custom_hotstring_include(
    custom_file_path: str | Path,
    *,
    project_dir: str | Path,
) -> bool:
    """Add a custom file to AutoCorrect2's autocorrection context if absent.

    Args:
        custom_file_path:
            Custom file to include. A relative path is interpreted relative to
            the AutoCorrect2 project directory.
        project_dir:
            AutoCorrect2 project directory.

    Returns:
        `True` if the source file was modified, or `False` if the include was
        already correctly present.

    Raises:
        FileNotFoundError:
            If AutoCorrect2's main source file does not exist.
        ValueError:
            If the path is invalid or cannot be represented safely.
        AutoCorrect2IntegrationError:
            If the expected include/context structure is ambiguous.
        OSError:
            If the source file cannot be read or written.
    """
    custom_file_path = Path(custom_file_path)
    project_dir = Path(project_dir)
    state = _inspect_include_state(custom_file_path, project_dir)
    if state.custom_include_index is not None:
        return False

    custom_path = _resolve_custom_file_path(custom_file_path, project_dir)
    include_path = _render_include_path(custom_path, state.source_path.parent)
    indentation = _hotif_indentation(state)
    newline = _detect_newline(state.lines)
    comment = (
        "; Library of custom hotstrings for auto-correction generated by the "
        "python package 'autocorrect2-hotstring-generation'."
    )
    include_line = f'{indentation}#Include "{include_path}" {comment}{newline}'

    lines = state.lines.copy()
    lines.insert(state.hotif_end, include_line)
    write_text(
        state.source_path,
        "".join(lines),
        encoding="utf-8-sig",
        create_parents=False,
    )
    return True

remove_custom_hotstring_include

remove_custom_hotstring_include(
    custom_file_path: str | Path, *, project_dir: str | Path
) -> bool

Remove a custom file from AutoCorrect2's autocorrection context if present.

Parameters:

Name Type Description Default
custom_file_path str | Path

Custom file whose include should be removed.

required
project_dir str | Path

AutoCorrect2 project directory.

required

Returns:

Type Description
bool

True if the source file was modified, or False if the include was

bool

already absent.

Raises:

Type Description
FileNotFoundError

If AutoCorrect2's main source file does not exist.

ValueError

If the custom path refers to a protected integration source.

AutoCorrect2IntegrationError

If the expected include/context structure is ambiguous.

OSError

If the source file cannot be read or written.

Source code in hotstring\autocorrect2\integration.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def remove_custom_hotstring_include(
    custom_file_path: str | Path,
    *,
    project_dir: str | Path,
) -> bool:
    """Remove a custom file from AutoCorrect2's autocorrection context if present.

    Args:
        custom_file_path:
            Custom file whose include should be removed.
        project_dir:
            AutoCorrect2 project directory.

    Returns:
        `True` if the source file was modified, or `False` if the include was
        already absent.

    Raises:
        FileNotFoundError:
            If AutoCorrect2's main source file does not exist.
        ValueError:
            If the custom path refers to a protected integration source.
        AutoCorrect2IntegrationError:
            If the expected include/context structure is ambiguous.
        OSError:
            If the source file cannot be read or written.
    """
    custom_file_path = Path(custom_file_path)
    project_dir = Path(project_dir)
    state = _inspect_include_state(custom_file_path, project_dir)
    if state.custom_include_index is None:
        return False

    lines = state.lines.copy()
    del lines[state.custom_include_index]
    write_text(
        state.source_path,
        "".join(lines),
        encoding="utf-8-sig",
        create_parents=False,
    )
    return True

_inspect_include_state

_inspect_include_state(
    custom_file_path: Path, project_dir: Path
) -> _IncludeState

Read and validate AutoCorrect2's include state for one custom file.

Source code in hotstring\autocorrect2\integration.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
def _inspect_include_state(custom_file_path: Path, project_dir: Path) -> _IncludeState:
    """Read and validate AutoCorrect2's include state for one custom file."""
    source_path = (project_dir / AUTOCORRECT2_MAIN_RELATIVE_PATH).resolve(strict=False)
    if not source_path.is_file():
        raise FileNotFoundError(f"AutoCorrect2 main source file was not found: {source_path}")

    custom_path = _resolve_custom_file_path(custom_file_path, project_dir)
    built_in_hotstrings_path = (project_dir / AUTOCORRECT_HOTSTRINGS_RELATIVE_PATH).resolve(
        strict=False
    )

    if _same_path(custom_path, source_path):
        raise ValueError("The custom include path cannot refer to AutoCorrect2.ahk itself.")
    if _same_path(custom_path, built_in_hotstrings_path):
        raise ValueError("The custom include path cannot refer to AutoCorrectHotstrings.ahk.")

    content = read_text(source_path, encoding="utf-8-sig")
    lines = content.splitlines(keepends=True)
    hotif_start, hotif_end = _find_autocorrection_hotif(lines, source_path)

    built_in_indices = _find_include_indices(
        lines,
        target_path=built_in_hotstrings_path,
        source_dir=source_path.parent,
    )
    if len(built_in_indices) != 1:
        raise AutoCorrect2IntegrationError(
            "Expected exactly one include of AutoCorrectHotstrings.ahk in "
            f"{source_path}, found {len(built_in_indices)}."
        )

    built_in_index = built_in_indices[0]
    if not hotif_start < built_in_index < hotif_end:
        raise AutoCorrect2IntegrationError(
            "AutoCorrectHotstrings.ahk is not included inside the expected "
            "#HotIf AutoCorrectionsActivelyRunning() context."
        )

    custom_indices = _find_include_indices(
        lines,
        target_path=custom_path,
        source_dir=source_path.parent,
    )
    if len(custom_indices) > 1:
        raise AutoCorrect2IntegrationError(
            f"Found {len(custom_indices)} includes of the custom file {custom_path}; "
            "exactly zero or one is allowed."
        )

    custom_include_index = custom_indices[0] if custom_indices else None
    if custom_include_index is not None and not (hotif_start < custom_include_index < hotif_end):
        raise AutoCorrect2IntegrationError(
            f"The custom file {custom_path} is included outside the expected "
            "#HotIf AutoCorrectionsActivelyRunning() context."
        )

    return _IncludeState(
        source_path=source_path,
        lines=lines,
        hotif_start=hotif_start,
        hotif_end=hotif_end,
        custom_include_index=custom_include_index,
    )

_find_autocorrection_hotif

_find_autocorrection_hotif(
    lines: list[str], source_path: Path
) -> tuple[int, int]

Return the start and end indices of the autocorrection #HotIf block.

Source code in hotstring\autocorrect2\integration.py
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
def _find_autocorrection_hotif(lines: list[str], source_path: Path) -> tuple[int, int]:
    """Return the start and end indices of the autocorrection `#HotIf` block."""
    starts = [
        index
        for index, line in enumerate(lines)
        if _TARGET_HOTIF_PATTERN.fullmatch(line.rstrip("\r\n")) is not None
    ]
    if len(starts) != 1:
        raise AutoCorrect2IntegrationError(
            "Expected exactly one '#HotIf AutoCorrectionsActivelyRunning()' "
            f"directive in {source_path}, found {len(starts)}."
        )

    start = starts[0]
    for index in range(start + 1, len(lines)):
        if _HOTIF_PATTERN.fullmatch(lines[index].rstrip("\r\n")) is not None:
            return start, index

    raise AutoCorrect2IntegrationError(
        "The '#HotIf AutoCorrectionsActivelyRunning()' context has no terminating "
        f"#HotIf directive in {source_path}."
    )

_find_include_indices

_find_include_indices(
    lines: list[str], *, target_path: Path, source_dir: Path
) -> list[int]

Return line indices whose static include resolves to target_path.

Source code in hotstring\autocorrect2\integration.py
288
289
290
291
292
293
294
295
def _find_include_indices(lines: list[str], *, target_path: Path, source_dir: Path) -> list[int]:
    """Return line indices whose static include resolves to `target_path`."""
    return [
        index
        for index, line in enumerate(lines)
        if (include_path := _parse_include_path(line, source_dir)) is not None
        and _same_path(include_path, target_path)
    ]

_parse_include_path

_parse_include_path(
    line: str, source_dir: Path
) -> Path | None

Resolve a static AutoHotkey include directive, if the line contains one.

Source code in hotstring\autocorrect2\integration.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def _parse_include_path(line: str, source_dir: Path) -> Path | None:
    """Resolve a static AutoHotkey include directive, if the line contains one."""
    match = _INCLUDE_PATTERN.fullmatch(line.rstrip("\r\n"))
    if match is None:
        return None

    include_text = match.group(1) if match.group(1) is not None else match.group(2)
    if include_text is None:
        return None

    include_text = _OPTIONAL_INCLUDE_PREFIX_PATTERN.sub("", include_text.strip(), count=1)
    if not include_text or (include_text.startswith("<") and include_text.endswith(">")):
        return None

    include_text = include_text.replace("\\", os.sep).replace("/", os.sep)
    include_path = Path(include_text)
    if not include_path.is_absolute():
        include_path = source_dir / include_path

    return include_path.resolve(strict=False)

_resolve_custom_file_path

_resolve_custom_file_path(
    custom_file_path: Path, project_dir: Path
) -> Path

Resolve a caller-supplied custom path against the AutoCorrect2 project.

Source code in hotstring\autocorrect2\integration.py
320
321
322
323
324
325
326
327
328
def _resolve_custom_file_path(custom_file_path: Path, project_dir: Path) -> Path:
    """Resolve a caller-supplied custom path against the AutoCorrect2 project."""
    if not isinstance(custom_file_path, Path):
        raise TypeError(
            f"custom_file_path must be a pathlib.Path, not {type(custom_file_path).__name__}"
        )

    path = custom_file_path if custom_file_path.is_absolute() else project_dir / custom_file_path
    return path.resolve(strict=False)

_render_include_path

_render_include_path(
    target_path: Path, source_dir: Path
) -> str

Render a static include path relative to AutoCorrect2's main source file.

Source code in hotstring\autocorrect2\integration.py
331
332
333
334
335
336
337
338
339
340
341
342
def _render_include_path(target_path: Path, source_dir: Path) -> str:
    """Render a static include path relative to AutoCorrect2's main source file."""
    try:
        include_path = os.path.relpath(target_path, start=source_dir)
    except ValueError:
        include_path = str(target_path)

    include_path = include_path.replace("/", "\\")
    if '"' in include_path or "\n" in include_path or "\r" in include_path:
        raise ValueError(f"Custom include path cannot be represented safely: {target_path}")

    return include_path

_hotif_indentation

_hotif_indentation(state: _IncludeState) -> str

Return the indentation convention used inside the target #HotIf block.

Source code in hotstring\autocorrect2\integration.py
345
346
347
348
349
350
351
def _hotif_indentation(state: _IncludeState) -> str:
    """Return the indentation convention used inside the target `#HotIf` block."""
    for line in state.lines[state.hotif_start + 1 : state.hotif_end]:
        stripped = line.lstrip(" \t")
        if stripped and not stripped.startswith(";"):
            return line[: len(line) - len(stripped)]
    return "    "

_detect_newline

_detect_newline(lines: list[str]) -> str

Return the source file's existing newline convention.

Source code in hotstring\autocorrect2\integration.py
354
355
356
357
358
359
360
361
362
363
def _detect_newline(lines: list[str]) -> str:
    """Return the source file's existing newline convention."""
    for line in lines:
        if line.endswith("\r\n"):
            return "\r\n"
        if line.endswith("\n"):
            return "\n"
        if line.endswith("\r"):
            return "\r"
    return os.linesep

_same_path

_same_path(first: Path, second: Path) -> bool

Compare paths using Windows-style case-insensitive semantics.

Source code in hotstring\autocorrect2\integration.py
366
367
368
369
370
def _same_path(first: Path, second: Path) -> bool:
    """Compare paths using Windows-style case-insensitive semantics."""
    first_text = str(first.resolve(strict=False)).replace("\\", "/").casefold()
    second_text = str(second.resolve(strict=False)).replace("\\", "/").casefold()
    return first_text == second_text