From af7eb5bc7a959a7a5ee9d138a47263e4e4bbef14 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 21 Oct 2024 11:52:30 -0500 Subject: [PATCH 1/4] Add improved SciJava Ops Python gateway This commit adds an improved Python gateway contained in the ops-gateway.py file. This gateway resolves a usability bug where nested namespaces like "features.haralick.asm" are unreachable as the string "haralick.asm" is appended as an attribute to the gateway instead of the "haralick" namespace being added as an intermediate namespace. Attempting to access "haralick" fails as that attribute does not exist. Additionally this commit adds the scijava-ops-flim library to the gateway. Intended use cases/scenarios - A user can simply run `python -i ops-gateway.py` to obtain an actiave `ops` environment. - A user can copy the ops-gateway.py module into their own project and create an ops gateway for their own internal project use. --- scijava-ops-tutorial/scripts/ops-gateway.py | 205 ++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 scijava-ops-tutorial/scripts/ops-gateway.py diff --git a/scijava-ops-tutorial/scripts/ops-gateway.py b/scijava-ops-tutorial/scripts/ops-gateway.py new file mode 100644 index 000000000..4c00c4fae --- /dev/null +++ b/scijava-ops-tutorial/scripts/ops-gateway.py @@ -0,0 +1,205 @@ +""" +TODO: Module description. +""" + +from types import MethodType +from typing import List, Sequence +import scyjava as sj + +endpoints = [ + "net.imglib2:imglib2:6.4.0", + "net.imglib2:imglib2-imglyb", + "org.scijava:scijava-ops-engine", + "org.scijava:scijava-ops-flim", + "org.scijava:scijava-ops-image" + ] + +class OpNamespace: + """Op namespace class. + + Represents intermediate Ops categories and Ops. For example, + "math.add" and "features.haralick.asm". + """ + + def __init__(self, env: "scijava.OpEnvironment", ns: str): + self.op = env.op + self._env = env + self._ns = ns + + +class OpsGateway(OpNamespace): + """SciJava Ops Gateway class. + + Contains all other namespaces, in addition to all Ops in + the "global" namespace. + """ + + def __init__(self, env): + super().__init__(env, "global") + + def help(self, op_name: str = None): + """SciJava Ops help. + + :param op_name: + + Namespace and Op name (e.g. "filter.gauss") + """ + if op_name: + print(self._env.help(op_name), sep="\n") + else: + print(self._env.help(), sep="\n") + + def helpVerbose(self, op_name: str = None): + """SciJava Ops verbose help. + + :param op_name: + + Namespace and Op name (e.g. "filter.gauss") + """ + if op_name: + print(self._env.helpVerbose(op_name), sep="\n") + else: + print(self._env_helpVerbose(), sep="\n") + + +def init(endpoints: List[str]) -> OpsGateway: + """Get the SciJava Ops Gateway. + + Initialize the JVM and return an instance of the + SciJava Ops Gateway class. + + :return: + + The SciJava Ops Gateway. + """ + # configure and start the jvm + if not sj.jvm_started(): + sj.config.endpoints = endpoints + sj.start_jvm() + + # build Ops environment + env = sj.jimport("org.scijava.ops.api.OpEnvironment").build() + + # find op names, base namespaces and intermediate namespaces + op_names = _find_op_names(env) + op_base_ns = [] + for op in op_names: + op_sig = op.split(".") + # skip "base" Ops + if len(op_sig) == 1: + continue + else: + op_base_ns.append(op_sig[0]) + op_base_ns = set(op_base_ns) + + # populate base namespaces + for ns in op_base_ns: + _add_namespace(OpsGateway, env, ns) + + # populate nested namespaces and ops + for op in op_names: + op_sig = op.split(".") + sig_size = len(op_sig) + if sig_size > 1: + # find/add nested namespaces + gateway_ref = OpsGateway # used to reference nested namespaces + for s in op_sig[:-1]: + if hasattr(gateway_ref, s): + gateway_ref = getattr(gateway_ref, s) + else: + _add_namespace(gateway_ref, env, s) + gateway_ref = getattr(gateway_ref, s) + # add the Op to the nested namespace + _add_op(gateway_ref, env, op_sig[-1]) + else: + _add_op(OpsGateway, env, op_sig[0]) + + return OpsGateway(env) + +def _add_namespace(gc: OpsGateway, env: "scijava.OpEnvironment", ns: str): + """Add an Op and it's namespace to the OpsGateway. + + Helper method to add an Op call with the appropriate nested + OpNamespace instances if needed. + + :param gc: + + OpsGateway class + + :param env: + + SciJava Ops environment instance + + :param ns: + + Namespace + + :param on: + + Op name + """ + if not hasattr(gc, ns): + setattr(gc, ns, OpNamespace(env, ns)) + + +def _add_op(gc: OpsGateway, env: "scijava.OpEnvironment", on: str): + """Add an Op to the OpsGateway. + + Helper method to add an Op with its corresponding function call + to the given class. + + :param gc: + + OpsGateway class + + :param env: + + SciJava Ops environment instance + + :param on: + + Op name + """ + if hasattr(gc, on): + return + + def f(self, *args, **kwargs): + """Op call instance methods. + + Instance method to attach to the OpNamespace/OpsGateway that does + the actual Op call. + """ + fqop = on if self._ns == "global" else self._ns + "." + on + run = kwargs.get("run", True) + req = env.op(fqop).input(*args) + + # inplace Op requests + if (inplace := kwargs.get("inplace", None)) is not None: + return req.mutate(inplace) if run else req.inplace(inplace) + + # computer Op requests + if (out := kwargs.get("out", None)) is not None: + req = req.output(out) + return req.compute() if run else req.computer() + + # function Op requests + return req.apply() if run else req.function() + + if gc == OpsGateway: + # Op name is a global + setattr(gc, on, f) + else: + m = MethodType(f, gc) + setattr(gc, on, m) + + +def _find_op_names(env: "scijava.OpEnvironment") -> set: + """Find all Op names in a SciJava Ops environment. + + :return: + + Set of all Op names/signatures + """ + return {str(name) for info in env.infos() for name in info.names()} + +ops = init(endpoints) From cec7d39588c168b894c70f54d90627d9a9724ae3 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 21 Oct 2024 12:09:24 -0500 Subject: [PATCH 2/4] Remove old ops Python gateway --- scijava-ops-tutorial/scripts/gateways.py | 123 ---------------------- scijava-ops-tutorial/scripts/ops-setup.py | 20 ---- 2 files changed, 143 deletions(-) delete mode 100644 scijava-ops-tutorial/scripts/gateways.py delete mode 100644 scijava-ops-tutorial/scripts/ops-setup.py diff --git a/scijava-ops-tutorial/scripts/gateways.py b/scijava-ops-tutorial/scripts/gateways.py deleted file mode 100644 index 24e77365e..000000000 --- a/scijava-ops-tutorial/scripts/gateways.py +++ /dev/null @@ -1,123 +0,0 @@ -''' -Create namespace convenience methods for all discovered ops. -Assumes we're running in an environment with scyjava configured to have a classpath with some SciJava Ops implementations (i.e. org.scijava:scijava-ops-engine plus implementations) - -Classes: - OpNamespace - OpGateway - -Variables: - env - OpEnvironment used to populate the OpGateway and OpNamespaces - ops - OpGateway entry point -''' - -from scyjava import jimport -from types import MethodType -OpEnvironment = jimport('org.scijava.ops.api.OpEnvironment') -env = OpEnvironment.build() - -op_names={str(name) for info in env.infos() for name in info.names()} - -class OpNamespace: - ''' - Represents intermediate ops categories. For example, "math.add" and "math.sub" are in both in the "math" namespace. - ''' - def __init__(self, env, ns): - self.env = env - self.ns = ns - - def help(self, op_name=None): - ''' - Convenience wrapper for OpEnvironment.help(), for information about available ops. Prints all returned information, line-by-line. - ''' - print(*self.env.help(op_name), sep = "\n") - -class OpGateway(OpNamespace): - ''' - Global base specialization of OpNamespace. Contains all other namespaces, in addition to all ops in the "global" namespace. - ''' - def __init__(self, env): - super().__init__(env, 'global') - -def nary(env, fqop, arity): - ''' - Helper method to convert a numerical arity to the corresponding OpEnvironment method - - Parameters: - env (OpEnvironment): the environment being used - fqop (str): the fully-qualified op name - arity (int): the desired arity of the op - - Returns: - The appropriate OpBuilder.Arity instance for invoking the given op with the indicated number of parameters - ''' - arities=[env.op, env.op, env.op, env.op, env.op, env.op, env.op, env.op, env.op, env.op, env.op, env.arity11, env.arity12, env.arity13, env.arity14, env.arity15, env.arity16] - return arities[arity](fqop) - -def add_op(c, op_name): - ''' - Helper method patch in a given op name as its corresponding function call within the given class - - Parameters: - c (class): the OpNamespace/OpGateway to add a new function with the given op_name that calls the op - op_name (str): the actual name of the op we're adding - ''' - if hasattr(c, op_name): - return - - def f(self, *args, **kwargs): - ''' - Instance method to attach to our OpNamespace/OpGateway that does the actual op call - ''' - fqop = op_name if self.ns == 'global' else self.ns + "." + op_name - run = kwargs.get('run', True) - b = nary(self.env, fqop, len(args)).input(*args) - # inplace - # ops.filter.gauss(image, 5, inplace=0) - if (inplace:=kwargs.get('inplace', None)) is not None: - return b.mutate(inplace) if run else b.inplace(inplace) - - # computer - # ops.filter.gauss(image, 5, out=result) - if (out:=kwargs.get('out', None)) is not None: - b=b.output(out) - return b.compute() if run else b.computer() - - # function - # gauss_op = ops.filter.gauss(image, 5) - # result = ops.filter.gauss(image, 5) - return b.apply() if run else b.function() - - if c == OpGateway: - # op_name is a global. - setattr(c, op_name, f) - else: - m=MethodType(f, c) - setattr(c, op_name, m) - -def add_namespace(c, ns, op_name): - ''' - Helper method to add an op call with nested OpNamespace instances if needed - - Parameters: - c (class): the OpNamespace/OpGateway to add the given op and namespace - ns(str): the namespace to nest the given op within - op_name (str): the actual name of the op we're adding - ''' - if not hasattr(c, ns): - setattr(c, ns, OpNamespace(env, ns)) - add_op(getattr(c, ns), op_name) - -# Entry point to do the namespace population. -# This modifies the OpGateway class to add a call for each op, with intermediate accessors by namespace -for op in op_names: - dot = op.find('.') - if dot >= 0: - ns=op[:dot] - op_name=op[dot+1:] - add_namespace(OpGateway, ns, op_name) - else: - add_op(OpGateway, op) - -# Make an instance of our modified OpGateway class as a global for external use -ops = OpGateway(env) diff --git a/scijava-ops-tutorial/scripts/ops-setup.py b/scijava-ops-tutorial/scripts/ops-setup.py deleted file mode 100644 index a2b5e745a..000000000 --- a/scijava-ops-tutorial/scripts/ops-setup.py +++ /dev/null @@ -1,20 +0,0 @@ -''' -Python entry point script for using SciJava Ops in python. After running this script -you will have an "ops" variable that can be used to call ops, and explore available ops. - -For example: - ops.math.add(27, 15) - ops.filter.addPoissonNoise(img) - -Variables: - ops - fully intialized OpGateway with all SciJava and ImageJ Ops available - env - OpEnvironment for more traditional Java-style API -''' - -from scyjava import config, jimport -config.endpoints.append('org.scijava:scijava-ops-tutorial:1.0.0') - -import gateways as g - -ops = g.ops -env = g.env From bbedba2859bfe3d4e669d4f54e01d81ff41a8d06 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 21 Oct 2024 17:01:09 -0500 Subject: [PATCH 3/4] Use a underscore instead of hyphen in module name The hyphen is not pythonic. --- .../{ops-gateway.py => ops_gateway.py} | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) rename scijava-ops-tutorial/scripts/{ops-gateway.py => ops_gateway.py} (94%) diff --git a/scijava-ops-tutorial/scripts/ops-gateway.py b/scijava-ops-tutorial/scripts/ops_gateway.py similarity index 94% rename from scijava-ops-tutorial/scripts/ops-gateway.py rename to scijava-ops-tutorial/scripts/ops_gateway.py index 4c00c4fae..af6932958 100644 --- a/scijava-ops-tutorial/scripts/ops-gateway.py +++ b/scijava-ops-tutorial/scripts/ops_gateway.py @@ -1,5 +1,28 @@ """ -TODO: Module description. +Classes +------- + + - OpsNamespace + - OpsGateway + +Functions +--------- + + - init + +Variables +--------- + + - `ops` + +Example +------- + + - Interactive Python session: + python -i ops-gateway.py + - Module import: + >>> import ops-gateway + >>> ops = ops-gateway.init() """ from types import MethodType From 759d523414276fcb054ef061cc73deff4245ded5 Mon Sep 17 00:00:00 2001 From: Edward Evans Date: Mon, 21 Oct 2024 17:37:24 -0500 Subject: [PATCH 4/4] Prevent ops gateway initialization on import The SciJava Ops gateway should only be initialized if the ops_gateway.py file is run as a script. The module should be importable without creating the gateway. --- scijava-ops-tutorial/scripts/ops_gateway.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scijava-ops-tutorial/scripts/ops_gateway.py b/scijava-ops-tutorial/scripts/ops_gateway.py index af6932958..f8fa940cc 100644 --- a/scijava-ops-tutorial/scripts/ops_gateway.py +++ b/scijava-ops-tutorial/scripts/ops_gateway.py @@ -225,4 +225,5 @@ def _find_op_names(env: "scijava.OpEnvironment") -> set: """ return {str(name) for info in env.infos() for name in info.names()} -ops = init(endpoints) +if __name__ == "__main__": + ops = init(endpoints)