"""
Conflict policy for file-write operations (DOP Phase 7, v2.24.0).
This module introduces :data:`ConflictPolicy`, a ``Literal`` type that
replaces the historical ``(overwrite: bool, exist_ok: bool)`` XOR pair
on the COO writers and reconstruction wrappers.
DOP alignment
-------------
- **Principle 4 (make illegal states unrepresentable):** the legacy
pair ``(overwrite=False, exist_ok=False)`` raises, ``(True, False)``
overwrites silently, ``(False, True)`` skips silently, ``(True, True)``
is ambiguous (logs a warning in some sites). Three of the four
combinations are legal, only three are meaningful, and one is silently
ill-defined. Phase 7 collapses the pair into a single named mode
(``"error"``, ``"overwrite"``, ``"skip"``) so the illegal fourth
combination cannot be expressed.
- **Principle 1 (separate code from data):** the policy is a literal
type — pure data — separate from the file-writing code that consumes
it. The conversion from the legacy ``(overwrite, exist_ok)`` pair to
a ``ConflictPolicy`` lives in :func:`from_legacy_flags`, which is
the only place that touches both shapes during the deprecation window.
Pattern: bool-pair → Literal
----------------------------
Five functions currently accept the ``(overwrite, exist_ok)`` pair:
- ``gunz_cm.converters.coo.convert_to_cm_coo``
- ``gunz_cm.converters.coo.convert_all_intra_to_cm_coo`` (asymmetric:
only ``overwrite`` exposed, ``exist_ok`` hardcoded internally)
- ``gunz_cm.reconstructions.implementations.superrec.gen_superrec_coo``
- ``gunz_cm.reconstructions.implementations.shneigh.gen_shneigh_coo``
- ``gunz_cm.reconstructions.implementations.h3dg.gen_h3dg_coo``
(asymmetric: only ``overwrite`` exposed)
Phase 7 introduces ``on_conflict: ConflictPolicy = "error"`` on all
five. The legacy ``overwrite`` and ``exist_ok`` kwargs remain as
back-compat inputs for one release (v2.24.0) with ``DeprecationWarning``.
Phase 8 (v2.25.0) will remove them.
Examples
--------
>>> from gunz_cm.structs.conflict_policy import ConflictPolicy, from_legacy_flags
>>> from_legacy_flags(overwrite=False, exist_ok=False)
'error'
>>> from_legacy_flags(overwrite=True, exist_ok=False)
'overwrite'
>>> from_legacy_flags(overwrite=False, exist_ok=True)
'skip'
>>> # The (True, True) combination is ill-defined and produces a clear error.
>>> from_legacy_flags(overwrite=True, exist_ok=True)
Traceback (most recent call last):
...
ValueError: (overwrite=True, exist_ok=True) is ambiguous — use on_conflict instead.
"""
from __future__ import annotations
import typing as t
# ConflictPolicy: the canonical named modes for "what to do when the
# output file already exists."
#
# - "error": raise FileExistsError. The safe default for batch jobs.
# - "overwrite": silently replace the existing file. Use when the caller
# has verified the existing file is stale (e.g. via cache invalidation).
# - "skip": log a message and return without writing. Use in long-running
# pipelines that should be idempotent across re-runs.
ConflictPolicy = t.Literal["error", "overwrite", "skip"]
[docs]def from_legacy_flags(*, overwrite: bool, exist_ok: bool) -> ConflictPolicy:
"""Translate the legacy ``(overwrite, exist_ok)`` pair to a :data:`ConflictPolicy`.
This is the single point of conversion during the v2.24.0 deprecation
window. It exists so that:
1. The four legal ``(overwrite, exist_ok)`` combinations map to
exactly one of the three named modes.
2. The illegal ``(True, True)`` combination produces a clear
``ValueError`` rather than silently falling through to one of
the other branches.
Mapping
-------
- ``(False, False)`` → ``"error"`` (raise on conflict)
- ``(True, False)`` → ``"overwrite"`` (replace existing)
- ``(False, True)`` → ``"skip"`` (log + return)
- ``(True, True)`` → ``ValueError`` (ambiguous — prefer the new
``on_conflict`` parameter for clarity)
Parameters
----------
overwrite
The legacy ``overwrite`` flag.
exist_ok
The legacy ``exist_ok`` flag.
Returns
-------
ConflictPolicy
The corresponding named mode.
Raises
------
ValueError
If both flags are ``True`` — the combination is ambiguous.
Examples
--------
>>> from_legacy_flags(overwrite=False, exist_ok=False)
'error'
>>> from_legacy_flags(overwrite=True, exist_ok=False)
'overwrite'
>>> from_legacy_flags(overwrite=False, exist_ok=True)
'skip'
"""
if overwrite and exist_ok:
raise ValueError(
"(overwrite=True, exist_ok=True) is ambiguous — use the new "
"on_conflict parameter instead. See gunz_cm.structs.ConflictPolicy."
)
if overwrite:
return "overwrite"
if exist_ok:
return "skip"
return "error"
__all__ = [
"ConflictPolicy",
"from_legacy_flags",
]