orthonym.assembly.handlers#
Note
Internal API. Names and behaviour may change between releases.
Orthonym Handler Package (a phase).
Per-class IUPAC-name handlers lifted from
composer.py:_assemble_name_impl into per-file modules under this
package (DECOMP-01, DECOMP-05). Each handler exposes ONE public
name_<handler_id>(features, mol=None, style='pin') -> Optional[NamingResult]
callable registered in src/orthonym/assembly/inner_dispatch.py per
internal notes +.
Substrate commit 02-00 ships the package SKELETON: this __init__.py,
the private _enrichment.py + _handler_shared.py modules, and the
re-exports of NameTreeNode + NamingResult + name_tree_to_string. No
handler files yet — those land in commits 02-01..02-29 (Plan-02 Tier-1 +
Tier-1.5) + 03-01..03-09 (Plan-03 Tier-2 + Tier-3).
Public exports per internal notes mirror (a phase routing/__init__.py
shape):
name_<handler_id>callables (28 handlers +general_acyclic+ion_dispatch+simple_molecule; appended per atomic commit).NameTreeNodere-export fromassembly/name_tree.py(convenience).NamingResultre-export fromassembly/name_tree.py(convenience).name_tree_to_stringre-export fromassembly/name_tree_to_string.py.dispatch_inner+INNER_DISPATCH_TABLEre-export fromassembly/inner_dispatch.py(convenience for tests + Plan-04 CLI).
Internal helpers (_enrichment.py, _handler_shared.py) are PRIVATE
and intentionally not exported per internal notes + a phase mirror
(no public plugin API; + extracts one if needed).
Anti-pattern hygiene: - -14 banned: silent ImportError fallback in handlers package
(no
try: from orthonym.assembly.handlers import...; except: pass).
-06 banned: invent-as-you-go handler_id outside HANDLER_POLICIES (only ion_dispatch + simple_molecule + general_acyclic are a phase additions per internal notes-192).
References: - internal notes-DECOMP.md + — handler enumeration + dependency graph. - 160-internal notes — one file per HANDLER_POLICIES handler_id. - internal notes — analog: routing/__init__.py:1-43.
- class orthonym.assembly.handlers.NameTreeNode(parent_stem, locants=(), suffix=None, prefixes=(), stereo=None, indicated_h=(), unsaturation_locants=((), ()), class_id='', multiplicative_prefix=None, parenthesization_hint=False, iupac_section_cite=None, fragment_legacy=None)#
Bases:
objectOne part of a name, and the parts inside it.
A name is written in the order stereodescriptors, prefixes (in alphanumerical order), parent, indicated hydrogen, unsaturation and suffix (IUPAC. A node holds those pieces for one parent; each prefix is a node of its own, so a substituent with its own substituents is a subtree. The node cannot be changed after it is made.
- Variables:
parent_stem (str) – The parent, for example
'cyclohex'.locants (tuple of int) – Locants of the suffix.
suffix (str or None) – The suffix, for example
'ol'.prefixes (tuple of NameTreeNode) – The substituent prefixes, each a node.
stereo (str or None) – The stereodescriptor part, for example
'(2R)'.indicated_h (tuple of int) – Locants of indicated hydrogen.
unsaturation_locants (tuple of (tuple of int, tuple of int)) – Locants of double and of triple bonds.
class_id (str) – The compound class that built the node.
multiplicative_prefix (str or None) – A multiplying prefix such as
'di'.parenthesization_hint (bool) – True when the prefix must be written in enclosing marks.
iupac_section_cite (str or None) – The section of the recommendations the node follows, for example
''.fragment_legacy (object or None) – An older representation of the same part, kept for the engine’s own use.
Examples
>>> from orthonym import name_with_tree >>> tree = name_with_tree("OC1CCCCC1").tree >>> tree.parent_stem, tree.suffix ('cyclohex', 'ol')
- parent_stem: str#
- locants: Tuple[int, ...] = ()#
- suffix: str | None = None#
- prefixes: Tuple[NameTreeNode, ...] = ()#
- stereo: str | None = None#
- indicated_h: Tuple[int, ...] = ()#
- unsaturation_locants: Tuple[Tuple[int, ...], Tuple[int, ...]] = ((), ())#
- class_id: str = ''#
- multiplicative_prefix: str | None = None#
- parenthesization_hint: bool = False#
- iupac_section_cite: str | None = None#
- fragment_legacy: object | None = None#
- class orthonym.assembly.handlers.NamingResult(name, tree=None, atom_to_locant_hint=None)#
Bases:
NamedTupleA name together with the tree of its parts.
Returned by:func:orthonym.name_with_tree and
orthonym.Orthonym.name_with_tree(). It is a named tuple of three fields.- Variables:
name (str) – The name, the same string:func:orthonym.name_compound returns.
tree (NameTreeNode or None) – The parts of the name.
atom_to_locant_hint (dict of int to int or None) – Atom index to locant, where the part of the engine that built the name recorded it.
Examples
>>> from orthonym import name_with_tree >>> name, tree, hint = name_with_tree("OC1CCCCC1") >>> name 'cyclohexanol'
- name: str#
Alias for field number 0
- tree: NameTreeNode | None#
Alias for field number 1
- atom_to_locant_hint: Dict[int, int] | None#
Alias for field number 2
- orthonym.assembly.handlers.name_tree_to_string(node, style='pin')#
a phase + IUPAC Pass-2 serializer.
Composition order:
[stereo] + [prefixes (alpha-sorted; recursively serialized)] + [parent_stem] + [indicated_h] + [unsaturation_infix] + [suffix]
- Parameters:
node (NameTreeNode) – The NameTreeNode to serialize.
style (str) – Naming style (“pin”, “general”, “cas”); forwarded to the legacy assembler when
fragment_legacyis set.
- Returns:
IUPAC name string per internal notes byte-identical contract.
- Raises:
NameTreeSerializerError – if
node.parent_stem == ""ANDnode.fragment_legacy is None(malformed tree per DECOMP-02 honest-fail-on-data).- Return type:
str
- orthonym.assembly.handlers.dispatch_inner(features, mol=None, style='pin')#
a phase + a phase / -04: first-match-AND- succeeds-wins inner-cascade dispatch.
Iterates
INNER_DISPATCH_TABLEin priority order (lowest first; OrderedDict insertion order matches priority-sorted insertion via_register_inner); for each entry whose predicate matches, INVOKES the handler internally. On non-None return, builds and returnsInnerDispatchResultcarrying the result. On None return (gate-fail per -04 contract), CONTINUES to the next-priority entry. Final no-match returnsNone.-04 amends a phase internal notes from “first-match-wins” to “first-match-AND-succeeds-wins.” Handler contract:
Return non-None
NamingResult⇒ “I succeeded; use this result.”Return
None⇒ “I gate-failed; defer to next-priority handler.”Raise
Exception⇒ surfaced asRuntimeErrorchained via__cause__(no silent swallowing).
Predicate purity (a phase) preserved verbatim: predicates report whether a handler CAN POSSIBLY apply (cheap structural check); handlers may perform internal pool.add side effects per their individual contracts.
- Parameters:
features (Any) –
MolecularFeaturesinstance (the upstream perception output that handlers consume).mol (Any) – RDKit
Molobject. Defaults tofeatures.molwhen None (preserves the pre-amendment call-site signature).style (str) – PIN / IUPAC name-style flag forwarded to handlers; default
"pin".
- Returns:
InnerDispatchResultcarrying the SUCCESSFUL handler’sNamingResultin.result, orNoneif no handler succeeded. Oncegeneral_acycliccatch-all ships (Plan-03-03 per internal notes), the None branch is unreachable in production.- Return type:
InnerDispatchResult | None
Per internal notes honest-fail-on-data: defensive try/except around BOTH predicate AND handler invocations surface bugs (NoneType attribute access, etc.) as
RuntimeErrorchained via__cause__.
- class orthonym.assembly.handlers.InnerDispatchEntry(handler_id, priority, predicate, handler, iupac_section, description, side_effect_inventory=())#
Bases:
objecta phase: frozen dataclass row of INNER_DISPATCH_TABLE.
Mirrors a phase + (routing/dispatch_table.ClassDispatchEntry) at the INNER dispatch layer. The 7-field schema is locked per internal notes :
handler_id HANDLER_POLICIES key (lowercase snake_case) priority spaced int; lower = earlier; first-match-wins predicate Callable[…, bool]; MolecularFeatures → bool;
PURE per (no mutation of features / mol / module-global state)
- handler Callable[…, Optional[NamingResult]]; the
handler module’s name_<handler_id> entry point
iupac_section Blue Book P-section cite (audit metadata) description one-line summary for audit logging side_effect_inventory MUST be per hard invariant; non-empty
tuples raise ValueError in _register_inner.
Per internal notes + -15:
side_effect_inventory == `` is the HARD invariant; the integrity test ``test_side_effect_inventory_is_empty(Plan-04) asserts emptiness for every entry. -26 explicitly bans any predicate that mutatesMolecularFeatures,mol, module-global state, or thread-local state. Handler invocations MAY callpool.add(the SOLE accepted shared-state interaction per a phase contract); recursiveorthonym.name_compound(...)calls are PERMITTED in the N-oxide handler (audited per of internal notes-DECOMP.md).- handler_id: str#
- priority: int#
- predicate: Callable[[...], bool]#
- handler: Callable[[...], Any | None]#
- iupac_section: str#
- description: str#
- side_effect_inventory: Tuple[str, ...] = ()#
- orthonym.assembly.handlers.name_oxime(features, mol=None, style='pin')#
a phase Tier-B oxime handler.
Verbatim semantics of composer.py:771-782 (inline branch). Returns
NamingResult(name=<final string>, tree=None, atom_to_locant_hint=None)on success;Noneon gate-fail / not-applicable (pool gate threshold 0.40 per HANDLER_POLICIES[‘oxime’]).Wave-1 strategy per internal notes: lazy-import the composer.py body (
_name_oxime_or_hydrazone) and the enrichment helper (_enrich_handler_name); call them with the same arguments the inline branch used; route through the samepool.addcall so a phase byte-identical lock methodology is preserved.- The
molparameter is accepted for API uniformity per internal notes but not used here (composer.py:_name_oxime_or_hydrazone reads
features.mol directly). The
styleparameter is similarly unused for first-wave Tier-B handlers (style only affects Pass-2 assembly, not Tier-B handlers).- Parameters:
features (Any) – MolecularFeatures object.
mol (Any) – RDKit Mol object (not used in first-wave; reserved per).
style (str) – Naming style (not used in first-wave Tier-B; reserved per).
- Returns:
NamingResult on success, or None on gate-fail. Per internal notes the
treefield is None for first-wave handlers; thenamefield is the byte-identical contract per DECOMP-03.- Return type:
NamingResult | None
- The
- orthonym.assembly.handlers.name_hydrazone(features, mol=None, style='pin')#
a phase Tier-B hydrazone handler.
Verbatim semantics of composer.py:785-794 (inline branch). Returns
NamingResult(name, tree=None, atom_to_locant_hint=None)on success; None on gate-fail (Pool’s HANDLER_POLICIES[‘hydrazone’] gate 0.40).
- orthonym.assembly.handlers.name_n_oxide(features, mol=None, style='pin')#
a phase direct-return N-oxide handler.
Verbatim semantics of composer.py:813-820 (inline branch). Returns
NamingResult(name, tree=None, atom_to_locant_hint=<heterocycle locant map>)on success; None when not an N-oxide.Per internal notes-DECOMP.md: the recursive name_fragment_recursively call inside _try_name_n_oxide is PERMITTED under because it instantiates a fresh push_pool/pop_pool lifecycle per a phase and operates on a COPY of features.mol (no outer mutation).
- orthonym.assembly.handlers.name_isocyanate(features, mol=None, style='pin')#
a phase Tier-B isocyanate handler.
Verbatim semantics of composer.py:828-836 (inline branch).
- orthonym.assembly.handlers.name_isothiocyanate(features, mol=None, style='pin')#
a phase Tier-B isothiocyanate handler.
Verbatim semantics of composer.py:842-850 (inline branch).
- orthonym.assembly.handlers.name_carbamic_acid(features, mol=None, style='pin')#
a phase Tier-B carbamic acid handler.
Verbatim semantics of composer.py:855-863.
- orthonym.assembly.handlers.name_carbamate(features, mol=None, style='pin')#
a phase Tier-B carbamate handler.
- orthonym.assembly.handlers.name_urea(features, mol=None, style='pin')#
a phase Tier-B urea handler.
- orthonym.assembly.handlers.name_thiourea(features, mol=None, style='pin')#
Tier-B chalcogen-urea retained-name handler.
- orthonym.assembly.handlers.name_guanidine(features, mol=None, style='pin')#
a phase Tier-B guanidine handler.
- orthonym.assembly.handlers.name_cyanamide(features, mol=None, style='pin')#
Tier-B cyanamide handler.
- orthonym.assembly.handlers.name_boronic_acid(features, mol=None, style='pin')#
a phase Tier-B boronic acid handler.
Note: inline branch at composer.py:1213 passes no explicit atom_to_locant to _inject_stereo_if_missing (default None). We mirror that here.
- orthonym.assembly.handlers.name_acid_halide(features, mol=None, style='pin')#
Direct-return acid-halide handler.
Verbatim lift of composer.py:919-932. Lazy import keeps the handlers.acid_halide -> rules.acid_halides chain off the module- import-time graph (a phase + PATTERNS § Lazy Import).
- orthonym.assembly.handlers.name_anhydride(features, mol=None, style='pin')#
Direct-return anhydride handler.
- orthonym.assembly.handlers.name_lactone(features, mol=None, style='pin')#
Direct-return lactone handler with coverage guard.
Verbatim semantics of composer.py:827-848. Returns None if the coverage guard rejects (large substituted lactone where the bare name would be incomplete).
- orthonym.assembly.handlers.name_lactam(features, mol=None, style='pin')#
Direct-return lactam handler with coverage guard.
- orthonym.assembly.handlers.name_sulfoxide(features, mol=None, style='pin')#
Tier-B sulfoxide handler.
- orthonym.assembly.handlers.name_sulfone(features, mol=None, style='pin')#
Tier-B sulfone handler.
- orthonym.assembly.handlers.name_thioether(features, mol=None, style='pin')#
Tier-B thioether handler with cyclic + fused-heterocycle skip guards.
- orthonym.assembly.handlers.name_phosphine_oxide(features, mol=None, style='pin')#
Direct-return phosphine_oxide handler.
- orthonym.assembly.handlers.name_phosphate_ester(features, mol=None, style='pin')#
Direct-return phosphate-ester handler.
- orthonym.assembly.handlers.name_sulfate_ester(features, mol=None, style='pin')#
Direct-return sulfate-ester handler.
- orthonym.assembly.handlers.name_phosphine(features, mol=None, style='pin')#
Direct-return phosphine handler with benzene-parent skip guard.
- orthonym.assembly.handlers.name_phosphinic_acid(features, mol=None, style='pin')#
Direct-return phosphinic_acid handler.
- orthonym.assembly.handlers.name_ring_assembly(features, mol=None, style='pin')#
Direct-return ring_assembly handler.
- orthonym.assembly.handlers.name_polycyclic(features, mol=None, style='pin')#
Direct-return polycyclic handler — wraps _assemble_polycyclic_name.
- orthonym.assembly.handlers.name_partial_sat(features, mol=None, style='pin')#
Tier-B partial_sat handler.
- orthonym.assembly.handlers.name_simple_molecule(features, mol=None, style='pin')#
Direct-return simple-molecule handler.
Plan-10 DECOMP-02 OBSERVABLE CLOSURE: this is the FIRST tree-emitting handler in Orthonym. The NameTreeNode contains only
parent_stem(the noble-gas / atom name) because simple molecules have no locants, no prefixes, no suffix, no stereo.name_tree_to_string(tree)is byte-identical toresult.name.
- orthonym.assembly.handlers.name_ion_dispatch(features, mol=None, style='pin')#
Pre-pool ion / salt / zwitterion / radical naming dispatcher.
Wraps
composer.assemble_ion_nameper internal notes. The inline bypass at composer.py:751-768 is the primary call path; this handler function exists as a callable target so the architecture is uniform across all HANDLER_POLICIES keys.
- orthonym.assembly.handlers.name_ring_nitrile(features, mol=None, style='pin')#
Direct-return ring_nitrile handler.
Verbatim semantics of composer.py:1337-1348. Returns
NamingResult(name=<final string>, tree=None, atom_to_locant_hint=None)on success._inject_stereo_if_missingis applied per the inline branch’s behavior at composer.py:1348.
- orthonym.assembly.handlers.name_amide(features, mol=None, style='pin')#
Direct-return amide handler (fast-path).
Verbatim semantics of composer.py:1343-1356. Returns
NamingResult(name=best.name, tree=best.tree, atom_to_locant_hint=None)afterpool.add(name, 'amide', features, tree=...)(a phase SCORE-01: structured tree from the chain-fragment path, else a coarse node). No_inject_stereo_if_missingwrap per the inline branch behavior at composer.py:1356.
- orthonym.assembly.handlers.name_amine(features, mol=None, style='pin')#
Direct-return amine handler (fast-path).
Verbatim semantics of composer.py:1374-1386. Returns
NamingResult(name=best.name, tree=best.tree, atom_to_locant_hint=None)afterpool.add(name, 'amine', features, tree=...)(a phase SCORE-01: counted coarse node, parity-safe via fragment_legacy). Returns None if_assemble_amine_namereturns falsy (per inline guard at composer.py:1375).
- orthonym.assembly.handlers.name_imine(features, mol=None, style='pin')#
Direct-return N-substituted-imine handler (amine-handler pattern).
- orthonym.assembly.handlers.name_ring_ester(features, mol=None, style='pin')#
Direct-return ring_ester handler.
Verbatim semantics of composer.py:850-865. Returns
NamingResult(name=<stereo-injected name>, tree=None, atom_to_locant_hint=None)on success. Wraps the result in_inject_stereo_if_missingper the inline branch behavior at composer.py:865.
- orthonym.assembly.handlers.name_organometallic(features, mol=None, style='pin')#
a phase ORGM handler; CFR-routed at priority 50.
Per internal notes: returns Optional[NamingResult] with tree populated when result is non-None. The CFR shim _handle_organometallic in routing/dispatch_table.py extracts result.name as Optional[str].
Tier-1 fast path: retained-PIN lookup for style=”pin” — ferrocene, ruthenocene, etc. Tier-1 systematic + Tier-2/3/4 forms go through the systematic-assembly path.
Cascade-continuation on None per internal notes: any failure (mol is None; metal_complex is None; multimetal; ValueError from hapticity; result is None) returns None so CFR cascade falls through to SALT@100 →… → GENERAL.