Wrapper & Adapter¶
This note defines MolPy’s external integration contract and the boundaries between:
molpy.wrapper(subprocess wrappers)molpy.adapter(representation adapters)
The intent is to keep molpy.core stable and dependency-light while still supporting optional tooling (AmberTools, RDKit, etc.).
The contract (strict)¶
molpy.wrapper¶
Wrappers may execute subprocesses. They own:
- selecting an executable (
exe) and resolving it from PATH/conda - managing working directories (
workdir) - environment overrides (
env) - capturing stdout/stderr and surfacing return codes
Wrappers must not own:
- workflow decisions (what steps to run / in what order)
- chemistry semantics or domain logic
- conversion of tool outputs into MolPy data models (that belongs in workflow code or explicit readers/adapters)
molpy.adapter¶
Adapters must not execute subprocesses. They own:
- keeping two representations synchronized (internal ↔ external)
- stable mapping/IDs for round-trips
Adapters must not own:
- invoking CLIs/binaries
- hidden workflow logic
Wrapper: minimal, explicit side effects¶
The base type is molpy.wrapper.base.Wrapper. It is intentionally small: configure + run a command.
Key API points:
resolve_executable()/is_available()/check()run(args=..., cwd=..., input_text=...)→ returnssubprocess.CompletedProcess
Docs are built with notebook execution enabled; so examples below avoid calling optional binaries.
from pathlib import Path
from molpy.wrapper.base import Wrapper
w = Wrapper(name="example", exe="echo", workdir=Path("./_doc_tmp/wrapper"))
p = w.run(args=["wrapper", "ok"])
print(p.returncode, (p.stdout or '').strip())
0 wrapper ok
Adapter: deterministic sync, no subprocess¶
The base type is molpy.adapter.base.Adapter. It provides:
get_internal()/get_external()(lazy sync)sync_to_internal()/sync_to_external()(implemented by subclasses)
To keep docs runnable without optional deps, we demonstrate with a tiny pure-Python adapter below.
from molpy.adapter.base import Adapter
class UppercaseAdapter(Adapter[str, dict]):
def sync_to_internal(self) -> None:
super().sync_to_internal()
assert self._external is not None
self._internal = str(self._external.get('value', '')).upper()
def sync_to_external(self) -> None:
super().sync_to_external()
assert self._internal is not None
self._external = {'value': self._internal.lower()}
a = UppercaseAdapter(external={'value': 'MolPy'})
print('internal:', a.get_internal())
a.set_internal('Hello')
print('external:', a.get_external())
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[2], line 16 12 assert self._internal is not None 13 self._external = {'value': self._internal.lower()} ---> 16 a = UppercaseAdapter(external={'value': 'MolPy'}) 17 print('internal:', a.get_internal()) 18 a.set_internal('Hello') TypeError: Can't instantiate abstract class UppercaseAdapter without an implementation for abstract methods '_do_sync_to_external', '_do_sync_to_internal'
Composition rule¶
When you need files + CLIs + conversion, split responsibilities:
- Workflow/compute: decides steps + parameters, owns artifact paths
- Adapter/IO: reads/writes explicit artifacts or bridges in-memory objects
- Wrapper: executes the CLI
This separation is what keeps MolPy integrations testable and prevents adapters/wrappers from becoming a hidden workflow system.
Compatibility facade¶
molpy.external remains as a temporary compatibility facade that re-exports the new adapter/wrapper APIs.
New code should import from molpy.wrapper and molpy.adapter directly.