Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 28 additions & 12 deletions NLPPlus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,20 +204,29 @@ def analyze(self, text: str, analyzer_name: str, develop: bool = False,
return Results(outtext, outdir)

def compile(self, analyzer_name: str, develop: bool = False,
kb_only: bool = False) -> Path:
kb_only: bool = False, analyzer_only: bool = False) -> Path:
"""Generate C++ source files for the named analyzer.

Runs the engine in ``-COMPILE`` mode (or ``-COMPILEKB`` if
``kb_only=True``), which emits the analyzer body under
``<analyzer>/run/`` and the knowledge base under
``<analyzer>/kb/``. Returns the analyzer directory containing
Runs the engine in ``-COMPILE`` mode, or ``-COMPILEKB`` if
``kb_only=True`` (KB only), or ``-COMPILEANA`` if
``analyzer_only=True`` (analyzer rules only, skipping the KB).
``-COMPILE`` emits the analyzer body under ``<analyzer>/run/``
and the knowledge base under ``<analyzer>/kb/``; ``-COMPILEKB``
emits just ``<analyzer>/kb/``; ``-COMPILEANA`` emits just
``<analyzer>/run/``. Returns the analyzer directory containing
those generated trees.

Use ``analyzer_only=True`` when only the rules changed and the
KB is already compiled. ``kb_only`` and ``analyzer_only`` are
mutually exclusive.

The generated C++ still needs to be built into shared
libraries before :meth:`analyze` can load them with
``compiled=True``. Use :meth:`cloud_compile` to do the build
step via the public nlp-compile-service in one call.
"""
if kb_only and analyzer_only:
raise ValueError("compile: kb_only and analyzer_only are mutually exclusive")
analyzer_name_p = Path(analyzer_name)
if self.analyzer_path:
analyzer_dir = (
Expand All @@ -229,12 +238,13 @@ def compile(self, analyzer_name: str, develop: bool = False,
self.working_folder / "analyzers" / analyzer_name_p
)
engine_arg = str(analyzer_name_p)
self.engine.compile(engine_arg, develop, kb_only)
self.engine.compile(engine_arg, develop, kb_only, analyzer_only)
return analyzer_dir

def cloud_compile(self, analyzer_name: str,
dispatcher_url: Optional[str] = None,
kb_only: bool = False,
analyzer_only: bool = False,
develop: bool = False,
poll_interval: float = 2.0,
timeout: float = 30 * 60,
Expand All @@ -259,6 +269,8 @@ def cloud_compile(self, analyzer_name: str,
dispatcher_url: override the public dispatcher endpoint
(default: ``cloud.DEFAULT_DISPATCHER_URL``).
kb_only: compile only the KB.
analyzer_only: compile only the analyzer rules (skip the KB).
Mutually exclusive with ``kb_only``.
develop: forwarded to local ``-COMPILE``.
poll_interval: seconds between job-status checks.
timeout: max seconds to wait for the runner build.
Expand All @@ -271,7 +283,7 @@ def cloud_compile(self, analyzer_name: str,
return cloud.cloud_compile(
self, analyzer_name,
dispatcher_url=dispatcher_url or cloud.DEFAULT_DISPATCHER_URL,
kb_only=kb_only, develop=develop,
kb_only=kb_only, analyzer_only=analyzer_only, develop=develop,
poll_interval=poll_interval, timeout=timeout,
skip_local_compile=skip_local_compile,
)
Expand Down Expand Up @@ -349,20 +361,23 @@ def analyze(text: str, parser: str = "parse-en-us", develop: bool = False,


def compile(analyzer: str = "parse-en-us", develop: bool = False,
kb_only: bool = False):
kb_only: bool = False, analyzer_only: bool = False):
"""Generate C++ source files for the named analyzer.

Wraps :meth:`Engine.compile`. The generated trees land under
``<analyzer>/run/`` and ``<analyzer>/kb/`` inside the engine's
working folder; they still need to be built into shared libraries
before :func:`analyze` can load them with ``compiled=True``.
working folder (or just ``kb/`` for ``kb_only``, or just ``run/``
for ``analyzer_only``); they still need to be built into shared
libraries before :func:`analyze` can load them with
``compiled=True``.
"""
return engine.compile(analyzer, develop, kb_only)
return engine.compile(analyzer, develop, kb_only, analyzer_only)


def cloud_compile(analyzer: str = "parse-en-us",
dispatcher_url: Optional[str] = None,
kb_only: bool = False,
analyzer_only: bool = False,
develop: bool = False,
poll_interval: float = 2.0,
timeout: float = 30 * 60,
Expand All @@ -376,7 +391,8 @@ def cloud_compile(analyzer: str = "parse-en-us",
"""
return engine.cloud_compile(
analyzer, dispatcher_url=dispatcher_url, kb_only=kb_only,
develop=develop, poll_interval=poll_interval, timeout=timeout,
analyzer_only=analyzer_only, develop=develop,
poll_interval=poll_interval, timeout=timeout,
skip_local_compile=skip_local_compile,
)

Expand Down
49 changes: 35 additions & 14 deletions NLPPlus/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,21 @@ def shared_library_ext() -> str:
return ".so"


def _stage_payload(analyzer_dir: Path, stage_dir: Path, kb_only: bool) -> None:
def _stage_payload(analyzer_dir: Path, stage_dir: Path, kb_only: bool,
analyzer_only: bool = False) -> None:
"""Copy run/ + kb/ trees and write the StdAfx.h stub into stage_dir.

Skips run/ when ``kb_only=True``. Only `.cpp` and `.h` files are
copied — the dispatcher's emit-cmake.sh globs both and the engine
emits per-pass headers alongside the .cpp files.
Skips run/ when ``kb_only=True``; skips kb/ when ``analyzer_only=True``.
Only `.cpp` and `.h` files are copied — the dispatcher's emit-cmake.sh
globs both and the engine emits per-pass headers alongside the .cpp
files.
"""
subdirs = ["kb"] if kb_only else ["run", "kb"]
if kb_only:
subdirs = ["kb"]
elif analyzer_only:
subdirs = ["run"]
else:
subdirs = ["run", "kb"]
any_source = False
for sub in subdirs:
src = analyzer_dir / sub
Expand Down Expand Up @@ -305,15 +312,18 @@ def _stage_artifact(
analyzer_dir: Path,
analyzer_name: str,
kb_only: bool,
analyzer_only: bool = False,
) -> Path:
"""Download the cloud artifact and stage it as the bin/ shared libs.

The dispatcher returns ONE shared library per build (either run+kb
fused, or kb-only). We mirror the vscode-nlp extension's staging
behaviour: drop it into ``<analyzer>/bin/`` as both ``run.<ext>`` and
``kb.<ext>`` for the full build (the engine's -COMPILED dlopens both
from there) and as ``kb.<ext>`` alone for ``kb_only``. Returns the
bin/ directory path.
The dispatcher returns ONE shared library per build (run+kb fused,
kb-only, or analyzer-only). We mirror the vscode-nlp extension's
staging behaviour: drop it into ``<analyzer>/bin/`` as both
``run.<ext>`` and ``kb.<ext>`` for the full build (the engine's
-COMPILED dlopens both from there), as ``kb.<ext>`` alone for
``kb_only``, or as ``run.<ext>`` alone for ``analyzer_only``
(leaving any existing ``kb.<ext>`` in place). Returns the bin/
directory path.
"""
ext = shared_library_ext()
bin_dir = analyzer_dir / "bin"
Expand All @@ -326,6 +336,8 @@ def _stage_artifact(
_download(artifact_url, tmp_artifact)
if kb_only:
targets = [f"kb{ext}"]
elif analyzer_only:
targets = [f"run{ext}", f"runu{ext}"]
else:
# vscode-nlp also writes the "u" (unicode) variants on
# Windows; mirror that so the engine's load_compiled finds
Expand All @@ -346,6 +358,7 @@ def cloud_compile(
analyzer_name: str,
dispatcher_url: str = DEFAULT_DISPATCHER_URL,
kb_only: bool = False,
analyzer_only: bool = False,
develop: bool = False,
poll_interval: float = 2.0,
timeout: float = 30 * 60,
Expand All @@ -366,6 +379,8 @@ def cloud_compile(
Worker. Default points at the public dehilster.workers.dev
deployment that the vscode-nlp extension also uses.
kb_only: if True, only the KB is compiled (``run/`` is skipped).
analyzer_only: if True, only the analyzer rules are compiled
(``kb/`` is skipped). Mutually exclusive with ``kb_only``.
develop: forwarded to the local ``-COMPILE`` step.
poll_interval: seconds between ``GET /jobs/<id>`` checks.
timeout: max seconds to wait for the runner build before raising
Expand All @@ -377,9 +392,14 @@ def cloud_compile(
:meth:`Engine.compile`). Useful if you want to re-package and
re-submit without regenerating the .cpp.
"""
if kb_only and analyzer_only:
raise CloudCompileError(
"kb_only and analyzer_only are mutually exclusive"
)
if not skip_local_compile:
analyzer_dir = engine.compile(
analyzer_name, develop=develop, kb_only=kb_only
analyzer_name, develop=develop, kb_only=kb_only,
analyzer_only=analyzer_only,
)
else:
# Mirror Engine.compile's directory resolution without invoking
Expand All @@ -405,7 +425,7 @@ def cloud_compile(

with tempfile.TemporaryDirectory(prefix="nlpplus-cloud-") as stage_str:
stage_dir = Path(stage_str)
_stage_payload(analyzer_dir, stage_dir, kb_only)
_stage_payload(analyzer_dir, stage_dir, kb_only, analyzer_only)
tar_path = stage_dir / "_payload.tar.gz"
_make_tarball(stage_dir, tar_path)
sources_hash = _sha256_file(tar_path)
Expand All @@ -415,6 +435,7 @@ def cloud_compile(
"platform": platform_key,
"analyzerName": analyzer_name,
"kbOnly": kb_only,
"analyzerOnly": analyzer_only,
"sourcesHash": "sha256:" + sources_hash,
"client": "NLPPlus",
}
Expand Down Expand Up @@ -455,7 +476,7 @@ def cloud_compile(
# tarball cleanup happens via TemporaryDirectory.__exit__; now stage
# the artifact into the analyzer's bin/ dir.
bin_dir = _stage_artifact(
artifact_url, analyzer_dir, analyzer_name, kb_only
artifact_url, analyzer_dir, analyzer_name, kb_only, analyzer_only
)
LOGGER.info("Cloud compile output staged into %s", bin_dir)
return bin_dir
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# NLPPlus

**NLP++ lets you build fully customized text analyzers using the [NLP++ VSCode language extension](https://vscode.visualtext.org), giving you 100% visibility into — and complete control over — every rule and decision your analyzer makes. Unlike other NLP packages that are statistical black boxes you cannot inspect or change, every NLP++ analyzer is glass-box code you own and can tailor to your exact needs.**

[![NLP++ Textbook](https://github.com/VisualText/py-package-nlpengine/raw/main/assets/TextbookLaunch01_LinkedIn%20Banner.png)](https://book.visualtext.org)

## First Textbook on the NLP++ Programming Langauge
Expand Down Expand Up @@ -221,21 +223,25 @@ interpreted from the `.nlp` source. See `compile()` and
The analyze function returns a results object that make the analyzer
output files easily accessible to python. (see reults below)

#### compile(analyzer: str = "parse-en-us", develop: bool = False, kb_only: bool = False)
#### compile(analyzer: str = "parse-en-us", develop: bool = False, kb_only: bool = False, analyzer_only: bool = False)
Generates C++ source files for the analyzer by running the engine in
`-COMPILE` mode. The output lands under `<analyzer>/run/*.cpp` and
`<analyzer>/kb/*.cpp` (or just `<analyzer>/kb/*.cpp` if `kb_only=True`).
The generated files still need to be built into shared libraries
before `analyze(..., compiled=True)` can load them — see
`cloud_compile()` for the one-call end-to-end path.

#### cloud_compile(analyzer: str = "parse-en-us", dispatcher_url: Optional[str] = None, kb_only: bool = False, develop: bool = False, poll_interval: float = 2.0, timeout: float = 1800, skip_local_compile: bool = False)
`<analyzer>/kb/*.cpp` — or just `<analyzer>/kb/*.cpp` if `kb_only=True`
(`-COMPILEKB`), or just `<analyzer>/run/*.cpp` if `analyzer_only=True`
(`-COMPILEANA`). Use `analyzer_only=True` when only the rules changed
and the KB is already compiled; `kb_only` and `analyzer_only` are
mutually exclusive. The generated files still need to be built into
shared libraries before `analyze(..., compiled=True)` can load them —
see `cloud_compile()` for the one-call end-to-end path.

#### cloud_compile(analyzer: str = "parse-en-us", dispatcher_url: Optional[str] = None, kb_only: bool = False, analyzer_only: bool = False, develop: bool = False, poll_interval: float = 2.0, timeout: float = 1800, skip_local_compile: bool = False)
End-to-end compile via the public nlp-compile-service cloud build:
runs `compile()` to produce the C++ trees, tars them up, submits to a
Cloudflare-Worker dispatcher, polls the GitHub-Actions runner build,
downloads the resulting shared library and stages it into
`<analyzer>/bin/` as `run.<ext>` + `runu.<ext>` + `kb.<ext>` +
`kbu.<ext>` (or just `kb.<ext>` + `kbu.<ext>` for `kb_only=True`).
`kbu.<ext>` (or just `kb.<ext>` + `kbu.<ext>` for `kb_only=True`, or
just `run.<ext>` + `runu.<ext>` for `analyzer_only=True`).
After it returns, `analyze(..., compiled=True)` will pick up the
staged libraries.

Expand Down
14 changes: 10 additions & 4 deletions bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,18 @@ wrap_analyze(NLP_ENGINE &engine, const std::string &parser,
* `kbOnly=true` switches to KB-only codegen — the analyzer grammar is
* skipped and only `<analyzer>/kb/` is emitted. Matches `init(...,
* compileKB=true)`.
*
* `analyzerOnly=true` switches to analyzer-only codegen — only
* `<analyzer>/run/` is emitted and the KB is left alone. Matches
* `init(..., compileAna=true)`. Mutually exclusive with kbOnly.
*/
void
wrap_compile(NLP_ENGINE &engine, const std::string &analyzer,
const bool develop, const bool kbOnly) {
const bool develop, const bool kbOnly, const bool analyzerOnly) {
_TCHAR *_analyzer = _tcsdup(analyzer.c_str());
engine.init(_analyzer, develop, /*silent*/false,
/*compile*/!kbOnly, /*compiled*/false,
/*compileKB*/kbOnly);
/*compile*/(!kbOnly && !analyzerOnly), /*compiled*/false,
/*compileKB*/kbOnly, /*compileAna*/analyzerOnly);
free(_analyzer);
}

Expand Down Expand Up @@ -115,9 +119,11 @@ NB_MODULE(bindings, m) {
"cloud build step.")
.def("compile", &wrap_compile,
"analyzer"_a, "develop"_a = false, "kbOnly"_a = false,
"analyzerOnly"_a = false,
"Emit C++ source files for `analyzer`.\n"
"Generates <analyzer>/run/*.cpp and <analyzer>/kb/*.cpp\n"
"(or just <analyzer>/kb/*.cpp if `kbOnly=True`). Those\n"
"(or just <analyzer>/kb/*.cpp if `kbOnly=True`, or just\n"
"<analyzer>/run/*.cpp if `analyzerOnly=True`). Those\n"
"still need to be built into shared libraries before\n"
"`analyze(..., compiled=True)` can load them.")
// NLP_ENGINE has two close() overloads: close() and
Expand Down
Loading