Skip to content

Add explicit Engine.close() + context-manager; bump engine to v3.1.55 - #27

Merged
ddehilster merged 2 commits into
VisualText:mainfrom
ddehilster:feature/explicit-close
Jun 3, 2026
Merged

ddehilster merged 2 commits into
VisualText:mainfrom
ddehilster:feature/explicit-close

Conversation

@ddehilster

Copy link
Copy Markdown
Member

Background

Today on Windows, NLPPlus.Engine() (auto-tempdir form) silences a PermissionError during cleanup via TemporaryDirectory(prefix=..., ignore_cleanup_errors=True) — PR #24. The error came from the C++ engine holding <workfolder>/logs/cgerr.log open at process exit; Python's tempdir cleanup ran during atexit, before nanobind tore down the NLP_ENGINE instance, so Windows refused to delete a directory containing an open file.

The band-aid leaks the tempdir on disk and silences a real ordering bug.

The companion engine PR (nlp-engine#641, shipped in v3.1.55 as NLP-ENGINE-523) made NLP_ENGINE::close() idempotent so it can be called explicitly from Python without breaking the existing ~NLP_ENGINE → close() path.

What this PR does

  1. Bump nlp-engine submodule to v3.1.55.

  2. bindings.cpp: expose NLP_ENGINE::close() via nanobind:

    .def("close", &NLP_ENGINE::close,
         "Tear down the engine's VTRun runtime and release the open "
         "<workfolder>/logs/cgerr.log handle. Safe to call multiple "
         "times (engine v3.1.55+ NLP-ENGINE-523 made close() idempotent). ...");
  3. NLPPlus/__init__.py Engine:

    • New close() method — calls self.engine.close(), then self.tmpdir.cleanup() if applicable. Idempotent via self._closed guard.
    • New __enter__ / __exit__ so users can do with NLPPlus.Engine() as e: ... and get deterministic cleanup.
    • New __del__ that best-effort calls close() (swallows exceptions because the bindings module may be torn down during interpreter shutdown).
    • AttributeError fallback in close() if someone runs new NLPPlus against an older engine binding without .close() exposed — degrades to the old behaviour.
  4. ignore_cleanup_errors=True stays as defense-in-depth, not as the load-bearing fix:

    • with / explicit close() → engine closes, tempdir rmtree's cleanly. No PermissionError, no leaked tempdir.
    • del e → __del__ best-effort closes too. Usually clean.
    • Crash / os._exit / shutdown ordering preventing __del__ from running → flag catches it, stderr stays quiet, OS reclaims tempdir at process exit anyway.

Test plan

# 1. Explicit close — preferred for scripts
e = NLPPlus.Engine()
e.analyze("hello world", "parse-en-us")
e.close()                                # tempdir gone, no PermissionError

# 2. Context manager — preferred for blocks
with NLPPlus.Engine() as e:
    e.analyze("hello world", "parse-en-us")
                                          # tempdir gone at end of block

# 3. Implicit / legacy — still works, no regression
e = NLPPlus.Engine()
e.analyze("hello world", "parse-en-us")
del e                                     # __del__ closes; tempdir cleans

On Windows: verify with Process Monitor that <workfolder> is gone after close() returns, and that there's no stale PermissionError traceback on del e.

Versioning

Ready for the next NLPPlus release (would be v2.0.4) once this merges:

git tag -a v2.0.4 -m "..." && git push upstream v2.0.4

The bundled engine is now v3.1.55, so PyPI consumers get the deterministic-close path automatically.

ddehilster and others added 2 commits June 3, 2026 13:14
Companion to nlp-engine v3.1.55 (NLP-ENGINE-523), which made
NLP_ENGINE::close() idempotent so it can be called explicitly
from Python without breaking the existing ~NLP_ENGINE -> close()
path.

Changes:

1. bindings.cpp: expose .def("close", &NLP_ENGINE::close, ...) so
   Python code can deterministically tear down the engine before
   the auto-created TemporaryDirectory is removed.

2. NLPPlus/__init__.py Engine:
   - New explicit .close() method that calls self.engine.close()
     and then self.tmpdir.cleanup() in that order. Idempotent.
     After close(), further analyze()/compile() calls are
     undefined behavior.
   - New __enter__ / __exit__ so `with Engine() as e:` gets
     deterministic cleanup at the end of the block.
   - New __del__ that best-effort calls close(). Swallows
     exceptions because during interpreter shutdown the bindings
     module may already be torn down.
   - Initialized self._closed = False guard.
   - AttributeError fallback in close() in case someone runs the
     new NLPPlus against an older engine binding that doesn't have
     .close() exposed -- behaves the same as before in that case.

3. nlp-engine submodule bumped from v3.1.54 to v3.1.55.

4. TemporaryDirectory(ignore_cleanup_errors=True) is kept as
   defense-in-depth, not the load-bearing fix. Users who use
   `with Engine() as e:` or call e.close() get a clean rmtree;
   users who let __del__ tear things down at interpreter shutdown
   are also fine (best-effort close() runs first, and the
   ignore-flag catches the residual case where shutdown ordering
   prevents __del__ from running at all -- crash, os._exit, etc).

Test plan:

  # explicit close
  e = NLPPlus.Engine()
  e.analyze("hello world", "parse-en-us")
  e.close()
  # tempdir gone, no PermissionError on Windows

  # context manager
  with NLPPlus.Engine() as e:
      e.analyze("hello world", "parse-en-us")
  # tempdir gone

  # legacy "just let it GC" (still works, ignore_cleanup_errors=True
  # catches the residual case if __del__ doesn't run)
  e = NLPPlus.Engine()
  e.analyze("hello world", "parse-en-us")
  del e

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
NLP_ENGINE has two close() overloads in include/Api/lite/nlp_engine.h:

    int close();
    int close(_TCHAR *analyzer);

So &NLP_ENGINE::close is an unresolved overloaded function type and
g++ rejects nb::class_<>::def("close", &NLP_ENGINE::close, ...) with:

  error: no matching function for call to
  'nanobind::class_<NLP_ENGINE>::def(const char [6],
   <unresolved overloaded function type>, const char [521])'

Cast the member function pointer to the nullary overload — that's
the global-teardown one we want (the close(analyzer) overload removes
a single named analyzer from VTRun, unrelated to NLP-ENGINE-523).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ddehilster
ddehilster merged commit 6a3f27e into VisualText:main Jun 3, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant