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).

  • NameTreeNode re-export from assembly/name_tree.py (convenience).

  • NamingResult re-export from assembly/name_tree.py (convenience).

  • name_tree_to_string re-export from assembly/name_tree_to_string.py.

  • dispatch_inner + INNER_DISPATCH_TABLE re-export from assembly/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: object

One 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: NamedTuple

A 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_legacy is set.

Returns:

IUPAC name string per internal notes byte-identical contract.

Raises:

NameTreeSerializerError – if node.parent_stem == "" AND node.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_TABLE in 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 returns InnerDispatchResult carrying the result. On None return (gate-fail per -04 contract), CONTINUES to the next-priority entry. Final no-match returns None.

-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 as RuntimeError chained 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) – MolecularFeatures instance (the upstream perception output that handlers consume).

  • mol (Any) – RDKit Mol object. Defaults to features.mol when None (preserves the pre-amendment call-site signature).

  • style (str) – PIN / IUPAC name-style flag forwarded to handlers; default "pin".

Returns:

InnerDispatchResult carrying the SUCCESSFUL handler’s NamingResult in .result, or None if no handler succeeded. Once general_acyclic catch-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 RuntimeError chained via __cause__.

class orthonym.assembly.handlers.InnerDispatchEntry(handler_id, priority, predicate, handler, iupac_section, description, side_effect_inventory=())#

Bases: object

a 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 mutates MolecularFeatures, mol, module-global state, or thread-local state. Handler invocations MAY call pool.add (the SOLE accepted shared-state interaction per a phase contract); recursive orthonym.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; None on 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 same pool.add call so a phase byte-identical lock methodology is preserved.

The mol parameter is accepted for API uniformity per internal notes

but not used here (composer.py:_name_oxime_or_hydrazone reads

features.mol directly). The style parameter 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 tree field is None for first-wave handlers; the name field is the byte-identical contract per DECOMP-03.

Return type:

NamingResult | None

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 to result.name.

orthonym.assembly.handlers.name_ion_dispatch(features, mol=None, style='pin')#

Pre-pool ion / salt / zwitterion / radical naming dispatcher.

Wraps composer.assemble_ion_name per 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_missing is 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) after pool.add(name, 'amide', features, tree=...) (a phase SCORE-01: structured tree from the chain-fragment path, else a coarse node). No _inject_stereo_if_missing wrap 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) after pool.add(name, 'amine', features, tree=...) (a phase SCORE-01: counted coarse node, parity-safe via fragment_legacy). Returns None if _assemble_amine_name returns 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_missing per 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.