From 0bed008b654f779c55beccff85a3e361afc5b761 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Tue, 16 Dec 2025 17:50:26 -0800 Subject: [PATCH 01/10] first mv: py --- pyproject.toml => py/pyproject.toml | 0 .../requirements-dev.txt | 0 requirements.txt => py/requirements.txt | 0 {src => py/src}/clippy/__init__.py | 0 {src => py/src}/clippy/backends/__init__.py | 0 .../src}/clippy/backends/fs/__init__.py | 0 {src => py/src}/clippy/backends/fs/config.py | 0 .../src}/clippy/backends/fs/constants.py | 0 .../src}/clippy/backends/fs/execution.py | 0 py/src/clippy/backends/monolith/__init__.py | 323 ++++++++++++++++++ py/src/clippy/backends/monolith/config.py | 9 + py/src/clippy/backends/monolith/constants.py | 18 + .../src}/clippy/backends/serialization.py | 0 {src => py/src}/clippy/backends/version.py | 0 {src => py/src}/clippy/clippy_types.py | 0 {src => py/src}/clippy/config.py | 0 {src => py/src}/clippy/constants.py | 0 {src => py/src}/clippy/error.py | 0 {src => py/src}/clippy/selectors.py | 0 {src => py/src}/clippy/utils.py | 0 {test => py/test}/conftest.py | 0 {test => py/test}/test_clippy.py | 0 22 files changed, 350 insertions(+) rename pyproject.toml => py/pyproject.toml (100%) rename requirements-dev.txt => py/requirements-dev.txt (100%) rename requirements.txt => py/requirements.txt (100%) rename {src => py/src}/clippy/__init__.py (100%) rename {src => py/src}/clippy/backends/__init__.py (100%) rename {src => py/src}/clippy/backends/fs/__init__.py (100%) rename {src => py/src}/clippy/backends/fs/config.py (100%) rename {src => py/src}/clippy/backends/fs/constants.py (100%) rename {src => py/src}/clippy/backends/fs/execution.py (100%) create mode 100644 py/src/clippy/backends/monolith/__init__.py create mode 100644 py/src/clippy/backends/monolith/config.py create mode 100644 py/src/clippy/backends/monolith/constants.py rename {src => py/src}/clippy/backends/serialization.py (100%) rename {src => py/src}/clippy/backends/version.py (100%) rename {src => py/src}/clippy/clippy_types.py (100%) rename {src => py/src}/clippy/config.py (100%) rename {src => py/src}/clippy/constants.py (100%) rename {src => py/src}/clippy/error.py (100%) rename {src => py/src}/clippy/selectors.py (100%) rename {src => py/src}/clippy/utils.py (100%) rename {test => py/test}/conftest.py (100%) rename {test => py/test}/test_clippy.py (100%) diff --git a/pyproject.toml b/py/pyproject.toml similarity index 100% rename from pyproject.toml rename to py/pyproject.toml diff --git a/requirements-dev.txt b/py/requirements-dev.txt similarity index 100% rename from requirements-dev.txt rename to py/requirements-dev.txt diff --git a/requirements.txt b/py/requirements.txt similarity index 100% rename from requirements.txt rename to py/requirements.txt diff --git a/src/clippy/__init__.py b/py/src/clippy/__init__.py similarity index 100% rename from src/clippy/__init__.py rename to py/src/clippy/__init__.py diff --git a/src/clippy/backends/__init__.py b/py/src/clippy/backends/__init__.py similarity index 100% rename from src/clippy/backends/__init__.py rename to py/src/clippy/backends/__init__.py diff --git a/src/clippy/backends/fs/__init__.py b/py/src/clippy/backends/fs/__init__.py similarity index 100% rename from src/clippy/backends/fs/__init__.py rename to py/src/clippy/backends/fs/__init__.py diff --git a/src/clippy/backends/fs/config.py b/py/src/clippy/backends/fs/config.py similarity index 100% rename from src/clippy/backends/fs/config.py rename to py/src/clippy/backends/fs/config.py diff --git a/src/clippy/backends/fs/constants.py b/py/src/clippy/backends/fs/constants.py similarity index 100% rename from src/clippy/backends/fs/constants.py rename to py/src/clippy/backends/fs/constants.py diff --git a/src/clippy/backends/fs/execution.py b/py/src/clippy/backends/fs/execution.py similarity index 100% rename from src/clippy/backends/fs/execution.py rename to py/src/clippy/backends/fs/execution.py diff --git a/py/src/clippy/backends/monolith/__init__.py b/py/src/clippy/backends/monolith/__init__.py new file mode 100644 index 0000000..c1dcec3 --- /dev/null +++ b/py/src/clippy/backends/monolith/__init__.py @@ -0,0 +1,323 @@ +"""Monolith backend for clippy.""" + +from __future__ import annotations + +import os +import stat +import json +import sys +import pathlib +import logging +import select +import subprocess +from typing import Any + + +from .execution import _validate, _run, _help +from ..version import _check_version +from ..serialization import ClippySerializable + +from ... import constants +from . import constants as local_constants + +from ...error import ( + ClippyConfigurationError, + ClippyTypeError, + ClippyValidationError, + ClippyInvalidSelectorError, +) +from ...selectors import Selector +from ...utils import flat_dict_to_nested + +from ...clippy_types import CLIPPY_CONFIG +from .config import _monolith_config_entries + +# create a fs-specific configuration. +cfg = CLIPPY_CONFIG(_monolith_config_entries) + +PATH = sys.path[0] + + +def _is_user_executable(path: pathlib.Path) -> bool: + # Must be a regular file + if not os.path.isfile(path): + return False + + st = os.stat(path) + mode = st.st_mode + uid = os.getuid() + gid = os.getgid() + + # Owner permissions + if st.st_uid == uid and mode & stat.S_IXUSR: + return True + # Group permissions + elif st.st_gid == gid and mode & stat.S_IXGRP: + return True + # Other permissions + elif mode & stat.S_IXOTH: + return True + + return False + + +def get_cfg() -> CLIPPY_CONFIG: + """This is a mandatory function for all backends. It returns the backend-specific configuration.""" + return cfg + + +def send_cmd( + p: subprocess.Popen, cmd: tuple[str, str], args: dict = {} +) -> tuple[dict, str]: + """Sends a command with optional args and blocks until return of a dict of JSON-response output. + The command is the first element in the tuple. + """ + + assert p.stdin is not None + assert p.stdout is not None + send_d = {"cmd": cmd[0]} + send_d.update(args) + + send_j = json.dumps(send_d) + p.stdin.write(send_j + "\n") + p.stdin.flush() + + # Wait for response + readable, _, _ = select.select( + [p.stdout, p.stderr], [], [], local_constants.SELECT_TIMEOUT + ) + if p.stderr in readable: # return the stderr and exit. + assert p.stderr is not None + error = p.stderr.readline() + return ({}, error) + + if p.stdout not in readable: + return ({}, "Timeout waiting for response") + + # Read STATUS_START + start_line = p.stdout.readline() + try: + start_status = json.loads(start_line) + if start_status != local_constants.STATUS_START: + return ({}, f"Expected STATUS_START, got: {start_status}") + except json.JSONDecodeError: + return ({}, f"Invalid JSON for STATUS_START: {start_line}") + + # Read zero or more dictionary lines until STATUS_END + results = [] + while True: + line = p.stdout.readline() + if not line: + return ({}, "Unexpected EOF while reading response") + + try: + data = json.loads(line) + except json.JSONDecodeError: + return ({}, f"Invalid JSON in response: {line}") + + # Check if this is STATUS_END + if data == local_constants.STATUS_END: + break + + # Check if this is STATUS_UPDATE with a message + if data.get(local_constants.STATUS_KEY) == "update": + if "message" in data: + print(data["message"]) + continue + + # Otherwise, it's a result dictionary + results.append(data) + + # Package results into response dictionary + recv_d = {local_constants.RESULTS_KEY: results} + + # Check for any stderr messages (non-blocking) + readable, _, _ = select.select([p.stderr], [], [], 0) # 0 timeout = immediate + if p.stderr in readable: + assert p.stderr is not None + error = p.stderr.readline() + return ({}, error) + + return (recv_d, "") + + +def classes() -> dict[str, Any]: + """This is a mandatory function for all backends. It returns a dictionary of class name + to the actual Class for all classes supported by the backend.""" + from ... import cfg as topcfg # pylint: disable=import-outside-toplevel + + _classes = {} + monolith_exe = cfg.get("monolith_exe") + if not _is_user_executable(monolith_exe): + raise ClippyConfigurationError("File is not executable: ", monolith_exe) + + p = subprocess.Popen( + [ + monolith_exe, + ], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, # Decodes streams as text + ) + + # Wait for STATUS_READY from the subprocess + assert p.stdout is not None + assert p.stderr is not None + readable, _, _ = select.select( + [p.stdout, p.stderr], [], [], local_constants.READY_TIMEOUT + ) + + if p.stderr in readable: + error = p.stderr.readline() + raise ClippyConfigurationError(f"Error starting monolith: {error}") + + if p.stdout not in readable: + raise ClippyConfigurationError( + f"{monolith_exe} did not send ready status within {local_constants.READY_TIMEOUT} seconds" + ) + + status_line = p.stdout.readline() + try: + status = json.loads(status_line) + if status != local_constants.STATUS_READY: + raise ClippyConfigurationError(f"Expected STATUS_READY, got: {status}") + except json.JSONDecodeError as e: + raise ClippyConfigurationError( + f"Invalid JSON from {monolith_exe}: {status_line}" + ) from e + + path = pathlib.Path(monolith_exe).parent + + class_result, class_error = send_cmd(p, local_constants.CLASSES) + + class_dict = class_result.get(local_constants.RESULTS_KEY, {}) + # this is a dict of classname: classdata + for class_name, class_data in class_dict: + _classes[class_name] = _create_class(p, class_name, class_data, topcfg) + + return _classes + + +def _create_class( + p: subprocess.Popen, class_name: str, class_data: dict, topcfg: CLIPPY_CONFIG +): + """Given a dictionary of class data and a master configuration, + create a class with the given name, and add methods based on the + class_data. Set convenience fields (_name, _cfg) as well""" + + # pull the selectors out since we don't want them in the class definition right now + selectors = class_data.pop(constants.INITIAL_SELECTOR_KEY, {}) + methods = class_data.pop(local_constants.CLASS_METHOD_KEY, {}) + class_data["_name"] = class_name + class_data["_cfg"] = topcfg + class_data["_p"] = p + class_logger = logging.getLogger(topcfg.get("logname") + "." + class_name) + class_logger.setLevel(topcfg.get("loglevel")) + class_data["logger"] = class_logger + + cls = type(class_name, (ClippySerializable,), class_data) + + # add the methods + for method, method_meta in methods: + docstring = method_meta.get(constants.DOCSTRING_KEY, "") + args = method_meta.get(constants.ARGS_KEY, {}) + + if hasattr(cls, method) and not method.startswith("__"): + assert hasattr(cls, "logger"), "Class must have a logger attribute" + logger = getattr(cls, "logger") + logger.warning( + f"Overwriting existing method {method} for class {cls} with executable {executable}" + ) + + _define_method(cls, method, executable, docstring, args) + + # add the selectors + # this should be in the meta.json file. + for selector, docstr in selectors.items(): + class_logger.debug("adding %s to class", selector) + setattr(cls, selector, Selector(None, selector, docstr)) + return cls + + +def _define_method( + cls, name: str, docstr: str, arguments: list[str] | None +): # pylint: disable=too-complex + """Defines a method on a given class.""" + + def m(self, *args, **kwargs): + """ + Generic Method that calls an executable with specified arguments + """ + + # special cases for __init__ + # call the superclass to initialize the _state + if name == "__init__": + super(cls, self).__init__() + + argdict = {} + # statej = {} + + # make json from args and state + + # .. add state + # argdict[STATE_KEY] = self._state + argdict[constants.STATE_KEY] = getattr(self, constants.STATE_KEY) + # ~ for key in statedesc: + # ~ statej[key] = getattr(self, key) + + # .. add positional arguments + numpositionals = len(args) + for argdesc in arguments: + value = arguments[argdesc] + if "position" in value: + if 0 <= value["position"] < numpositionals: + argdict[argdesc] = args[value["position"]] + + # .. add keyword arguments + argdict.update(kwargs) + + # call executable and create json output + outj = _run(executable, argdict, self.logger) + + # if we have results that have keys that are in our + # kwargs, let's update the kwarg references. Works + # for lists and dicts only. + for kw, kwval in kwargs.items(): + if kw in outj.get(constants.REFERENCE_KEY, {}): + kwval.clear() + if isinstance(kwval, dict): + kwval.update(outj[kw]) + elif isinstance(kwval, list): + kwval += outj[kw] + else: + raise ClippyTypeError() + + # dump any output + if constants.OUTPUT_KEY in outj: + print(outj[constants.OUTPUT_KEY]) + # update state according to json output + if constants.STATE_KEY in outj: + setattr(self, constants.STATE_KEY, outj[constants.STATE_KEY]) + + # update selectors if necessary. + if constants.SELECTOR_KEY in outj: + d = flat_dict_to_nested(outj[constants.SELECTOR_KEY]) + for topsel, subsels in d.items(): + if not hasattr(self, topsel): + raise ClippyInvalidSelectorError( + f"selector {topsel} not found in class; aborting" + ) + getattr(self, topsel)._import_from_dict(subsels) + + # return result + if outj.get(constants.SELF_KEY, False): + return self + return outj.get(constants.RETURN_KEY) + + # end of nested def m + + # Add a new member function with name and implementation m to the class cls + # setattr(name, '__doc__', docstr) + m.__doc__ = docstr + setattr(cls, name, m) diff --git a/py/src/clippy/backends/monolith/config.py b/py/src/clippy/backends/monolith/config.py new file mode 100644 index 0000000..1f85ff4 --- /dev/null +++ b/py/src/clippy/backends/monolith/config.py @@ -0,0 +1,9 @@ +# pylint: disable=consider-using-namedtuple-or-dataclass +import os + +_monolith_config_entries = { + # backend path for executables, in addition to the CLIPPY_BACKEND_PATH environment variable. + # Add to it here. + # TODO: support multiple executables, one per class? + "monolith_exe": os.environ.get("CLIPPY_MONOLITH_EXE", "") +} diff --git a/py/src/clippy/backends/monolith/constants.py b/py/src/clippy/backends/monolith/constants.py new file mode 100644 index 0000000..3651a59 --- /dev/null +++ b/py/src/clippy/backends/monolith/constants.py @@ -0,0 +1,18 @@ +# Constants unique to the monolith backend. +# the flag to pass for a dry run to make sure syntax is proper + +# format is command and key in return dict +CLASSES = ("_getclasses", "_classes") + +STATUS_KEY = "_status" +STATUS_READY = {STATUS_KEY: "ready"} +READY_TIMEOUT = 5.0 # seconds +STATUS_START = {STATUS_KEY: "start"} +STATUS_END = {STATUS_KEY: "end"} +STATUS_UPDATE = {STATUS_KEY: "update"} + +SELECT_TIMEOUT = 2.0 # seconds +RESULTS_KEY = "results" +CLASS_METHOD_KEY = "methods" +CLASS_NAME_KEY = "name" +CLASS_DOCSTR = "doc" diff --git a/src/clippy/backends/serialization.py b/py/src/clippy/backends/serialization.py similarity index 100% rename from src/clippy/backends/serialization.py rename to py/src/clippy/backends/serialization.py diff --git a/src/clippy/backends/version.py b/py/src/clippy/backends/version.py similarity index 100% rename from src/clippy/backends/version.py rename to py/src/clippy/backends/version.py diff --git a/src/clippy/clippy_types.py b/py/src/clippy/clippy_types.py similarity index 100% rename from src/clippy/clippy_types.py rename to py/src/clippy/clippy_types.py diff --git a/src/clippy/config.py b/py/src/clippy/config.py similarity index 100% rename from src/clippy/config.py rename to py/src/clippy/config.py diff --git a/src/clippy/constants.py b/py/src/clippy/constants.py similarity index 100% rename from src/clippy/constants.py rename to py/src/clippy/constants.py diff --git a/src/clippy/error.py b/py/src/clippy/error.py similarity index 100% rename from src/clippy/error.py rename to py/src/clippy/error.py diff --git a/src/clippy/selectors.py b/py/src/clippy/selectors.py similarity index 100% rename from src/clippy/selectors.py rename to py/src/clippy/selectors.py diff --git a/src/clippy/utils.py b/py/src/clippy/utils.py similarity index 100% rename from src/clippy/utils.py rename to py/src/clippy/utils.py diff --git a/test/conftest.py b/py/test/conftest.py similarity index 100% rename from test/conftest.py rename to py/test/conftest.py diff --git a/test/test_clippy.py b/py/test/test_clippy.py similarity index 100% rename from test/test_clippy.py rename to py/test/test_clippy.py From 8cfb80d1f73e0c20ac63c78852f31eb1724ff5df Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Tue, 16 Dec 2025 17:58:12 -0800 Subject: [PATCH 02/10] update requirements --- py/requirements-dev.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/py/requirements-dev.txt b/py/requirements-dev.txt index 4cc3f4a..25946d0 100644 --- a/py/requirements-dev.txt +++ b/py/requirements-dev.txt @@ -1,7 +1,7 @@ -coverage>=5.5 +coverage flake8 mypy -pylint>=2.15,<3 -pyproj>=3.6,<4 -pytest>=7,<8 +pylint +pyproj +pytest json-logic-qubit From c743501dd102b6599e6ee0f2542ad6f03cddd7a3 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Tue, 16 Dec 2025 18:01:30 -0800 Subject: [PATCH 03/10] cleanup --- .gitignore | 2 + py/src/clippy/backends/monolith/__init__.py | 323 ------------------- py/src/clippy/backends/monolith/config.py | 9 - py/src/clippy/backends/monolith/constants.py | 18 -- 4 files changed, 2 insertions(+), 350 deletions(-) delete mode 100644 py/src/clippy/backends/monolith/__init__.py delete mode 100644 py/src/clippy/backends/monolith/config.py delete mode 100644 py/src/clippy/backends/monolith/constants.py diff --git a/.gitignore b/.gitignore index e2bc832..28ce1bb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ setup.cfg .vscode *.egg-info .coverage +*.tgz +**/attic diff --git a/py/src/clippy/backends/monolith/__init__.py b/py/src/clippy/backends/monolith/__init__.py deleted file mode 100644 index c1dcec3..0000000 --- a/py/src/clippy/backends/monolith/__init__.py +++ /dev/null @@ -1,323 +0,0 @@ -"""Monolith backend for clippy.""" - -from __future__ import annotations - -import os -import stat -import json -import sys -import pathlib -import logging -import select -import subprocess -from typing import Any - - -from .execution import _validate, _run, _help -from ..version import _check_version -from ..serialization import ClippySerializable - -from ... import constants -from . import constants as local_constants - -from ...error import ( - ClippyConfigurationError, - ClippyTypeError, - ClippyValidationError, - ClippyInvalidSelectorError, -) -from ...selectors import Selector -from ...utils import flat_dict_to_nested - -from ...clippy_types import CLIPPY_CONFIG -from .config import _monolith_config_entries - -# create a fs-specific configuration. -cfg = CLIPPY_CONFIG(_monolith_config_entries) - -PATH = sys.path[0] - - -def _is_user_executable(path: pathlib.Path) -> bool: - # Must be a regular file - if not os.path.isfile(path): - return False - - st = os.stat(path) - mode = st.st_mode - uid = os.getuid() - gid = os.getgid() - - # Owner permissions - if st.st_uid == uid and mode & stat.S_IXUSR: - return True - # Group permissions - elif st.st_gid == gid and mode & stat.S_IXGRP: - return True - # Other permissions - elif mode & stat.S_IXOTH: - return True - - return False - - -def get_cfg() -> CLIPPY_CONFIG: - """This is a mandatory function for all backends. It returns the backend-specific configuration.""" - return cfg - - -def send_cmd( - p: subprocess.Popen, cmd: tuple[str, str], args: dict = {} -) -> tuple[dict, str]: - """Sends a command with optional args and blocks until return of a dict of JSON-response output. - The command is the first element in the tuple. - """ - - assert p.stdin is not None - assert p.stdout is not None - send_d = {"cmd": cmd[0]} - send_d.update(args) - - send_j = json.dumps(send_d) - p.stdin.write(send_j + "\n") - p.stdin.flush() - - # Wait for response - readable, _, _ = select.select( - [p.stdout, p.stderr], [], [], local_constants.SELECT_TIMEOUT - ) - if p.stderr in readable: # return the stderr and exit. - assert p.stderr is not None - error = p.stderr.readline() - return ({}, error) - - if p.stdout not in readable: - return ({}, "Timeout waiting for response") - - # Read STATUS_START - start_line = p.stdout.readline() - try: - start_status = json.loads(start_line) - if start_status != local_constants.STATUS_START: - return ({}, f"Expected STATUS_START, got: {start_status}") - except json.JSONDecodeError: - return ({}, f"Invalid JSON for STATUS_START: {start_line}") - - # Read zero or more dictionary lines until STATUS_END - results = [] - while True: - line = p.stdout.readline() - if not line: - return ({}, "Unexpected EOF while reading response") - - try: - data = json.loads(line) - except json.JSONDecodeError: - return ({}, f"Invalid JSON in response: {line}") - - # Check if this is STATUS_END - if data == local_constants.STATUS_END: - break - - # Check if this is STATUS_UPDATE with a message - if data.get(local_constants.STATUS_KEY) == "update": - if "message" in data: - print(data["message"]) - continue - - # Otherwise, it's a result dictionary - results.append(data) - - # Package results into response dictionary - recv_d = {local_constants.RESULTS_KEY: results} - - # Check for any stderr messages (non-blocking) - readable, _, _ = select.select([p.stderr], [], [], 0) # 0 timeout = immediate - if p.stderr in readable: - assert p.stderr is not None - error = p.stderr.readline() - return ({}, error) - - return (recv_d, "") - - -def classes() -> dict[str, Any]: - """This is a mandatory function for all backends. It returns a dictionary of class name - to the actual Class for all classes supported by the backend.""" - from ... import cfg as topcfg # pylint: disable=import-outside-toplevel - - _classes = {} - monolith_exe = cfg.get("monolith_exe") - if not _is_user_executable(monolith_exe): - raise ClippyConfigurationError("File is not executable: ", monolith_exe) - - p = subprocess.Popen( - [ - monolith_exe, - ], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, # Decodes streams as text - ) - - # Wait for STATUS_READY from the subprocess - assert p.stdout is not None - assert p.stderr is not None - readable, _, _ = select.select( - [p.stdout, p.stderr], [], [], local_constants.READY_TIMEOUT - ) - - if p.stderr in readable: - error = p.stderr.readline() - raise ClippyConfigurationError(f"Error starting monolith: {error}") - - if p.stdout not in readable: - raise ClippyConfigurationError( - f"{monolith_exe} did not send ready status within {local_constants.READY_TIMEOUT} seconds" - ) - - status_line = p.stdout.readline() - try: - status = json.loads(status_line) - if status != local_constants.STATUS_READY: - raise ClippyConfigurationError(f"Expected STATUS_READY, got: {status}") - except json.JSONDecodeError as e: - raise ClippyConfigurationError( - f"Invalid JSON from {monolith_exe}: {status_line}" - ) from e - - path = pathlib.Path(monolith_exe).parent - - class_result, class_error = send_cmd(p, local_constants.CLASSES) - - class_dict = class_result.get(local_constants.RESULTS_KEY, {}) - # this is a dict of classname: classdata - for class_name, class_data in class_dict: - _classes[class_name] = _create_class(p, class_name, class_data, topcfg) - - return _classes - - -def _create_class( - p: subprocess.Popen, class_name: str, class_data: dict, topcfg: CLIPPY_CONFIG -): - """Given a dictionary of class data and a master configuration, - create a class with the given name, and add methods based on the - class_data. Set convenience fields (_name, _cfg) as well""" - - # pull the selectors out since we don't want them in the class definition right now - selectors = class_data.pop(constants.INITIAL_SELECTOR_KEY, {}) - methods = class_data.pop(local_constants.CLASS_METHOD_KEY, {}) - class_data["_name"] = class_name - class_data["_cfg"] = topcfg - class_data["_p"] = p - class_logger = logging.getLogger(topcfg.get("logname") + "." + class_name) - class_logger.setLevel(topcfg.get("loglevel")) - class_data["logger"] = class_logger - - cls = type(class_name, (ClippySerializable,), class_data) - - # add the methods - for method, method_meta in methods: - docstring = method_meta.get(constants.DOCSTRING_KEY, "") - args = method_meta.get(constants.ARGS_KEY, {}) - - if hasattr(cls, method) and not method.startswith("__"): - assert hasattr(cls, "logger"), "Class must have a logger attribute" - logger = getattr(cls, "logger") - logger.warning( - f"Overwriting existing method {method} for class {cls} with executable {executable}" - ) - - _define_method(cls, method, executable, docstring, args) - - # add the selectors - # this should be in the meta.json file. - for selector, docstr in selectors.items(): - class_logger.debug("adding %s to class", selector) - setattr(cls, selector, Selector(None, selector, docstr)) - return cls - - -def _define_method( - cls, name: str, docstr: str, arguments: list[str] | None -): # pylint: disable=too-complex - """Defines a method on a given class.""" - - def m(self, *args, **kwargs): - """ - Generic Method that calls an executable with specified arguments - """ - - # special cases for __init__ - # call the superclass to initialize the _state - if name == "__init__": - super(cls, self).__init__() - - argdict = {} - # statej = {} - - # make json from args and state - - # .. add state - # argdict[STATE_KEY] = self._state - argdict[constants.STATE_KEY] = getattr(self, constants.STATE_KEY) - # ~ for key in statedesc: - # ~ statej[key] = getattr(self, key) - - # .. add positional arguments - numpositionals = len(args) - for argdesc in arguments: - value = arguments[argdesc] - if "position" in value: - if 0 <= value["position"] < numpositionals: - argdict[argdesc] = args[value["position"]] - - # .. add keyword arguments - argdict.update(kwargs) - - # call executable and create json output - outj = _run(executable, argdict, self.logger) - - # if we have results that have keys that are in our - # kwargs, let's update the kwarg references. Works - # for lists and dicts only. - for kw, kwval in kwargs.items(): - if kw in outj.get(constants.REFERENCE_KEY, {}): - kwval.clear() - if isinstance(kwval, dict): - kwval.update(outj[kw]) - elif isinstance(kwval, list): - kwval += outj[kw] - else: - raise ClippyTypeError() - - # dump any output - if constants.OUTPUT_KEY in outj: - print(outj[constants.OUTPUT_KEY]) - # update state according to json output - if constants.STATE_KEY in outj: - setattr(self, constants.STATE_KEY, outj[constants.STATE_KEY]) - - # update selectors if necessary. - if constants.SELECTOR_KEY in outj: - d = flat_dict_to_nested(outj[constants.SELECTOR_KEY]) - for topsel, subsels in d.items(): - if not hasattr(self, topsel): - raise ClippyInvalidSelectorError( - f"selector {topsel} not found in class; aborting" - ) - getattr(self, topsel)._import_from_dict(subsels) - - # return result - if outj.get(constants.SELF_KEY, False): - return self - return outj.get(constants.RETURN_KEY) - - # end of nested def m - - # Add a new member function with name and implementation m to the class cls - # setattr(name, '__doc__', docstr) - m.__doc__ = docstr - setattr(cls, name, m) diff --git a/py/src/clippy/backends/monolith/config.py b/py/src/clippy/backends/monolith/config.py deleted file mode 100644 index 1f85ff4..0000000 --- a/py/src/clippy/backends/monolith/config.py +++ /dev/null @@ -1,9 +0,0 @@ -# pylint: disable=consider-using-namedtuple-or-dataclass -import os - -_monolith_config_entries = { - # backend path for executables, in addition to the CLIPPY_BACKEND_PATH environment variable. - # Add to it here. - # TODO: support multiple executables, one per class? - "monolith_exe": os.environ.get("CLIPPY_MONOLITH_EXE", "") -} diff --git a/py/src/clippy/backends/monolith/constants.py b/py/src/clippy/backends/monolith/constants.py deleted file mode 100644 index 3651a59..0000000 --- a/py/src/clippy/backends/monolith/constants.py +++ /dev/null @@ -1,18 +0,0 @@ -# Constants unique to the monolith backend. -# the flag to pass for a dry run to make sure syntax is proper - -# format is command and key in return dict -CLASSES = ("_getclasses", "_classes") - -STATUS_KEY = "_status" -STATUS_READY = {STATUS_KEY: "ready"} -READY_TIMEOUT = 5.0 # seconds -STATUS_START = {STATUS_KEY: "start"} -STATUS_END = {STATUS_KEY: "end"} -STATUS_UPDATE = {STATUS_KEY: "update"} - -SELECT_TIMEOUT = 2.0 # seconds -RESULTS_KEY = "results" -CLASS_METHOD_KEY = "methods" -CLASS_NAME_KEY = "name" -CLASS_DOCSTR = "doc" From 9a14b507de8da2c8746ef65be56f134e2de3f3fc Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Tue, 16 Dec 2025 18:53:02 -0800 Subject: [PATCH 04/10] ruff linting --- py/pyproject.toml | 12 +++++ py/requirements-dev.txt | 4 +- py/src/clippy/backends/fs/__init__.py | 64 ++++++++++++------------- py/src/clippy/backends/fs/constants.py | 6 --- py/src/clippy/backends/fs/execution.py | 15 +++--- py/src/clippy/backends/serialization.py | 8 ++-- py/src/clippy/backends/version.py | 4 +- py/src/clippy/clippy_types.py | 5 +- py/src/clippy/config.py | 1 + py/src/clippy/selectors.py | 3 +- py/src/clippy/utils.py | 2 +- py/{test => tests}/conftest.py | 0 py/{test => tests}/test_clippy.py | 0 13 files changed, 65 insertions(+), 59 deletions(-) rename py/{test => tests}/conftest.py (100%) rename py/{test => tests}/test_clippy.py (100%) diff --git a/py/pyproject.toml b/py/pyproject.toml index aa36ad7..e15d8cc 100644 --- a/py/pyproject.toml +++ b/py/pyproject.toml @@ -31,3 +31,15 @@ optional-dependencies = {dev = {file = ["requirements-dev.txt"] }} [project.urls] Homepage = "https://github.com/LLNL/clippy" Issues = "https://github.com/LLNL/clippy/issues" + +[tool.mypy] +exclude = [ + "^build/", + "attic/", + "^venv/", + "^tests/", +] + +[tool.ruff] +select = ["E", "F", "I", "UP", "B", "C4", "SIM"] +line-length = 120 \ No newline at end of file diff --git a/py/requirements-dev.txt b/py/requirements-dev.txt index 25946d0..55f9c19 100644 --- a/py/requirements-dev.txt +++ b/py/requirements-dev.txt @@ -1,7 +1,5 @@ coverage -flake8 +ruff mypy -pylint -pyproj pytest json-logic-qubit diff --git a/py/src/clippy/backends/fs/__init__.py b/py/src/clippy/backends/fs/__init__.py index 0aa959e..ed49101 100644 --- a/py/src/clippy/backends/fs/__init__.py +++ b/py/src/clippy/backends/fs/__init__.py @@ -2,32 +2,29 @@ from __future__ import annotations +import json +import logging import os +import pathlib import stat -import json import sys -import pathlib -import logging from subprocess import CalledProcessError from typing import Any - -from .execution import _validate, _run, _help -from ..version import _check_version -from ..serialization import ClippySerializable - -from ... import constants +from ... import constants as clippy_constants +from ...clippy_types import CLIPPY_CONFIG from ...error import ( ClippyConfigurationError, + ClippyInvalidSelectorError, ClippyTypeError, ClippyValidationError, - ClippyInvalidSelectorError, ) from ...selectors import Selector from ...utils import flat_dict_to_nested - -from ...clippy_types import CLIPPY_CONFIG +from ..serialization import ClippySerializable +from ..version import _check_version from .config import _fs_config_entries +from .execution import _help, _run, _validate # create a fs-specific configuration. cfg = CLIPPY_CONFIG(_fs_config_entries) @@ -46,10 +43,10 @@ def _is_user_executable(path: pathlib.Path) -> bool: gid = os.getgid() # Owner permissions - if st.st_uid == uid and mode & stat.S_IXUSR: + if st.st_uid == uid and mode & stat.S_IXUSR: # noqa: SIM114 return True # Group permissions - elif st.st_gid == gid and mode & stat.S_IXGRP: + elif st.st_gid == gid and mode & stat.S_IXGRP: # noqa: SIM114 return True # Other permissions elif mode & stat.S_IXOTH: @@ -93,13 +90,13 @@ def _create_class(name: str, path: str, topcfg: CLIPPY_CONFIG): a meta.json file in each class directory. The meta.json file typically holds the class's docstring and any initial top-level selectors as a dictionary of selector: docstring.""" - metafile = pathlib.Path(path, name, constants.CLASS_META_FILE) + metafile = pathlib.Path(path, name, clippy_constants.CLASS_META_FILE) meta = {} if metafile.exists(): - with open(metafile, "r", encoding="utf-8") as json_file: + with open(metafile, encoding="utf-8") as json_file: meta = json.load(json_file) # pull the selectors out since we don't want them in the class definition right now - selectors = meta.pop(constants.INITIAL_SELECTOR_KEY, {}) + selectors = meta.pop(clippy_constants.INITIAL_SELECTOR_KEY, {}) meta["_name"] = name meta["_path"] = path meta["_cfg"] = topcfg @@ -144,14 +141,14 @@ def _process_executable(executable: str, cls): # check to make sure we have the method name. This is so the executable can have # a different name than the actual method. - if constants.METHODNAME_KEY not in j: + if clippy_constants.METHODNAME_KEY not in j: raise ClippyConfigurationError("No method_name in " + executable) # check version if not _check_version(j): raise ClippyConfigurationError("Invalid version information in " + executable) - docstring = j.get(constants.DOCSTRING_KEY, "") - args: dict[str, dict] = j.get(constants.ARGS_KEY, {}) # this is now a dict. + docstring = j.get(clippy_constants.DOCSTRING_KEY, "") + args: dict[str, dict] = j.get(clippy_constants.ARGS_KEY, {}) # this is now a dict. # Create a list of descriptions ordered by position (excluding position=-1) ordered_descs = [] @@ -175,7 +172,7 @@ def _process_executable(executable: str, cls): docstring += f" {desc}\n" # if we don't explicitly pass the method name, use the name of the exe. - method = j.get(constants.METHODNAME_KEY, os.path.basename(executable)) + method = j.get(clippy_constants.METHODNAME_KEY, os.path.basename(executable)) if hasattr(cls, method) and not method.startswith("__"): cls.logger.warning( f"Overwriting existing method {method} for class {cls} with executable {executable}" @@ -189,7 +186,7 @@ def _define_method( ): # pylint: disable=too-complex """Defines a method on a given class.""" if arguments is None: - arguments = dict() + arguments = {} def m(self, *args, **kwargs): """ @@ -208,7 +205,7 @@ def m(self, *args, **kwargs): # .. add state # argdict[STATE_KEY] = self._state - argdict[constants.STATE_KEY] = getattr(self, constants.STATE_KEY) + argdict[clippy_constants.STATE_KEY] = getattr(self, clippy_constants.STATE_KEY) # ~ for key in statedesc: # ~ statej[key] = getattr(self, key) @@ -216,8 +213,7 @@ def m(self, *args, **kwargs): numpositionals = len(args) for argdesc in arguments: value = arguments[argdesc] - if "position" in value: - if 0 <= value["position"] < numpositionals: + if 0 <= value.get("position", -1) < numpositionals: argdict[argdesc] = args[value["position"]] # .. add keyword arguments @@ -235,7 +231,7 @@ def m(self, *args, **kwargs): # kwargs, let's update the kwarg references. Works # for lists and dicts only. for kw, kwval in kwargs.items(): - if kw in outj.get(constants.REFERENCE_KEY, {}): + if kw in outj.get(clippy_constants.REFERENCE_KEY, {}): kwval.clear() if isinstance(kwval, dict): kwval.update(outj[kw]) @@ -245,15 +241,15 @@ def m(self, *args, **kwargs): raise ClippyTypeError() # dump any output - if constants.OUTPUT_KEY in outj: - print(outj[constants.OUTPUT_KEY]) + if clippy_constants.OUTPUT_KEY in outj: + print(outj[clippy_constants.OUTPUT_KEY]) # update state according to json output - if constants.STATE_KEY in outj: - setattr(self, constants.STATE_KEY, outj[constants.STATE_KEY]) + if clippy_constants.STATE_KEY in outj: + setattr(self, clippy_constants.STATE_KEY, outj[clippy_constants.STATE_KEY]) # update selectors if necessary. - if constants.SELECTOR_KEY in outj: - d = flat_dict_to_nested(outj[constants.SELECTOR_KEY]) + if clippy_constants.SELECTOR_KEY in outj: + d = flat_dict_to_nested(outj[clippy_constants.SELECTOR_KEY]) for topsel, subsels in d.items(): if not hasattr(self, topsel): raise ClippyInvalidSelectorError( @@ -262,9 +258,9 @@ def m(self, *args, **kwargs): getattr(self, topsel)._import_from_dict(subsels) # return result - if outj.get(constants.SELF_KEY, False): + if outj.get(clippy_constants.SELF_KEY, False): return self - return outj.get(constants.RETURN_KEY) + return outj.get(clippy_constants.RETURN_KEY) # end of nested def m diff --git a/py/src/clippy/backends/fs/constants.py b/py/src/clippy/backends/fs/constants.py index fb85c5b..d3e3944 100644 --- a/py/src/clippy/backends/fs/constants.py +++ b/py/src/clippy/backends/fs/constants.py @@ -4,9 +4,3 @@ DRY_RUN_FLAG = "--clippy-validate" # the flag to pass to get detailed help for constructing the class HELP_FLAG = "--clippy-help" - -# Arguments for execution progress -PROGRESS_START_KEY = "progress_start" -PROGRESS_END_KEY = "progress_end" -PROGRESS_INC_KEY = "progress_inc" -PROGRESS_SET_KEY = "progress_set" diff --git a/py/src/clippy/backends/fs/execution.py b/py/src/clippy/backends/fs/execution.py index 60523b6..2ec66de 100644 --- a/py/src/clippy/backends/fs/execution.py +++ b/py/src/clippy/backends/fs/execution.py @@ -3,18 +3,19 @@ """ from __future__ import annotations + +import contextlib import json import logging -import select import os - +import select import subprocess -from ...clippy_types import AnyDict + from ... import cfg +from ...clippy_types import AnyDict +from ..serialization import decode_clippy_json, encode_clippy_json from .constants import DRY_RUN_FLAG, HELP_FLAG -from ..serialization import encode_clippy_json, decode_clippy_json - def _stream_exec( cmd: list[str], @@ -110,10 +111,8 @@ def _stream_exec( # Process any remaining buffered data if stdout_buffer.strip(): - try: + with contextlib.suppress(json.JSONDecodeError): d = json.loads(stdout_buffer, object_hook=decode_clippy_json) - except json.JSONDecodeError: - pass if stderr_buffer.strip(): stderr_lines.append(stderr_buffer) diff --git a/py/src/clippy/backends/serialization.py b/py/src/clippy/backends/serialization.py index 2edd4e6..e81a336 100644 --- a/py/src/clippy/backends/serialization.py +++ b/py/src/clippy/backends/serialization.py @@ -3,12 +3,14 @@ """ from __future__ import annotations -import jsonlogic as jl + from typing import Any -from ..error import ClippySerializationError + +import jsonlogic as jl + from .. import _dynamic_types from ..clippy_types import AnyDict - +from ..error import ClippySerializationError # TODO: SAB 20240204 complete typing here. diff --git a/py/src/clippy/backends/version.py b/py/src/clippy/backends/version.py index 64f5779..7979af1 100644 --- a/py/src/clippy/backends/version.py +++ b/py/src/clippy/backends/version.py @@ -3,9 +3,11 @@ """ from __future__ import annotations + from semver import Version -from ..clippy_types import AnyDict + from .. import cfg +from ..clippy_types import AnyDict def _check_version(output_dict: AnyDict | None) -> bool: diff --git a/py/src/clippy/clippy_types.py b/py/src/clippy/clippy_types.py index 8247a35..a58027f 100644 --- a/py/src/clippy/clippy_types.py +++ b/py/src/clippy/clippy_types.py @@ -7,7 +7,8 @@ """ import os -from typing import Any, Optional +from typing import Any + from .error import ClippyConfigurationError # AnyDict is a convenience type so we can find places @@ -15,7 +16,7 @@ AnyDict = dict[str, Any] # CONFIG_ENTRY is a convenience type for use in CLIPPY_CONFIG. -CONFIG_ENTRY = tuple[Optional[str], Any] +CONFIG_ENTRY = tuple[str | None, Any] # CLIPPY_CONFIG holds configuration items for both diff --git a/py/src/clippy/config.py b/py/src/clippy/config.py index 0cb6567..5b8181f 100644 --- a/py/src/clippy/config.py +++ b/py/src/clippy/config.py @@ -4,6 +4,7 @@ # The format is config_key: (environment variable or None, default value) import logging + from .clippy_types import CONFIG_ENTRY _clippy_cfg: dict[str, CONFIG_ENTRY] = { diff --git a/py/src/clippy/selectors.py b/py/src/clippy/selectors.py index 9e8fd4a..4341ca7 100644 --- a/py/src/clippy/selectors.py +++ b/py/src/clippy/selectors.py @@ -1,6 +1,7 @@ """Custom selectors for clippy.""" from __future__ import annotations + import jsonlogic as jl from . import constants @@ -37,7 +38,7 @@ def hierarchy(self, acc: list[tuple[str, str]] | None = None): def describe(self): hier = self.hierarchy() - maxlen = max((len(sub_desc[0]) for sub_desc in hier)) + maxlen = max(len(sub_desc[0]) for sub_desc in hier) return "\n".join( f"{sub_desc[0]:<{maxlen+2}} {sub_desc[1]}" for sub_desc in hier ) diff --git a/py/src/clippy/utils.py b/py/src/clippy/utils.py index 23816a3..35bb3d2 100644 --- a/py/src/clippy/utils.py +++ b/py/src/clippy/utils.py @@ -3,8 +3,8 @@ """ from .clippy_types import AnyDict -from .error import ClippyInvalidSelectorError from .constants import SELECTOR_KEY +from .error import ClippyInvalidSelectorError def flat_dict_to_nested(input_dict: AnyDict) -> AnyDict: diff --git a/py/test/conftest.py b/py/tests/conftest.py similarity index 100% rename from py/test/conftest.py rename to py/tests/conftest.py diff --git a/py/test/test_clippy.py b/py/tests/test_clippy.py similarity index 100% rename from py/test/test_clippy.py rename to py/tests/test_clippy.py From 543f4a50a45197ceeac099a9c0d9e0c434e4ddd1 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Tue, 16 Dec 2025 19:03:49 -0800 Subject: [PATCH 05/10] ruff linting --- py/pyproject.toml | 2 +- py/src/clippy/__init__.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/py/pyproject.toml b/py/pyproject.toml index e15d8cc..efe7fda 100644 --- a/py/pyproject.toml +++ b/py/pyproject.toml @@ -41,5 +41,5 @@ exclude = [ ] [tool.ruff] -select = ["E", "F", "I", "UP", "B", "C4", "SIM"] +lint.select = ["E", "F", "I", "UP", "B", "C4", "SIM"] line-length = 120 \ No newline at end of file diff --git a/py/src/clippy/__init__.py b/py/src/clippy/__init__.py index a66808b..fb8ddc0 100644 --- a/py/src/clippy/__init__.py +++ b/py/src/clippy/__init__.py @@ -5,11 +5,11 @@ # Create the logger from __future__ import annotations -import logging import importlib -from .config import _clippy_cfg -from .clippy_types import AnyDict, CLIPPY_CONFIG +import logging +from .clippy_types import CLIPPY_CONFIG, AnyDict +from .config import _clippy_cfg # Create the main configuraton object and expose it globally. cfg = CLIPPY_CONFIG(_clippy_cfg) From ca274acae60bfbf95f23f2ba581ea53ef298a422 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Tue, 16 Dec 2025 19:10:03 -0800 Subject: [PATCH 06/10] ruff formatting --- py/src/clippy/__init__.py | 8 +++--- py/src/clippy/backends/fs/__init__.py | 14 +++------- py/src/clippy/backends/fs/execution.py | 36 ++++++++++++------------- py/src/clippy/backends/serialization.py | 4 +-- py/src/clippy/config.py | 2 +- py/src/clippy/error.py | 34 +++++++++++------------ py/src/clippy/selectors.py | 12 +++------ py/src/clippy/utils.py | 20 ++++++-------- py/tests/test_clippy.py | 7 +++-- 9 files changed, 58 insertions(+), 79 deletions(-) diff --git a/py/src/clippy/__init__.py b/py/src/clippy/__init__.py index fb8ddc0..f8319a3 100644 --- a/py/src/clippy/__init__.py +++ b/py/src/clippy/__init__.py @@ -1,4 +1,4 @@ -""" This is the clippy initialization file. """ +"""This is the clippy initialization file.""" # The general flow is as follows: # Create the configurations (see comments in .config for details) @@ -27,15 +27,15 @@ def load_classes(): - '''For each listed backend, import the module of the same name. The + """For each listed backend, import the module of the same name. The backend should expose two functions: a classes() function that returns a dictionary of classes keyed by name, and a get_cfg() function that returns a CLIPPY_CONFIG object with backend-specific configuration. This object is then made an attribute of the global configuration (i.e., `cfg.fs.get('fs_specific_config')`). - ''' + """ for backend in cfg.get("backends"): - b = importlib.import_module(f'.backends.{backend}', package=__name__) + b = importlib.import_module(f".backends.{backend}", package=__name__) setattr(cfg, backend, b.get_cfg()) for name, c in b.classes().items(): # backend_config = importlib.import_module(f".backends.{name}.config.{name}_config") diff --git a/py/src/clippy/backends/fs/__init__.py b/py/src/clippy/backends/fs/__init__.py index ed49101..2864dc3 100644 --- a/py/src/clippy/backends/fs/__init__.py +++ b/py/src/clippy/backends/fs/__init__.py @@ -174,16 +174,12 @@ def _process_executable(executable: str, cls): # if we don't explicitly pass the method name, use the name of the exe. method = j.get(clippy_constants.METHODNAME_KEY, os.path.basename(executable)) if hasattr(cls, method) and not method.startswith("__"): - cls.logger.warning( - f"Overwriting existing method {method} for class {cls} with executable {executable}" - ) + cls.logger.warning(f"Overwriting existing method {method} for class {cls} with executable {executable}") _define_method(cls, method, executable, docstring, args) return cls -def _define_method( - cls, name: str, executable: str, docstr: str, arguments: dict[str, dict] | None -): # pylint: disable=too-complex +def _define_method(cls, name: str, executable: str, docstr: str, arguments: dict[str, dict] | None): # pylint: disable=too-complex """Defines a method on a given class.""" if arguments is None: arguments = {} @@ -214,7 +210,7 @@ def m(self, *args, **kwargs): for argdesc in arguments: value = arguments[argdesc] if 0 <= value.get("position", -1) < numpositionals: - argdict[argdesc] = args[value["position"]] + argdict[argdesc] = args[value["position"]] # .. add keyword arguments argdict.update(kwargs) @@ -252,9 +248,7 @@ def m(self, *args, **kwargs): d = flat_dict_to_nested(outj[clippy_constants.SELECTOR_KEY]) for topsel, subsels in d.items(): if not hasattr(self, topsel): - raise ClippyInvalidSelectorError( - f"selector {topsel} not found in class; aborting" - ) + raise ClippyInvalidSelectorError(f"selector {topsel} not found in class; aborting") getattr(self, topsel)._import_from_dict(subsels) # return result diff --git a/py/src/clippy/backends/fs/execution.py b/py/src/clippy/backends/fs/execution.py index 2ec66de..6ed7f3a 100644 --- a/py/src/clippy/backends/fs/execution.py +++ b/py/src/clippy/backends/fs/execution.py @@ -1,5 +1,5 @@ """ - Functions to execute backend programs. +Functions to execute backend programs. """ from __future__ import annotations @@ -37,7 +37,7 @@ def _stream_exec( already be set. """ - logger.debug(f'Submission = {submission_dict}') + logger.debug(f"Submission = {submission_dict}") # PP support passing objects # ~ cmd_stdin = json.dumps(submission_dict) cmd_stdin = json.dumps(submission_dict, default=encode_clippy_json) @@ -48,7 +48,7 @@ def _stream_exec( stderr_lines = [] with subprocess.Popen( - cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf8' + cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf8" ) as proc: assert proc.stdin is not None assert proc.stdout is not None @@ -127,26 +127,24 @@ def _stream_exec( if not d: return None, stderr, proc.returncode if stderr: - logger.debug('Received stderr: %s', stderr) + logger.debug("Received stderr: %s", stderr) if proc.returncode != 0: logger.debug("Process returned %d", proc.returncode) - logger.debug('run(): final stdout = %s', d) + logger.debug("run(): final stdout = %s", d) return (d, stderr, proc.returncode) -def _validate( - cmd: str | list[str], dct: AnyDict, logger: logging.Logger -) -> tuple[bool, str]: - ''' +def _validate(cmd: str | list[str], dct: AnyDict, logger: logging.Logger) -> tuple[bool, str]: + """ Converts the dictionary dct into a json file and calls executable cmd with the DRY_RUN_FLAG. Returns True/False (validation successful) and any stderr. - ''' + """ if isinstance(cmd, str): cmd = [cmd] - execcmd = cfg.get('validate_cmd_prefix').split() + cmd + [DRY_RUN_FLAG] + execcmd = cfg.get("validate_cmd_prefix").split() + cmd + [DRY_RUN_FLAG] logger.debug("Validating %s", cmd) _, stderr, retcode = _stream_exec(execcmd, dct, logger, validate=True) @@ -154,15 +152,15 @@ def _validate( def _run(cmd: str | list[str], dct: AnyDict, logger: logging.Logger) -> AnyDict: - ''' + """ converts the dictionary dct into a json file and calls executable cmd. Prepends cmd_prefix configuration, if any. - ''' + """ if isinstance(cmd, str): cmd = [cmd] - execcmd = cfg.get('cmd_prefix').split() + cmd - logger.debug('Running %s', execcmd) + execcmd = cfg.get("cmd_prefix").split() + cmd + logger.debug("Running %s", execcmd) # should we do something with stderr? output, _, retcode = _stream_exec(execcmd, dct, logger, validate=False) @@ -172,15 +170,15 @@ def _run(cmd: str | list[str], dct: AnyDict, logger: logging.Logger) -> AnyDict: def _help(cmd: str | list[str], dct: AnyDict, logger: logging.Logger) -> AnyDict: - ''' + """ Retrieves the help output from the clippy command. Prepends validate_cmd_prefix if set and appends HELP_FLAG. Unlike `_validate()`, does not append DRY_RUN_FLAG, and returns the output. - ''' + """ if isinstance(cmd, str): cmd = [cmd] - execcmd = cfg.get('validate_cmd_prefix').split() + cmd + [HELP_FLAG] - logger.debug('Running %s', execcmd) + execcmd = cfg.get("validate_cmd_prefix").split() + cmd + [HELP_FLAG] + logger.debug("Running %s", execcmd) # should we do something with stderr? output, _, _ = _stream_exec(execcmd, dct, logger, validate=True) diff --git a/py/src/clippy/backends/serialization.py b/py/src/clippy/backends/serialization.py index e81a336..7a8f626 100644 --- a/py/src/clippy/backends/serialization.py +++ b/py/src/clippy/backends/serialization.py @@ -72,9 +72,7 @@ def from_serial(cls, o: AnyDict): raise ClippySerializationError("__clippy_type__.__class__ is unspecified") if type_name not in _dynamic_types: - raise ClippySerializationError( - f'"{type_name}" is not a known type, please clippy import it.' - ) + raise ClippySerializationError(f'"{type_name}" is not a known type, please clippy import it.') # get the type to deserialize into from the _dynamic_types dict # this does not account for the module the type may exist in diff --git a/py/src/clippy/config.py b/py/src/clippy/config.py index 5b8181f..f20c598 100644 --- a/py/src/clippy/config.py +++ b/py/src/clippy/config.py @@ -1,5 +1,5 @@ # pylint: disable=consider-using-namedtuple-or-dataclass -''' This holds a dictionary containing global configuration variables for clippy.''' +"""This holds a dictionary containing global configuration variables for clippy.""" # The format is config_key: (environment variable or None, default value) diff --git a/py/src/clippy/error.py b/py/src/clippy/error.py index e947f18..557ab20 100644 --- a/py/src/clippy/error.py +++ b/py/src/clippy/error.py @@ -3,52 +3,52 @@ # # SPDX-License-Identifier: MIT -""" This file contains custom Clippy errors. """ +"""This file contains custom Clippy errors.""" class ClippyError(Exception): - ''' + """ This is a top-level custom exception for Clippy. - ''' + """ class ClippyConfigurationError(ClippyError): - ''' + """ This error represents a configuration error on user input. - ''' + """ class ClippyBackendError(ClippyError): - ''' + """ This error should be thrown when the backend returns an abend. - ''' + """ class ClippyValidationError(ClippyError): - ''' + """ This error represents a validation error in the inputs to a clippy job. - ''' + """ class ClippySerializationError(ClippyError): - ''' + """ This error should be thrown when clippy object serialization fails - ''' + """ class ClippyClassInconsistencyError(ClippyError): - ''' + """ This error represents a class inconsistency error (name or docstring mismatch). - ''' + """ class ClippyTypeError(ClippyError): - ''' + """ This error represents an error with the type of data being passed to the back end. - ''' + """ class ClippyInvalidSelectorError(ClippyError): - ''' + """ This error represents an error with a selector that is not defined for a given clippy class. - ''' + """ diff --git a/py/src/clippy/selectors.py b/py/src/clippy/selectors.py index 4341ca7..5854606 100644 --- a/py/src/clippy/selectors.py +++ b/py/src/clippy/selectors.py @@ -12,14 +12,10 @@ class Selector(jl.Variable): """A Selector represents a single variable.""" def __init__(self, parent: Selector | None, name: str, docstr: str): - super().__init__( - name, docstr - ) # op and o2 are None to represent this as a variable. + super().__init__(name, docstr) # op and o2 are None to represent this as a variable. self.parent = parent self.name = name - self.fullname: str = ( - self.name if self.parent is None else f"{self.parent.fullname}.{self.name}" - ) + self.fullname: str = self.name if self.parent is None else f"{self.parent.fullname}.{self.name}" self.subselectors: set[Selector] = set() def __hash__(self): @@ -39,9 +35,7 @@ def hierarchy(self, acc: list[tuple[str, str]] | None = None): def describe(self): hier = self.hierarchy() maxlen = max(len(sub_desc[0]) for sub_desc in hier) - return "\n".join( - f"{sub_desc[0]:<{maxlen+2}} {sub_desc[1]}" for sub_desc in hier - ) + return "\n".join(f"{sub_desc[0]:<{maxlen + 2}} {sub_desc[1]}" for sub_desc in hier) def __str__(self): return repr(self.prepare()) diff --git a/py/src/clippy/utils.py b/py/src/clippy/utils.py index 35bb3d2..793ca06 100644 --- a/py/src/clippy/utils.py +++ b/py/src/clippy/utils.py @@ -1,5 +1,5 @@ """ - Utility functions +Utility functions """ from .clippy_types import AnyDict @@ -16,23 +16,19 @@ def flat_dict_to_nested(input_dict: AnyDict) -> AnyDict: output_dict: AnyDict = {} for k, v in input_dict.items(): # k is dotted - if '.' not in k: + if "." not in k: raise ClippyInvalidSelectorError("cannot set top-level selectors") - *path, last = k.split('.') - if last.startswith('_'): - raise ClippyInvalidSelectorError( - "selectors must not start with an underscore." - ) + *path, last = k.split(".") + if last.startswith("_"): + raise ClippyInvalidSelectorError("selectors must not start with an underscore.") curr_nest = output_dict for p in path: - if p.startswith('_'): - raise ClippyInvalidSelectorError( - "selectors must not start with an underscore." - ) + if p.startswith("_"): + raise ClippyInvalidSelectorError("selectors must not start with an underscore.") curr_nest.setdefault(p, {}) curr_nest[p].setdefault(SELECTOR_KEY, {}) curr_nest = curr_nest[p][SELECTOR_KEY] - curr_nest.setdefault(last, {'__doc__': v}) + curr_nest.setdefault(last, {"__doc__": v}) return output_dict diff --git a/py/tests/test_clippy.py b/py/tests/test_clippy.py index 5cbfcf8..8ae2972 100644 --- a/py/tests/test_clippy.py +++ b/py/tests/test_clippy.py @@ -43,7 +43,6 @@ def test_imports(): def test_bag(testbag): - testbag.insert(41) assert testbag.size() == 1 testbag.insert(42) @@ -191,9 +190,9 @@ def test_selectors(testsel): def test_graph(testgraph): - testgraph.add_edge("a", "b").add_edge("b", "c").add_edge("a", "c").add_edge( - "c", "d" - ).add_edge("d", "e").add_edge("e", "f").add_edge("f", "g").add_edge("e", "g") + testgraph.add_edge("a", "b").add_edge("b", "c").add_edge("a", "c").add_edge("c", "d").add_edge("d", "e").add_edge( + "e", "f" + ).add_edge("f", "g").add_edge("e", "g") assert testgraph.nv() == 7 assert testgraph.ne() == 8 From 0c9fbe307787f082c8251ef423d62a56d1776a2f Mon Sep 17 00:00:00 2001 From: Roger Pearce Date: Thu, 18 Dec 2025 05:16:07 -0600 Subject: [PATCH 07/10] Copied dev container from clippy-cpp --- .devcontainer/Dockerfile | 22 ++++++++++++ .devcontainer/devcontainer.json | 21 ++++++++++++ .devcontainer/reinstall-cmake.sh | 59 ++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/reinstall-cmake.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..5df108f --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +# FROM mcr.microsoft.com/devcontainers/cpp:1-ubuntu-24.04 +FROM mcr.microsoft.com/devcontainers/cpp:dev-ubuntu-24.04 + +ARG REINSTALL_CMAKE_VERSION_FROM_SOURCE="none" + +# Optionally install the cmake for vcpkg +COPY ./reinstall-cmake.sh /tmp/ + +RUN if [ "${REINSTALL_CMAKE_VERSION_FROM_SOURCE}" != "none" ]; then \ + chmod +x /tmp/reinstall-cmake.sh && /tmp/reinstall-cmake.sh ${REINSTALL_CMAKE_VERSION_FROM_SOURCE}; \ + fi \ + && rm -f /tmp/reinstall-cmake.sh + +# [Optional] Uncomment this section to install additional vcpkg ports. +# RUN su vscode -c "${VCPKG_ROOT}/vcpkg install clangd" + +# [Optional] Uncomment this section to install additional packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends +RUN apt update && export DEBIAN_FRONTEND=noninteractive && apt -y install clangd-19 clang-tidy-19 python3-pip +RUN update-alternatives --install /usr/bin/clangd clangd /usr/bin/clangd-19 100 +RUN update-alternatives --install /usr/bin/clang-tidy clang-tidy /usr/bin/clang-tidy-19 100 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..68e3eee --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,21 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/cpp +{ + "name": "C++", + "build": { + "dockerfile": "Dockerfile" + }, + "containerEnv": { + "CLIPPY_BACKEND_PATH": "${containerWorkspaceFolder}/build/test" + }, + "customizations": { + "vscode": { + "extensions": [ + "llvm-vs-code-extensions.vscode-clangd", + "ms-python.python" + // add other extensions as needed + ] + } + }, + "postCreateCommand": "pip install --break-system-packages -r ${containerWorkspaceFolder}/test/requirements.txt" +} diff --git a/.devcontainer/reinstall-cmake.sh b/.devcontainer/reinstall-cmake.sh new file mode 100644 index 0000000..408b81d --- /dev/null +++ b/.devcontainer/reinstall-cmake.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +set -e + +CMAKE_VERSION=${1:-"none"} + +if [ "${CMAKE_VERSION}" = "none" ]; then + echo "No CMake version specified, skipping CMake reinstallation" + exit 0 +fi + +# Cleanup temporary directory and associated files when exiting the script. +cleanup() { + EXIT_CODE=$? + set +e + if [[ -n "${TMP_DIR}" ]]; then + echo "Executing cleanup of tmp files" + rm -Rf "${TMP_DIR}" + fi + exit $EXIT_CODE +} +trap cleanup EXIT + + +echo "Installing CMake..." +apt-get -y purge --auto-remove cmake +mkdir -p /opt/cmake + +architecture=$(dpkg --print-architecture) +case "${architecture}" in + arm64) + ARCH=aarch64 ;; + amd64) + ARCH=x86_64 ;; + *) + echo "Unsupported architecture ${architecture}." + exit 1 + ;; +esac + +CMAKE_BINARY_NAME="cmake-${CMAKE_VERSION}-linux-${ARCH}.sh" +CMAKE_CHECKSUM_NAME="cmake-${CMAKE_VERSION}-SHA-256.txt" +TMP_DIR=$(mktemp -d -t cmake-XXXXXXXXXX) + +echo "${TMP_DIR}" +cd "${TMP_DIR}" + +curl -sSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_BINARY_NAME}" -O +curl -sSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/${CMAKE_CHECKSUM_NAME}" -O + +sha256sum -c --ignore-missing "${CMAKE_CHECKSUM_NAME}" +sh "${TMP_DIR}/${CMAKE_BINARY_NAME}" --prefix=/opt/cmake --skip-license + +ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake +ln -s /opt/cmake/bin/ctest /usr/local/bin/ctest From b371cd7dd3b471c3d84e20d3160a3f471ce2b3a5 Mon Sep 17 00:00:00 2001 From: Roger Pearce Date: Thu, 18 Dec 2025 11:18:21 +0000 Subject: [PATCH 08/10] commented out reqirments. --- .devcontainer/devcontainer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 68e3eee..8399eb1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,8 +6,8 @@ "dockerfile": "Dockerfile" }, "containerEnv": { - "CLIPPY_BACKEND_PATH": "${containerWorkspaceFolder}/build/test" - }, + "CLIPPY_BACKEND_PATH": "${containerWorkspaceFolder}/build/test" + }, "customizations": { "vscode": { "extensions": [ @@ -17,5 +17,5 @@ ] } }, - "postCreateCommand": "pip install --break-system-packages -r ${containerWorkspaceFolder}/test/requirements.txt" -} + //"postCreateCommand": "pip install --break-system-packages -r ${containerWorkspaceFolder}/test/requirements.txt" +} \ No newline at end of file From 9415b67a2f9f7eccbc0af15ca32920107395f102 Mon Sep 17 00:00:00 2001 From: Roger Pearce Date: Thu, 18 Dec 2025 12:14:56 +0000 Subject: [PATCH 09/10] Copying good files from clippy-cpp repo. --- .gitignore | 5 +- cpp/CMakeLists.txt | 99 +++ cpp/CMakePresets.json | 11 + cpp/examples/README.md | 22 + cpp/examples/SimpleExamples/CMakeLists.txt | 74 +++ cpp/examples/SimpleExamples/grumpy.cpp | 16 + cpp/examples/SimpleExamples/howdy.cpp | 18 + cpp/examples/SimpleExamples/return_tuple.cpp | 18 + cpp/examples/SimpleExamples/sort_edges.cpp | 30 + .../SimpleExamples/sort_string_edges.cpp | 32 + cpp/examples/SimpleExamples/sort_strings.cpp | 33 + cpp/examples/SimpleExamples/sum.cpp | 21 + cpp/include/clippy/clippy-object.hpp | 78 +++ cpp/include/clippy/clippy.hpp | 563 ++++++++++++++++ cpp/include/clippy/selector.hpp | 70 ++ cpp/include/clippy/version.hpp | 6 + cpp/test/CMakeLists.txt | 30 + cpp/test/TestBag/CMakeLists.txt | 11 + cpp/test/TestBag/__init__.cpp | 27 + cpp/test/TestBag/__str__.cpp | 38 ++ cpp/test/TestBag/insert.cpp | 31 + cpp/test/TestBag/meta.json | 6 + cpp/test/TestBag/remove.cpp | 44 ++ cpp/test/TestBag/remove_if.cpp | 47 ++ cpp/test/TestBag/size.cpp | 30 + cpp/test/TestFunctions/CMakeLists.txt | 11 + .../call_with_optional_string.cpp | 20 + cpp/test/TestFunctions/call_with_string.cpp | 18 + cpp/test/TestFunctions/missing_version.cpp | 7 + cpp/test/TestFunctions/old_version.cpp | 7 + .../TestFunctions/pass_by_reference_dict.cpp | 18 + .../pass_by_reference_vector.cpp | 18 + cpp/test/TestFunctions/returns_bool.cpp | 16 + cpp/test/TestFunctions/returns_dict.cpp | 21 + cpp/test/TestFunctions/returns_int.cpp | 16 + cpp/test/TestFunctions/returns_string.cpp | 16 + cpp/test/TestFunctions/returns_vec_int.cpp | 16 + cpp/test/TestGraph/CMakeLists.txt | 25 + cpp/test/TestGraph/__init__.cpp | 31 + cpp/test/TestGraph/__str__.cpp | 39 ++ cpp/test/TestGraph/add.cpp | 57 ++ cpp/test/TestGraph/add_edge.cpp | 35 + cpp/test/TestGraph/add_node.cpp | 33 + cpp/test/TestGraph/add_series.cpp | 79 +++ cpp/test/TestGraph/assign.cpp | 341 ++++++++++ cpp/test/TestGraph/bfs | Bin 0 -> 204440 bytes cpp/test/TestGraph/bfs.cpp | 34 + cpp/test/TestGraph/connected_components.cpp | 124 ++++ cpp/test/TestGraph/copy_series.cpp | 92 +++ cpp/test/TestGraph/count.cpp | 124 ++++ cpp/test/TestGraph/degree.cpp | 78 +++ cpp/test/TestGraph/drop_series.cpp | 66 ++ cpp/test/TestGraph/dump.cpp | 129 ++++ cpp/test/TestGraph/dump2.cpp | 89 +++ cpp/test/TestGraph/extrema.cpp | 155 +++++ cpp/test/TestGraph/for_all_edges.cpp | 52 ++ cpp/test/TestGraph/meta.json | 7 + cpp/test/TestGraph/mvmap.hpp | 609 ++++++++++++++++++ cpp/test/TestGraph/ne.cpp | 29 + cpp/test/TestGraph/nv.cpp | 29 + cpp/test/TestGraph/remove.cpp | 36 ++ cpp/test/TestGraph/series_str.cpp | 65 ++ cpp/test/TestGraph/testconst.cpp | 8 + cpp/test/TestGraph/testgraph.cpp | 48 ++ cpp/test/TestGraph/testgraph.hpp | 232 +++++++ cpp/test/TestGraph/testlocator.cpp | 7 + cpp/test/TestGraph/testmvmap.cpp | 71 ++ cpp/test/TestGraph/testselector.cpp | 20 + cpp/test/TestGraph/where.cpp | 114 ++++ cpp/test/TestSelector/CMakeLists.txt | 8 + cpp/test/TestSelector/__init__.cpp | 29 + cpp/test/TestSelector/add.cpp | 58 ++ cpp/test/TestSelector/drop.cpp | 52 ++ cpp/test/TestSelector/meta.json | 7 + cpp/test/TestSelector/selector.hpp | 26 + cpp/test/TestSet/CMakeLists.txt | 11 + cpp/test/TestSet/__init__.cpp | 30 + cpp/test/TestSet/__str__.cpp | 38 ++ cpp/test/TestSet/insert.cpp | 34 + cpp/test/TestSet/meta.json | 6 + cpp/test/TestSet/remove.cpp | 36 ++ cpp/test/TestSet/remove_if.cpp | 48 ++ cpp/test/TestSet/size.cpp | 31 + cpp/test/requirements.txt | 2 + cpp/test/test_clippy.py | 205 ++++++ 85 files changed, 5020 insertions(+), 3 deletions(-) create mode 100644 cpp/CMakeLists.txt create mode 100644 cpp/CMakePresets.json create mode 100644 cpp/examples/README.md create mode 100644 cpp/examples/SimpleExamples/CMakeLists.txt create mode 100644 cpp/examples/SimpleExamples/grumpy.cpp create mode 100644 cpp/examples/SimpleExamples/howdy.cpp create mode 100644 cpp/examples/SimpleExamples/return_tuple.cpp create mode 100644 cpp/examples/SimpleExamples/sort_edges.cpp create mode 100644 cpp/examples/SimpleExamples/sort_string_edges.cpp create mode 100644 cpp/examples/SimpleExamples/sort_strings.cpp create mode 100644 cpp/examples/SimpleExamples/sum.cpp create mode 100644 cpp/include/clippy/clippy-object.hpp create mode 100644 cpp/include/clippy/clippy.hpp create mode 100644 cpp/include/clippy/selector.hpp create mode 100644 cpp/include/clippy/version.hpp create mode 100644 cpp/test/CMakeLists.txt create mode 100644 cpp/test/TestBag/CMakeLists.txt create mode 100644 cpp/test/TestBag/__init__.cpp create mode 100644 cpp/test/TestBag/__str__.cpp create mode 100644 cpp/test/TestBag/insert.cpp create mode 100644 cpp/test/TestBag/meta.json create mode 100644 cpp/test/TestBag/remove.cpp create mode 100644 cpp/test/TestBag/remove_if.cpp create mode 100644 cpp/test/TestBag/size.cpp create mode 100644 cpp/test/TestFunctions/CMakeLists.txt create mode 100644 cpp/test/TestFunctions/call_with_optional_string.cpp create mode 100644 cpp/test/TestFunctions/call_with_string.cpp create mode 100644 cpp/test/TestFunctions/missing_version.cpp create mode 100644 cpp/test/TestFunctions/old_version.cpp create mode 100644 cpp/test/TestFunctions/pass_by_reference_dict.cpp create mode 100644 cpp/test/TestFunctions/pass_by_reference_vector.cpp create mode 100644 cpp/test/TestFunctions/returns_bool.cpp create mode 100644 cpp/test/TestFunctions/returns_dict.cpp create mode 100644 cpp/test/TestFunctions/returns_int.cpp create mode 100644 cpp/test/TestFunctions/returns_string.cpp create mode 100644 cpp/test/TestFunctions/returns_vec_int.cpp create mode 100644 cpp/test/TestGraph/CMakeLists.txt create mode 100644 cpp/test/TestGraph/__init__.cpp create mode 100644 cpp/test/TestGraph/__str__.cpp create mode 100644 cpp/test/TestGraph/add.cpp create mode 100644 cpp/test/TestGraph/add_edge.cpp create mode 100644 cpp/test/TestGraph/add_node.cpp create mode 100644 cpp/test/TestGraph/add_series.cpp create mode 100644 cpp/test/TestGraph/assign.cpp create mode 100755 cpp/test/TestGraph/bfs create mode 100644 cpp/test/TestGraph/bfs.cpp create mode 100644 cpp/test/TestGraph/connected_components.cpp create mode 100644 cpp/test/TestGraph/copy_series.cpp create mode 100644 cpp/test/TestGraph/count.cpp create mode 100644 cpp/test/TestGraph/degree.cpp create mode 100644 cpp/test/TestGraph/drop_series.cpp create mode 100644 cpp/test/TestGraph/dump.cpp create mode 100644 cpp/test/TestGraph/dump2.cpp create mode 100644 cpp/test/TestGraph/extrema.cpp create mode 100644 cpp/test/TestGraph/for_all_edges.cpp create mode 100644 cpp/test/TestGraph/meta.json create mode 100644 cpp/test/TestGraph/mvmap.hpp create mode 100644 cpp/test/TestGraph/ne.cpp create mode 100644 cpp/test/TestGraph/nv.cpp create mode 100644 cpp/test/TestGraph/remove.cpp create mode 100644 cpp/test/TestGraph/series_str.cpp create mode 100644 cpp/test/TestGraph/testconst.cpp create mode 100644 cpp/test/TestGraph/testgraph.cpp create mode 100644 cpp/test/TestGraph/testgraph.hpp create mode 100644 cpp/test/TestGraph/testlocator.cpp create mode 100644 cpp/test/TestGraph/testmvmap.cpp create mode 100644 cpp/test/TestGraph/testselector.cpp create mode 100644 cpp/test/TestGraph/where.cpp create mode 100644 cpp/test/TestSelector/CMakeLists.txt create mode 100644 cpp/test/TestSelector/__init__.cpp create mode 100644 cpp/test/TestSelector/add.cpp create mode 100644 cpp/test/TestSelector/drop.cpp create mode 100644 cpp/test/TestSelector/meta.json create mode 100644 cpp/test/TestSelector/selector.hpp create mode 100644 cpp/test/TestSet/CMakeLists.txt create mode 100644 cpp/test/TestSet/__init__.cpp create mode 100644 cpp/test/TestSet/__str__.cpp create mode 100644 cpp/test/TestSet/insert.cpp create mode 100644 cpp/test/TestSet/meta.json create mode 100644 cpp/test/TestSet/remove.cpp create mode 100644 cpp/test/TestSet/remove_if.cpp create mode 100644 cpp/test/TestSet/size.cpp create mode 100644 cpp/test/requirements.txt create mode 100644 cpp/test/test_clippy.py diff --git a/.gitignore b/.gitignore index 28ce1bb..b8b7123 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,10 @@ __pycache__ **/build venv -*.core -build setup.cfg .vscode *.egg-info .coverage -*.tgz **/attic +build +*/.cache \ No newline at end of file diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt new file mode 100644 index 0000000..0d53d76 --- /dev/null +++ b/cpp/CMakeLists.txt @@ -0,0 +1,99 @@ +# Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy +# Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: MIT + + +cmake_minimum_required(VERSION 3.26) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +set(ALLOW_DUPLICATE_CUSTOM_TARGETS TRUE) + +# Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24: +if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") +cmake_policy(SET CMP0135 NEW) +endif() + +project(CLIPPy + VERSION 0.5 + DESCRIPTION "Command Line Interface Plus Python" + LANGUAGES CXX) + +include(FetchContent) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Only do these if this is the main project, and not if it is included through add_subdirectory +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + + set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + + # Let's ensure -std=c++xx instead of -std=g++xx + set(CMAKE_CXX_EXTENSIONS OFF) + + # Let's nicely support folders in IDE's + set_property(GLOBAL PROPERTY USE_FOLDERS ON) + + # Testing only available if this is the main app + # Note this needs to be done in the main CMakeLists + # since it calls enable_testing, which must be in the + # main CMakeLists. + # include(CTest) + + # Docs only available if this is the main app + find_package(Doxygen) + if(Doxygen_FOUND) + #add_subdirectory(docs) + else() + message(STATUS "Doxygen not found, not building docs") + endif() +endif() + + +# +# Boost +# Download and build Boost::json +set(BOOST_URL + "https://github.com/boostorg/boost/releases/download/boost-1.87.0/boost-1.87.0-cmake.tar.gz" + CACHE STRING "URL to fetch Boost tarball") + + +set(BOOST_INCLUDE_LIBRARIES json lexical_cast range) +set(BUILD_SHARED_LIBS ON) +FetchContent_Declare( + Boost + URL ${BOOST_URL}) +FetchContent_MakeAvailable(Boost) + + +# +# JSONLogic +set(Boost_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR}/boost-install) # needed for jsonlogic + +FetchContent_Declare(jsonlogic + GIT_REPOSITORY https://github.com/LLNL/jsonlogic.git + GIT_TAG v0.2.0 + SOURCE_SUBDIR cpp +) +# set(jsonlogic_INCLUDE_DIR ${jsonlogic_SOURCE_DIR}/cpp/include/jsonlogic) +FetchContent_MakeAvailable(jsonlogic) +message(STATUS "jsonlogic source dir: ${jsonlogic_SOURCE_DIR}") + + + +### Require out-of-source builds +file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/CMakeLists.txt" LOC_PATH) +if(EXISTS "${LOC_PATH}") + message(FATAL_ERROR "You cannot build in a source directory (or any directory with a CMakeLists.txt file). Please make a build subdirectory. Feel free to remove CMakeCache.txt and CMakeFiles.") +endif() + +include_directories("${PROJECT_SOURCE_DIR}/include") + +option(TEST_WITH_SLURM "Run tests with Slurm" OFF) + +# Testing & examples are only available if this is the main app +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + message(STATUS "adding test subdir") + add_subdirectory(test) + # Example codes are here. + #add_subdirectory(examples) +endif() diff --git a/cpp/CMakePresets.json b/cpp/CMakePresets.json new file mode 100644 index 0000000..12fc7ce --- /dev/null +++ b/cpp/CMakePresets.json @@ -0,0 +1,11 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "cfg", + "generator": "Ninja", + "sourceDir": "src", + "binaryDir": "build" + } + ] +} \ No newline at end of file diff --git a/cpp/examples/README.md b/cpp/examples/README.md new file mode 100644 index 0000000..ac3209e --- /dev/null +++ b/cpp/examples/README.md @@ -0,0 +1,22 @@ +# C++ Examples using CLIPPy +This directory contains a series of examples of using CLIPPy for command +line configuration. + + +**NOTE:** These examples are trivial and are used for illustration and testing +purposes. In no way are we advocating sorting strings externally to +Python, for example. + + +## Examples + +- [howdy.cpp](howdy.cpp): Example of String input and output +- [sum.cpp](sum.cpp): Example of Number input and output +- [sort_edges.cpp](sort_edges.cpp): Example of VectorIntInt input and output, also contains optional Boolean. +- [sort_strings.cpp](sort_strings.cpp): Example of VectorStrings input and output, also contains optional Boolean. +- [grumpy.cpp](grumpy.cpp): Example of exception throwing in C++ backend. Grumpy always throws a std::runtime_error() +- [dataframe](dataframe): Example with a dataframe class using freestanding clippy functions (clippy) +- [oo-dataframe](oo-dataframe): Example with a dataframe class using clippy objects (ooclippy) + +## Building +Edit the `Makefile` as necessary and run `make`. diff --git a/cpp/examples/SimpleExamples/CMakeLists.txt b/cpp/examples/SimpleExamples/CMakeLists.txt new file mode 100644 index 0000000..8d1e73e --- /dev/null +++ b/cpp/examples/SimpleExamples/CMakeLists.txt @@ -0,0 +1,74 @@ +# Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy +# Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: MIT + +# +# This function adds an mpi example. +# +# Works with 3.11 and tested through 3.15 (not tested yet) +cmake_minimum_required(VERSION 3.14) +set(ALLOW_DUPLICATE_CUSTOM_TARGETS TRUE) + +include(FetchContent) + +find_package(Threads) +# PP: added because I got an error "Could NOT find Threads (missing: Threads_FOUND)" +# solution according to: https://github.com/alicevision/geogram/issues/2 +set(CMAKE_THREAD_LIBS_INIT "-lpthread") +set(CMAKE_HAVE_THREADS_LIBRARY 1) +set(CMAKE_USE_WIN32_THREADS_INIT 0) +set(CMAKE_USE_PTHREADS_INIT 1) +set(THREADS_PREFER_PTHREAD_FLAG ON) + +find_package(Boost 1.75 REQUIRED COMPONENTS) + +# +# Metall +find_package(Metall QUIET) +if (NOT Metall_FOUND) + #set(METALL_WORK_DIR ${CMAKE_CURRENT_BINARY_DIR}/metall-work) + set(METALL_SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/metall-src) + set(METALL_BUILD_DIR ${CMAKE_CURRENT_BINARY_DIR}/metall-build) + set(METALL_INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/metall-install) + FetchContent_Declare(Metall + GIT_REPOSITORY https://github.com/LLNL/metall.git + GIT_TAG v0.26 + ) +# SOURCE_DIR ${METALL_SOURCE_DIR} +# BINARY_DIR ${METALL_BUILD_DIR} +# CMAKE_ARGS -DINSTALL_HEADER_ONLY=ON -DCMAKE_INSTALL_PREFIX=${METALL_INSTALL_DIR} +# ) +# set(METALL_INCLUDE_DIR ${METALL_INSTALL_DIR}/include) + FetchContent_MakeAvailable(Metall) +endif () +function ( add_example example_name ) + set(example_source "${example_name}.cpp") + set(example_exe "${example_name}") + add_executable(${example_exe} ${example_source}) + target_include_directories(${example_exe} PRIVATE ${Boost_INCLUDE_DIRS} ${CMAKE_SOURCE_DIR}/../include) + target_link_libraries(${example_exe} PRIVATE Metall) + target_link_libraries(${example_exe} PRIVATE stdc++fs Threads::Threads) + if (UNIX AND NOT APPLE) + target_link_libraries(${example_exe} PRIVATE rt) + endif () + include_directories(${CMAKE_CURRENT_SOURCE_DIR}) + set(SOURCES ${example_source} ${CMAKE_SOURCE_DIR}/../include/clippy/clippy.hpp} ${CMAKE_SOURCE_DIR}/../include/experimental/cxx-compat.hpp) +endfunction() + +add_example(grumpy) +add_example(sort_edges) +add_example(sort_strings) +add_example(howdy) +add_example(sum) +add_example(return_tuple) +add_example(sort_string_edges) + +add_subdirectory(wordcounter) +add_subdirectory(dataframe) +add_subdirectory(dataframe-load) +add_subdirectory(oo-howdy) +add_subdirectory(oo-dataframe) +#add_subdirectory(mpi) +#add_subdirectory(ygm) +add_subdirectory(logic) diff --git a/cpp/examples/SimpleExamples/grumpy.cpp b/cpp/examples/SimpleExamples/grumpy.cpp new file mode 100644 index 0000000..a2cb4d5 --- /dev/null +++ b/cpp/examples/SimpleExamples/grumpy.cpp @@ -0,0 +1,16 @@ +// Copyright 2019 Lawrence Livermore National Security, LLC and other Clippy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("grumpy", "Always throws errors because he's Grumpy!"); + if (clip.parse(argc, argv)) { + return 0; + } + + throw std::runtime_error("I'm Grumpy!"); + return 0; +} diff --git a/cpp/examples/SimpleExamples/howdy.cpp b/cpp/examples/SimpleExamples/howdy.cpp new file mode 100644 index 0000000..f55f6f1 --- /dev/null +++ b/cpp/examples/SimpleExamples/howdy.cpp @@ -0,0 +1,18 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("howdy", "Formal Texan greeting."); + clip.add_required("name", "Name to greet"); + clip.returns("The greeting"); + if (clip.parse(argc, argv)) { return 0; } + + auto name = clip.get("name"); + + clip.to_return(std::string("Howdy, ") + name); + return 0; +} diff --git a/cpp/examples/SimpleExamples/return_tuple.cpp b/cpp/examples/SimpleExamples/return_tuple.cpp new file mode 100644 index 0000000..90cbf50 --- /dev/null +++ b/cpp/examples/SimpleExamples/return_tuple.cpp @@ -0,0 +1,18 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include + +int main(int argc, char **argv) { + clippy::clippy clip("return_tuple", "Always returns a tuple"); + + if (clip.parse(argc, argv)) { return 0; } + + clip.to_return(std::make_tuple("foo", 42, 3.24)); + + return 0; +} diff --git a/cpp/examples/SimpleExamples/sort_edges.cpp b/cpp/examples/SimpleExamples/sort_edges.cpp new file mode 100644 index 0000000..f189374 --- /dev/null +++ b/cpp/examples/SimpleExamples/sort_edges.cpp @@ -0,0 +1,30 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include + +int main(int argc, char **argv) { + clippy::clippy clip("sort_edges", "Sorts an edgelist"); + clip.add_required>("edges", "Unordered edgelist"); + clip.add_optional("reverse", "Sort in reverse order", false); + + clip.returns>("Sorted edgelist"); + if (clip.parse(argc, argv)) { return 0; } + + auto edges = clip.get>("edges"); + bool reverse = clip.get("reverse"); + + if (reverse) { + std::sort(edges.begin(), edges.end(), + std::greater{}); + } else { + std::sort(edges.begin(), edges.end()); + } + + clip.to_return(edges); + return 0; +} diff --git a/cpp/examples/SimpleExamples/sort_string_edges.cpp b/cpp/examples/SimpleExamples/sort_string_edges.cpp new file mode 100644 index 0000000..3d4eabb --- /dev/null +++ b/cpp/examples/SimpleExamples/sort_string_edges.cpp @@ -0,0 +1,32 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include + +using edge_array_t = std::vector>; + +int main(int argc, char **argv) { + clippy::clippy clip("sort_string_edges", "Sorts a string edgelist"); + clip.add_required("edges", "Unordered edgelist"); + clip.add_optional("reverse", "Sort in reverse order", false); + + clip.returns("Sorted edgelist"); + if (clip.parse(argc, argv)) { return 0; } + + auto edges = clip.get("edges"); + bool reverse = clip.get("reverse"); + + if (reverse) { + std::sort(edges.begin(), edges.end(), + std::greater{}); + } else { + std::sort(edges.begin(), edges.end()); + } + + clip.to_return(edges); + return 0; +} diff --git a/cpp/examples/SimpleExamples/sort_strings.cpp b/cpp/examples/SimpleExamples/sort_strings.cpp new file mode 100644 index 0000000..e0df234 --- /dev/null +++ b/cpp/examples/SimpleExamples/sort_strings.cpp @@ -0,0 +1,33 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include + +int main(int argc, char **argv) { + clippy::clippy clip("sort_strings", "Sorts an array of strings"); + clip.add_required>("strings", + "Unordered array of strings"); + clip.add_optional("reverse", "Sort in reverse order", false); + + clip.returns>("Sorted array of strings"); + if (clip.parse(argc, argv)) { return 0; } + + auto strings = clip.get>("strings"); + bool reverse = clip.get("reverse"); + + // std::cout << "after reverse. It equals " << std::boolalpha << reverse << + // std::endl; + if (reverse) { + std::sort(strings.begin(), strings.end(), + std::greater{}); + } else { + std::sort(strings.begin(), strings.end()); + } + + clip.to_return(strings); + return 0; +} diff --git a/cpp/examples/SimpleExamples/sum.cpp b/cpp/examples/SimpleExamples/sum.cpp new file mode 100644 index 0000000..aebca30 --- /dev/null +++ b/cpp/examples/SimpleExamples/sum.cpp @@ -0,0 +1,21 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("sum", "Sums to numbers"); + clip.add_required("i", "first Number"); + clip.add_required("j", "second Number"); + + clip.returns("i + j"); + if (clip.parse(argc, argv)) { return 0; } + + auto i = clip.get("i"); + auto j = clip.get("j"); + + clip.to_return(i + j); + return 0; +} diff --git a/cpp/include/clippy/clippy-object.hpp b/cpp/include/clippy/clippy-object.hpp new file mode 100644 index 0000000..d000371 --- /dev/null +++ b/cpp/include/clippy/clippy-object.hpp @@ -0,0 +1,78 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +namespace clippy { + class object; + class array; + + struct array { + using json_type = ::boost::json::array; + + array() = default; + ~array() = default; + array(const array&) = default; + array(array&&) = default; + array& operator=(const array&) = default; + array& operator=(array&&) = default; + + template + void append_json(JsonType obj) + { + data.emplace_back(std::move(obj).json()); + } + + template + void append_val(T obj) + { + data.emplace_back(std::move(obj)); + } + + json_type& json() & { return data; } + const json_type& json() const & { return data; } + json_type&& json() && { return std::move(data); } + + private: + json_type data; + }; + + struct object { + using json_type = ::boost::json::object; + + object() = default; + ~object() = default; + object(const object&) = default; + object(object&&) = default; + object& operator=(const object&) = default; + object& operator=(object&&) = default; + + object(const json_type& dat) + : data(dat) + {} + + template + void set_json(const std::string& key, JsonType val) + { + data[key] = std::move(val).json(); + } + + template + void set_val(const std::string& key, T val) + { + data[key] = ::boost::json::value_from(val); + } + + json_type& json() & { return data; } + const json_type& json() const & { return data; } + json_type&& json() && { return std::move(data); } + + private: + json_type data; + }; +} + diff --git a/cpp/include/clippy/clippy.hpp b/cpp/include/clippy/clippy.hpp new file mode 100644 index 0000000..60322d3 --- /dev/null +++ b/cpp/include/clippy/clippy.hpp @@ -0,0 +1,563 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "clippy-object.hpp" + +// #if __has_include() +// #include +// #endif + +#if __has_include("clippy-log.hpp") +#include "clippy-log.hpp" +#else +static constexpr bool LOG_JSON = false; +#endif + +#if WITH_YGM +#include +#endif /* WITH_YGM */ + +#include + +namespace clippy { + +namespace { +template +struct is_container { + enum { + value = false, + }; +}; + +template +struct is_container> { + enum { + value = true, + }; +}; + +boost::json::value asContainer(boost::json::value val, bool requiresContainer) { + if (!requiresContainer) return val; + if (val.is_array()) return val; + + boost::json::array res; + + res.emplace_back(std::move(val)); + return res; +} + +std::string clippyLogFile{"clippy.log"}; + +#if WITH_YGM +std::string userInputString; + +struct BcastInput { + void operator()(std::string inp) const { + userInputString = std::move(inp); + if (LOG_JSON) { + std::ofstream logfile{clippyLogFile, std::ofstream::app}; + + logfile << "--in-> " << userInputString << std::endl; + } + } +}; +#endif +} // namespace + +class clippy { + public: + clippy(const std::string &name, const std::string &desc) { + get_value(m_json_config, "method_name") = name; + get_value(m_json_config, "desc") = desc; + get_value(m_json_config, "version") = std::string(CLIPPY_VERSION_NAME); + } + + /// Makes a method a member of a class \ref className and documentation \ref + /// docString. + // \todo Shall we also model the module name? + // The Python serialization module has preliminary support for modules, + // but this is currently not used. + void member_of(const std::string &className, const std::string &docString) { + get_value(m_json_config, class_name_key) = className; + get_value(m_json_config, class_desc_key) = docString; + } + + ~clippy() { + const bool requiresResponse = + !(m_json_return.is_null() && m_json_state.empty() && + m_json_overwrite_args.empty() && m_json_selectors.is_null()); + + if (requiresResponse) { + int rank = 0; +#ifdef MPI_VERSION + if (::MPI_Comm_rank(MPI_COMM_WORLD, &rank) != MPI_SUCCESS) { + MPI_Abort(MPI_COMM_WORLD, EXIT_FAILURE); + } +#endif + if (rank == 0) { + write_response(std::cout); + + if (LOG_JSON) { + std::ofstream logfile{clippyLogFile, std::ofstream::app}; + + logfile << "<-out- "; + write_response(logfile); + logfile << std::endl; + } + } + } + } + + template + void log(std::ofstream &logfile, const M &msg) { + if (LOG_JSON) logfile << msg << std::flush; + } + + template + void log(const M &msg) { + if (!LOG_JSON) return; + + std::ofstream logfile{clippyLogFile, std::ofstream::app}; + log(logfile, msg); + } + + template + void add_required(const std::string &name, const std::string &desc) { + add_required_validator(name); + size_t position = m_next_position++; + get_value(m_json_config, "args", name, "desc") = desc; + get_value(m_json_config, "args", name, "position") = position; + } + + template + void add_required_state(const std::string &name, const std::string &desc) { + add_required_state_validator(name); + + get_value(m_json_config, state_key, name, "desc") = desc; + } + + template + void add_optional(const std::string &name, const std::string &desc, + const T &default_val) { + add_optional_validator(name); + get_value(m_json_config, "args", name, "desc") = desc; + get_value(m_json_config, "args", name, "position") = -1; + get_value(m_json_config, "args", name, "default_val") = + boost::json::value_from(default_val); + } + + void update_selectors( + const std::map &map_selectors) { + m_json_selectors = boost::json::value_from(map_selectors); + } + + template + void returns(const std::string &desc) { + get_value(m_json_config, returns_key, "desc") = desc; + } + + void returns_self() { + get_value(m_json_config, "returns_self") = true; + m_returns_self = true; + } + + void return_self() { m_returns_self = true; } + + template + void to_return(const T &value) { + // if (detail::get_type_name() != + // m_json_config[returns_key]["type"].get()) { + // throw std::runtime_error("clippy::to_return(value): Invalid type."); + // } + m_json_return = boost::json::value_from(value); + } + + void to_return(::clippy::object value) { + m_json_return = std::move(value).json(); + } + + void to_return(::clippy::array value) { + m_json_return = std::move(value).json(); + } + + bool parse(int argc, char **argv) { + const char *JSON_FLAG = "--clippy-help"; + const char *DRYRUN_FLAG = "--clippy-validate"; + if (argc == 2 && std::string(argv[1]) == JSON_FLAG) { + if (LOG_JSON) { + std::ofstream logfile{clippyLogFile, std::ofstream::app}; + + logfile << "<-hlp- " << m_json_config << std::endl; + } + std::cout << m_json_config; + return true; + } + std::string buf; + std::getline(std::cin, buf); + m_json_input = boost::json::parse(buf); + + if (LOG_JSON) { + std::ofstream logfile{clippyLogFile, std::ofstream::app}; + + logfile << "--in-> " << m_json_input << std::endl; + } + validate_json_input(); + + if (argc == 2 && std::string(argv[1]) == DRYRUN_FLAG) { + return true; + } + + // Good to go for reals + return false; + } + +#if WITH_YGM + bool parse(int argc, char **argv, ygm::comm &world) { + const char *JSON_FLAG = "--clippy-help"; + const char *DRYRUN_FLAG = "--clippy-validate"; + + clippyLogFile = "clippy-" + std::to_string(world.rank()) + ".log"; + + if (argc == 2 && std::string(argv[1]) == JSON_FLAG) { + if (LOG_JSON && (world.rank() == 0)) { + std::ofstream logfile{clippyLogFile, std::ofstream::app}; + + logfile << "<-hlp- " << m_json_config << std::endl; + } + + if (world.rank0()) { + std::cout << m_json_config; + } + return true; + } + + if (world.rank() == 0) { + std::getline(std::cin, userInputString); + world.async_bcast(BcastInput{}, userInputString); + } + + world.barrier(); + + m_json_input = boost::json::parse(userInputString); + + if (LOG_JSON && (world.rank() == 0)) { + std::ofstream logfile{clippyLogFile, std::ofstream::app}; + + logfile << "--in-> " << m_json_input << std::endl; + } + validate_json_input(); + + if (argc == 2 && std::string(argv[1]) == DRYRUN_FLAG) { + return true; + } + + // Good to go for reals + return false; + } +#endif /* WITH_YGM */ + + template + T get(const std::string &name) { + static constexpr bool requires_container = is_container::value; + + if (has_argument(name)) { // if the argument exists + auto foo = get_value(m_json_input, name); + return boost::json::value_to( + asContainer(get_value(m_json_input, name), requires_container)); + } else { // it's an optional + // std::cerr << "optional argument found: " + name << std::endl; + return boost::json::value_to( + asContainer(get_value(m_json_config, "args", name, "default_val"), + requires_container)); + } + } + + bool has_state(const std::string &name) const { + return has_value(m_json_input, state_key, name); + } + + template + T get_state(const std::string &name) const { + return boost::json::value_to(get_value(m_json_input, state_key, name)); + } + + template + void set_state(const std::string &name, T val) { + // if no state exists (= empty), then copy it from m_json_input if it exists + // there; + // otherwise just start with an empty state. + if (m_json_state.empty()) + if (boost::json::value *stateValue = + m_json_input.get_object().if_contains(state_key)) + m_json_state = stateValue->as_object(); + + m_json_state[name] = boost::json::value_from(val); + } + + template + void overwrite_arg(const std::string &name, const T &val) { + m_json_overwrite_args[name] = boost::json::value_from(val); + } + + bool has_argument(const std::string &name) const { + return m_json_input.get_object().contains(name); + } + + bool is_class_member_function() const try { + return m_json_config.get_object().if_contains(class_name_key) != nullptr; + } catch (const std::invalid_argument &) { + return false; + } + + private: + void write_response(std::ostream &os) const { + // construct the response object + boost::json::object json_response; + + // incl. the response if it has been set + if (m_returns_self) { + json_response["returns_self"] = true; + } else if (!m_json_return.is_null()) + json_response[returns_key] = m_json_return; + + // only communicate the state if it has been explicitly set. + // no state -> no state update + if (!m_json_state.empty()) json_response[state_key] = m_json_state; + + if (!m_json_selectors.is_null()) + json_response[selectors_key] = m_json_selectors; + + // only communicate the pass by reference arguments if explicitly set + if (!m_json_overwrite_args.empty()) + json_response["references"] = m_json_overwrite_args; + + // write the response + os << json_response << std::endl; + } + + void validate_json_input() { + for (auto &kv : m_input_validators) { + kv.second(m_json_input); + } + // TODO: Warn/Check for unknown args + } + + template + void add_optional_validator(const std::string &name) { + if (m_input_validators.count(name) > 0) { + std::stringstream ss; + ss << "CLIPPy ERROR: Cannot have duplicate argument names: " << name + << "\n"; + throw std::runtime_error(ss.str()); + } + m_input_validators[name] = [name](const boost::json::value &j) { + if (!j.get_object().contains(name)) { + return; + } // Optional, only eval if present + try { + static constexpr bool requires_container = is_container::value; + + boost::json::value_to( + asContainer(get_value(j, name), requires_container)); + } catch (const std::exception &e) { + std::stringstream ss; + ss << "CLIPPy ERROR: Optional argument " << name << ": \"" << e.what() + << "\"\n"; + throw std::runtime_error(ss.str()); + } + }; + } + + template + void add_required_validator(const std::string &name) { + if (m_input_validators.count(name) > 0) { + throw std::runtime_error("Clippy:: Cannot have duplicate argument names"); + } + m_input_validators[name] = [name](const boost::json::value &j) { + if (!j.get_object().contains(name)) { + std::stringstream ss; + ss << "CLIPPy ERROR: Required argument " << name << " missing.\n"; + throw std::runtime_error(ss.str()); + } + try { + static constexpr bool requires_container = is_container::value; + + boost::json::value_to( + asContainer(get_value(j, name), requires_container)); + } catch (const std::exception &e) { + std::stringstream ss; + ss << "CLIPPy ERROR: Required argument " << name << ": \"" << e.what() + << "\"\n"; + throw std::runtime_error(ss.str()); + } + }; + } + + template + void add_required_state_validator(const std::string &name) { + // state validator keys are prefixed with "state::" + std::string key{state_key}; + + key += "::"; + key += name; + + if (m_input_validators.count(key) > 0) { + throw std::runtime_error("Clippy:: Cannot have duplicate state names"); + } + + auto state_validator = [name](const boost::json::value &j) { + // \todo check that the path j["state"][name] exists + try { + // try access path and value conversion + boost::json::value_to( + j.as_object().at(clippy::state_key).as_object().at(name)); + //~ boost::json::value_to(get_value(j, clippy::state_key, name)); + } catch (const std::exception &e) { + std::stringstream ss; + ss << "CLIPPy ERROR: state attribute " << name << ": \"" << e.what() + << "\"\n"; + throw std::runtime_error(ss.str()); + } + }; + + m_input_validators[key] = state_validator; + } + + static constexpr bool has_value(const boost::json::value &) { return true; } + + template + static bool has_value(const boost::json::value &value, const std::string &key, + const argts &...inner_keys) { + if (const boost::json::object *obj = value.if_object()) + if (const auto pos = obj->find(key); pos != obj->end()) + return has_value(pos->value(), inner_keys...); + + return false; + } + + static boost::json::value &get_value(boost::json::value &value, + const std::string &key) { + if (!value.is_object()) { + value.emplace_object(); + } + return value.get_object()[key]; + } + + template + static boost::json::value &get_value(boost::json::value &value, + const std::string &key, + const argts &...inner_keys) { + if (!value.is_object()) { + value.emplace_object(); + } + return get_value(value.get_object()[key], inner_keys...); + } + + static const boost::json::value &get_value(const boost::json::value &value, + const std::string &key) { + return value.get_object().at(key); + } + + template + static const boost::json::value &get_value(const boost::json::value &value, + const std::string &key, + const argts &...inner_keys) { + return get_value(value.get_object().at(key), inner_keys...); + } + + boost::json::value m_json_config; + boost::json::value m_json_input; + boost::json::value m_json_return; + boost::json::value m_json_selectors; + boost::json::object m_json_state; + boost::json::object m_json_overwrite_args; + bool m_returns_self = false; + + boost::json::object *m_json_input_state = nullptr; + size_t m_next_position = 0; + + std::map> + m_input_validators; + + public: + static constexpr const char *const state_key = "_state"; + static constexpr const char *const selectors_key = "_selectors"; + static constexpr const char *const returns_key = "returns"; + static constexpr const char *const class_name_key = "class_name"; + static constexpr const char *const class_desc_key = "class_desc"; +}; + +} // namespace clippy + +namespace boost::json { +void tag_invoke(boost::json::value_from_tag, boost::json::value &jv, + const std::vector> &value) { + auto &outer_array = jv.emplace_array(); + outer_array.resize(value.size()); + + for (std::size_t i = 0; i < value.size(); ++i) { + auto &inner_array = outer_array[i].emplace_array(); + inner_array.resize(2); + inner_array[0] = value[i].first; + inner_array[1] = value[i].second; + } +} + +std::vector> tag_invoke( + boost::json::value_to_tag>>, + const boost::json::value &jv) { + std::vector> value; + + auto &outer_array = jv.get_array(); + for (const auto &inner_value : outer_array) { + const auto &inner_array = inner_value.get_array(); + value.emplace_back( + std::make_pair(inner_array[0].as_int64(), inner_array[1].as_int64())); + } + + return value; +} + +void tag_invoke(boost::json::value_from_tag, boost::json::value &jv, + const std::vector> &value) { + auto &outer_array = jv.emplace_array(); + outer_array.resize(value.size()); + + for (std::size_t i = 0; i < value.size(); ++i) { + auto &inner_array = outer_array[i].emplace_array(); + inner_array.resize(2); + inner_array[0] = value[i].first; + inner_array[1] = value[i].second; + } +} + +std::vector> tag_invoke( + boost::json::value_to_tag>>, + const boost::json::value &jv) { + std::vector> value; + + auto &outer_array = jv.get_array(); + for (const auto &inner_value : outer_array) { + const auto &inner_array = inner_value.get_array(); + value.emplace_back( + std::make_pair(std::string(inner_array[0].as_string().c_str()), + std::string(inner_array[1].as_string().c_str()))); + } + + return value; +} +} // namespace boost::json diff --git a/cpp/include/clippy/selector.hpp b/cpp/include/clippy/selector.hpp new file mode 100644 index 0000000..af198ab --- /dev/null +++ b/cpp/include/clippy/selector.hpp @@ -0,0 +1,70 @@ +#pragma once +#include +#include +#include + +#include "boost/json.hpp" + +class selector { + std::string sel_str; + + std::vector dots; + + static std::vector make_dots(const std::string &sel_str) { + std::vector dots; + for (size_t i = 0; i < sel_str.size(); ++i) { + if (sel_str[i] == '.') { + dots.push_back(i); + } + } + dots.push_back(sel_str.size()); + return dots; + } + + public: + friend std::ostream &operator<<(std::ostream &os, const selector &sel); + friend void tag_invoke(boost::json::value_from_tag /*unused*/, + boost::json::value &v, const selector &sel); + explicit selector(const std::string &sel) + : sel_str(sel), dots(make_dots(sel_str)) {} + explicit selector(const char *sel) : selector(std::string(sel)) {} + selector(boost::json::object o) { + auto v = o.at("rule").as_object()["var"]; + sel_str = v.as_string().c_str(); + dots = make_dots(sel_str); + } + bool operator<(const selector &other) const { + return sel_str < other.sel_str; + } + operator std::string() { return sel_str; } + operator std::string() const { return sel_str; } + bool headeq(const std::string &comp) const { + return std::string_view(sel_str).substr(0, dots[0]) == comp; + } + std::optional tail() const { + if (dots.size() <= 1) { // remember that end of string is a dot + return std::nullopt; + } + return selector(sel_str.substr(dots[0] + 1)); + } +}; + +std::ostream &operator<<(std::ostream &os, const selector &sel) { + os << sel.sel_str; + return os; +} + +selector tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value &v) { + return v.as_object(); +} + +void tag_invoke(boost::json::value_from_tag /*unused*/, boost::json::value &v, + const selector &sel) { + std::cerr << "This should not be called." << std::endl; + // std::map o {}; + // o["expression_type"] = "jsonlogic"; + // o["rule"] = {{"var", sel.sel_str}}; + // v = {"expression_type": "jsonlogic", "rule": {"var": + // "node.degree"}}}sel.sel_str; +} \ No newline at end of file diff --git a/cpp/include/clippy/version.hpp b/cpp/include/clippy/version.hpp new file mode 100644 index 0000000..8421cf2 --- /dev/null +++ b/cpp/include/clippy/version.hpp @@ -0,0 +1,6 @@ +#pragma once + +#define CLIPPY_VERSION_MAJOR 0 +#define CLIPPY_VERSION_MINOR 5 +#define CLIPPY_VERSION_PATCH 0 +#define CLIPPY_VERSION_NAME "0.5.0" diff --git a/cpp/test/CMakeLists.txt b/cpp/test/CMakeLists.txt new file mode 100644 index 0000000..25c58bb --- /dev/null +++ b/cpp/test/CMakeLists.txt @@ -0,0 +1,30 @@ +# Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy +# Project Developers. See the top-level COPYRIGHT file for details. +# +# SPDX-License-Identifier: MIT + +# +# This function adds a test. +# +function ( add_test class_name method_name ) + set(source "${method_name}.cpp") + set(target "${class_name}_${method_name}") + add_executable(${target} ${source}) + # target_include_directories(${target} PRIVATE ${Boost_INCLUDE_DIRS}) + include_directories(${CMAKE_CURRENT_SOURCE_DIR} include/) + set_target_properties(${target} PROPERTIES OUTPUT_NAME "${method_name}" ) + target_include_directories(${target} PRIVATE + ${PROJECT_SOURCE_DIR}/include + ${BOOST_INCLUDE_DIRS} + include/ + ${jsonlogic_SOURCE_DIR}/cpp/include + ) + target_link_libraries(${target} PRIVATE Boost::json) +endfunction() + + +add_subdirectory(TestBag) +add_subdirectory(TestSet) +add_subdirectory(TestFunctions) +add_subdirectory(TestSelector) +add_subdirectory(TestGraph) diff --git a/cpp/test/TestBag/CMakeLists.txt b/cpp/test/TestBag/CMakeLists.txt new file mode 100644 index 0000000..1c3259d --- /dev/null +++ b/cpp/test/TestBag/CMakeLists.txt @@ -0,0 +1,11 @@ +add_test(TestBag __init__) +add_test(TestBag __str__) +add_test(TestBag insert) +add_test(TestBag remove) +add_test(TestBag remove_if) +add_test(TestBag size) +add_custom_command( + TARGET TestBag_size POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/meta.json + ${CMAKE_CURRENT_BINARY_DIR}/meta.json) \ No newline at end of file diff --git a/cpp/test/TestBag/__init__.cpp b/cpp/test/TestBag/__init__.cpp new file mode 100644 index 0000000..f75e6cc --- /dev/null +++ b/cpp/test/TestBag/__init__.cpp @@ -0,0 +1,27 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "__init__"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Initializes a TestBag of strings"}; + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + std::vector the_bag; + clip.set_state(state_name, the_bag); + + return 0; +} diff --git a/cpp/test/TestBag/__str__.cpp b/cpp/test/TestBag/__str__.cpp new file mode 100644 index 0000000..119a955 --- /dev/null +++ b/cpp/test/TestBag/__str__.cpp @@ -0,0 +1,38 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "__str__"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Str method for TestBag"}; + + clip.add_required_state>(state_name, + "Internal container"); + + clip.returns("String of data."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_bag = clip.get_state>(state_name); + clip.set_state(state_name, the_bag); + + std::stringstream sstr; + for (auto item : the_bag) { + sstr << item << " "; + } + clip.to_return(sstr.str()); + + return 0; +} diff --git a/cpp/test/TestBag/insert.cpp b/cpp/test/TestBag/insert.cpp new file mode 100644 index 0000000..031a978 --- /dev/null +++ b/cpp/test/TestBag/insert.cpp @@ -0,0 +1,31 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include + +static const std::string class_name = "ClippyBag"; +static const std::string method_name = "insert"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Inserts a string into a ClippyBag"}; + clip.add_required("item", "Item to insert"); + clip.add_required_state>(state_name, + "Internal container"); + clip.returns_self(); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto item = clip.get("item"); + auto the_bag = clip.get_state>(state_name); + the_bag.push_back(item); + clip.set_state(state_name, the_bag); + clip.return_self(); + return 0; +} diff --git a/cpp/test/TestBag/meta.json b/cpp/test/TestBag/meta.json new file mode 100644 index 0000000..6992f16 --- /dev/null +++ b/cpp/test/TestBag/meta.json @@ -0,0 +1,6 @@ +{ + "__doc__" : "A bag data structure", + "initial_selectors" : { + "value" : "A value in the container" + } +} \ No newline at end of file diff --git a/cpp/test/TestBag/remove.cpp b/cpp/test/TestBag/remove.cpp new file mode 100644 index 0000000..0ac4572 --- /dev/null +++ b/cpp/test/TestBag/remove.cpp @@ -0,0 +1,44 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "remove"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + + clippy::clippy clip{method_name, "Removes a string from a TestBag"}; + clip.add_required("item", "Item to remove"); + clip.add_optional("all", "Remove all?", false); + clip.add_required_state>(state_name, + "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto item = clip.get("item"); + bool all = clip.get("all"); + auto the_bag = clip.get_state>(state_name); + if (all) { + the_bag.remove(item); + } else { + auto itr = std::find(the_bag.begin(), the_bag.end(), item); + if (itr != the_bag.end()) { + the_bag.erase(itr); + } + } + clip.set_state(state_name, the_bag); + clip.return_self(); + return 0; +} diff --git a/cpp/test/TestBag/remove_if.cpp b/cpp/test/TestBag/remove_if.cpp new file mode 100644 index 0000000..9bcb6ab --- /dev/null +++ b/cpp/test/TestBag/remove_if.cpp @@ -0,0 +1,47 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +// #include + +namespace boostjsn = boost::json; + +static const std::string class_name = "ClippyBag"; +static const std::string method_name = "remove_if"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Removes a string from a ClippyBag"}; + clip.add_required("expression", "Remove If Expression"); + clip.add_required_state>(state_name, "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto expression = clip.get("expression"); + auto the_bag = clip.get_state>(state_name); + + // + // Expression here + jsonlogic::logic_rule jlrule = jsonlogic::create_logic(expression["rule"]); + + auto apply_jl = [&jlrule](int value) { + boostjsn::object data; + data["value"] = value; + auto res = jlrule.apply(jsonlogic::json_accessor(data)); + return jsonlogic::unpack_value(res); + }; + + the_bag.remove_if(apply_jl); + + clip.set_state(state_name, the_bag); + clip.return_self(); + return 0; +} diff --git a/cpp/test/TestBag/size.cpp b/cpp/test/TestBag/size.cpp new file mode 100644 index 0000000..2ab35ab --- /dev/null +++ b/cpp/test/TestBag/size.cpp @@ -0,0 +1,30 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "size"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Returns the size of the bag"}; + + clip.returns("Size of bag."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_bag = clip.get_state>(state_name); + + clip.to_return(the_bag.size()); + return 0; +} diff --git a/cpp/test/TestFunctions/CMakeLists.txt b/cpp/test/TestFunctions/CMakeLists.txt new file mode 100644 index 0000000..77314a4 --- /dev/null +++ b/cpp/test/TestFunctions/CMakeLists.txt @@ -0,0 +1,11 @@ +add_test(TestFunctions missing_version) +add_test(TestFunctions old_version) +add_test(TestFunctions returns_string) +add_test(TestFunctions returns_int) +add_test(TestFunctions returns_bool) +add_test(TestFunctions returns_vec_int) +add_test(TestFunctions returns_dict) +add_test(TestFunctions call_with_string) +add_test(TestFunctions call_with_optional_string) +add_test(TestFunctions pass_by_reference_dict) +add_test(TestFunctions pass_by_reference_vector) diff --git a/cpp/test/TestFunctions/call_with_optional_string.cpp b/cpp/test/TestFunctions/call_with_optional_string.cpp new file mode 100644 index 0000000..596de4d --- /dev/null +++ b/cpp/test/TestFunctions/call_with_optional_string.cpp @@ -0,0 +1,20 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("call_with_optional_string", "Call With Optional String"); + clip.add_optional("name", "Optional String", "World"); + clip.returns("Returns a string"); + if (clip.parse(argc, argv)) { + return 0; + } + + auto name = clip.get("name"); + + clip.to_return(std::string("Howdy, ") + name); + return 0; +} diff --git a/cpp/test/TestFunctions/call_with_string.cpp b/cpp/test/TestFunctions/call_with_string.cpp new file mode 100644 index 0000000..830d718 --- /dev/null +++ b/cpp/test/TestFunctions/call_with_string.cpp @@ -0,0 +1,18 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("call_with_string", "Call With String"); + clip.add_required("name", "Required String"); + clip.returns("Returns a string"); + if (clip.parse(argc, argv)) { return 0; } + + auto name = clip.get("name"); + + clip.to_return(std::string("Howdy, ") + name); + return 0; +} diff --git a/cpp/test/TestFunctions/missing_version.cpp b/cpp/test/TestFunctions/missing_version.cpp new file mode 100644 index 0000000..c10d972 --- /dev/null +++ b/cpp/test/TestFunctions/missing_version.cpp @@ -0,0 +1,7 @@ +#include + +int main(int argc, char **arv) { + std::cout + << R"json({"method_name":"missing_version","desc":"Tests returning a string - missing a version spec","returns":{"desc":"The string"}})json"; + return 0; +} diff --git a/cpp/test/TestFunctions/old_version.cpp b/cpp/test/TestFunctions/old_version.cpp new file mode 100644 index 0000000..92de8e8 --- /dev/null +++ b/cpp/test/TestFunctions/old_version.cpp @@ -0,0 +1,7 @@ +#include + +int main(int argc, char **arv) { + std::cout + << R"json({"method_name":"old_version","desc":"Tests returning a string - outdated version spec","version":"0.1.0","returns":{"desc":"The string"}})json"; + return 0; +} diff --git a/cpp/test/TestFunctions/pass_by_reference_dict.cpp b/cpp/test/TestFunctions/pass_by_reference_dict.cpp new file mode 100644 index 0000000..983d2f0 --- /dev/null +++ b/cpp/test/TestFunctions/pass_by_reference_dict.cpp @@ -0,0 +1,18 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("pass_by_reference_dict", "Call with dict"); + clip.add_required>("debug_info", "Required dict"); + + if (clip.parse(argc, argv)) { return 0; } + + std::map m = {{"dummy_key", "dummy_value"}}; + + clip.overwrite_arg("debug_info", m); + return 0; +} diff --git a/cpp/test/TestFunctions/pass_by_reference_vector.cpp b/cpp/test/TestFunctions/pass_by_reference_vector.cpp new file mode 100644 index 0000000..3d96a6b --- /dev/null +++ b/cpp/test/TestFunctions/pass_by_reference_vector.cpp @@ -0,0 +1,18 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("pass_by_reference_vector", "Call with vector"); + clip.add_required>("vec", "Required vector"); + + if (clip.parse(argc, argv)) { return 0; } + + std::vector vec = {5,4,3,2,1}; + + clip.overwrite_arg("vec", vec); + return 0; +} diff --git a/cpp/test/TestFunctions/returns_bool.cpp b/cpp/test/TestFunctions/returns_bool.cpp new file mode 100644 index 0000000..bd12e6c --- /dev/null +++ b/cpp/test/TestFunctions/returns_bool.cpp @@ -0,0 +1,16 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("returns_bool", "Tests returning a bool"); + clip.returns("The bool"); + if (clip.parse(argc, argv)) { return 0; } + + + clip.to_return(true); + return 0; +} diff --git a/cpp/test/TestFunctions/returns_dict.cpp b/cpp/test/TestFunctions/returns_dict.cpp new file mode 100644 index 0000000..f8cf485 --- /dev/null +++ b/cpp/test/TestFunctions/returns_dict.cpp @@ -0,0 +1,21 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include + +int main(int argc, char **argv) { + clippy::clippy clip("returns_dict", "Tests returning a dict"); + clip.returns>("The Dict"); + if (clip.parse(argc, argv)) { + return 0; + } + + std::unordered_map m1{{"a", 1}, {"b", 2}, {"c", 3}}; + // std::unordered_map> + // big_m; + clip.to_return(m1); + return 0; +} diff --git a/cpp/test/TestFunctions/returns_int.cpp b/cpp/test/TestFunctions/returns_int.cpp new file mode 100644 index 0000000..ba2a9cb --- /dev/null +++ b/cpp/test/TestFunctions/returns_int.cpp @@ -0,0 +1,16 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("returns_int", "Tests returning a int"); + clip.returns("The Int"); + if (clip.parse(argc, argv)) { return 0; } + + + clip.to_return(size_t(42)); + return 0; +} diff --git a/cpp/test/TestFunctions/returns_string.cpp b/cpp/test/TestFunctions/returns_string.cpp new file mode 100644 index 0000000..6a09206 --- /dev/null +++ b/cpp/test/TestFunctions/returns_string.cpp @@ -0,0 +1,16 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("returns_string", "Tests returning a string"); + clip.returns("The string"); + if (clip.parse(argc, argv)) { return 0; } + + + clip.to_return(std::string("asdf")); + return 0; +} diff --git a/cpp/test/TestFunctions/returns_vec_int.cpp b/cpp/test/TestFunctions/returns_vec_int.cpp new file mode 100644 index 0000000..be61e94 --- /dev/null +++ b/cpp/test/TestFunctions/returns_vec_int.cpp @@ -0,0 +1,16 @@ +// Copyright 2020 Lawrence Livermore National Security, LLC and other CLIPPy Project Developers. +// See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include + +int main(int argc, char **argv) { + clippy::clippy clip("returns_vec_int", "Tests returning a vector of int"); + clip.returns>("The vec"); + if (clip.parse(argc, argv)) { return 0; } + + std::vector to_return = {0,1,2,3,4,5}; + clip.to_return(to_return); + return 0; +} diff --git a/cpp/test/TestGraph/CMakeLists.txt b/cpp/test/TestGraph/CMakeLists.txt new file mode 100644 index 0000000..c083d02 --- /dev/null +++ b/cpp/test/TestGraph/CMakeLists.txt @@ -0,0 +1,25 @@ +set(CMAKE_BUILD_TYPE Debug) + + +add_test(TestGraph __init__) +add_test(TestGraph __str__) +# add_test(TestGraph assign) +# add_test(TestGraph dump) +add_test(TestGraph dump2) +add_test(TestGraph add_edge) +add_test(TestGraph add_node) +add_test(TestGraph nv) +add_test(TestGraph ne) +add_test(TestGraph degree) +add_test(TestGraph add_series) +add_test(TestGraph connected_components) +add_test(TestGraph drop_series) +add_test(TestGraph copy_series) +add_test(TestGraph series_str) +add_test(TestGraph extrema) +add_test(TestGraph count) +add_custom_command( + TARGET TestGraph_nv POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/meta.json + ${CMAKE_CURRENT_BINARY_DIR}/meta.json) diff --git a/cpp/test/TestGraph/__init__.cpp b/cpp/test/TestGraph/__init__.cpp new file mode 100644 index 0000000..34fc260 --- /dev/null +++ b/cpp/test/TestGraph/__init__.cpp @@ -0,0 +1,31 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "__init__"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Initializes a TestGraph"}; + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + // testgraph needs to be convertible to json + testgraph::testgraph the_graph; + clip.set_state(state_name, the_graph); + std::map selectors; + clip.set_state(sel_state_name, selectors); + + return 0; +} diff --git a/cpp/test/TestGraph/__str__.cpp b/cpp/test/TestGraph/__str__.cpp new file mode 100644 index 0000000..a0cd317 --- /dev/null +++ b/cpp/test/TestGraph/__str__.cpp @@ -0,0 +1,39 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "__str__"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Str method for TestGraph"}; + + clip.add_required_state(state_name, + "Internal container"); + + clip.returns("String of data."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_graph = clip.get_state(state_name); + clip.set_state(state_name, the_graph); + + std::stringstream sstr; + sstr << "Graph with " << the_graph.nv() << " nodes and " << the_graph.ne() + << " edges"; + + clip.to_return(sstr.str()); + + return 0; +} diff --git a/cpp/test/TestGraph/add.cpp b/cpp/test/TestGraph/add.cpp new file mode 100644 index 0000000..481b290 --- /dev/null +++ b/cpp/test/TestGraph/add.cpp @@ -0,0 +1,57 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "add"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Adds a subselector"}; + clip.add_required("selector", "Parent Selector"); + clip.add_required("subname", "Description of new selector"); + clip.add_optional("desc", "Description", "EMPTY DESCRIPTION"); + clip.add_required_state>( + "selector_state", "Internal container"); + + if (clip.parse(argc, argv)) { + return 0; + } + + std::map sstate; + if (clip.has_state("selector_state")) { + sstate = + clip.get_state>("selector_state"); + } + + auto jo = clip.get("selector"); + std::string subname = clip.get("subname"); + std::string desc = clip.get("desc"); + + std::string parentname; + try { + if (jo["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + parentname = jo["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + sstate[parentname + "." + subname] = desc; + + clip.set_state("selector_state", sstate); + clip.update_selectors(sstate); + clip.return_self(); + + return 0; +} diff --git a/cpp/test/TestGraph/add_edge.cpp b/cpp/test/TestGraph/add_edge.cpp new file mode 100644 index 0000000..cc00d9f --- /dev/null +++ b/cpp/test/TestGraph/add_edge.cpp @@ -0,0 +1,35 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "add_edge"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Inserts (src, dst) into a TestGraph"}; + clip.add_required("src", "source node"); + clip.add_required("dst", "dest node"); + clip.add_required_state(state_name, + "Internal container"); + clip.returns_self(); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto src = clip.get("src"); + auto dst = clip.get("dst"); + auto the_graph = clip.get_state(state_name); + the_graph.add_edge(src, dst); + clip.set_state(state_name, the_graph); + clip.return_self(); + return 0; +} diff --git a/cpp/test/TestGraph/add_node.cpp b/cpp/test/TestGraph/add_node.cpp new file mode 100644 index 0000000..b043660 --- /dev/null +++ b/cpp/test/TestGraph/add_node.cpp @@ -0,0 +1,33 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "add_node"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Inserts a node into a TestGraph"}; + clip.add_required("node", "node to insert"); + clip.add_required_state(state_name, + "Internal container"); + clip.returns_self(); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto node = clip.get("node"); + auto the_graph = clip.get_state(state_name); + the_graph.add_node(node); + clip.set_state(state_name, the_graph); + clip.return_self(); + return 0; +} diff --git a/cpp/test/TestGraph/add_series.cpp b/cpp/test/TestGraph/add_series.cpp new file mode 100644 index 0000000..fb18c73 --- /dev/null +++ b/cpp/test/TestGraph/add_series.cpp @@ -0,0 +1,79 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include + +#include "clippy/clippy.hpp" +#include "clippy/selector.hpp" +#include "testgraph.hpp" + +namespace boostjsn = boost::json; + +static const std::string method_name = "add_series"; +static const std::string graph_state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Adds a subselector"}; + clip.add_required("parent_sel", "Parent Selector"); + clip.add_required("sub_sel", "Name of new selector"); + clip.add_optional("desc", "Description of new selector", ""); + + clip.add_required_state>( + sel_state_name, "Internal container for pending selectors"); + clip.add_required_state(graph_state_name, + "Internal state for the graph"); + + if (clip.parse(argc, argv)) { + return 0; + } + + std::string parstr = clip.get("parent_sel"); + auto parsel = selector{parstr}; + auto subsel = clip.get("sub_sel"); + auto desc = clip.get("desc"); + + std::string fullname = parstr + "." + subsel; + + // std::map selectors; + auto the_graph = clip.get_state(graph_state_name); + + if (parsel.headeq("edge")) { + if (the_graph.has_edge_series(subsel)) { + std::cerr << "!! ERROR: Selector name already exists in edge table !!" + << std::endl; + exit(-1); + } + } else if (parsel.headeq("node")) { + if (the_graph.has_node_series(subsel)) { + std::cerr << "!! ERROR: Selector name already exists in node table !!" + << std::endl; + exit(-1); + } + } else { + std::cerr + << "((!! ERROR: Parent must be either \"edge\" or \"node\" (received " + << parstr << ") !!)"; + exit(-1); + } + + if (clip.has_state(sel_state_name)) { + auto selectors = + clip.get_state>(sel_state_name); + if (selectors.contains(fullname)) { + std::cerr << "Warning: Selector name already exists; ignoring" + << std::endl; + } else { + selectors[fullname] = desc; + clip.set_state(sel_state_name, selectors); + clip.update_selectors(selectors); + clip.return_self(); + } + } + + return 0; +} diff --git a/cpp/test/TestGraph/assign.cpp b/cpp/test/TestGraph/assign.cpp new file mode 100644 index 0000000..ffeb548 --- /dev/null +++ b/cpp/test/TestGraph/assign.cpp @@ -0,0 +1,341 @@ + + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include + +#include "clippy/selector.hpp" +#include "testgraph.hpp" +#include "where.cpp" + +static const std::string method_name = "assign"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +static const std::string always_true = R"({"rule":{"==":[1,1]}})"; +static const std::string never_true = R"({"rule":{"==":[2,1]}})"; + +static const boost::json::object always_true_obj = + boost::json::parse(always_true).as_object(); + +using variants = + std::variant; + +std::optional obj_to_val(boost::json::object expr, + std::string extract = "var") { + if (expr["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + return std::nullopt; + } + + boost::json::value v; + v = expr["rule"].as_object()[extract]; + return v; +} +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Populates a column with a value"}; + clip.add_required( + "selector", + "Existing selector name into which the value will be written"); + + clip.add_required("value", "the value to assign"); + + clip.add_optional("where", "where filter", + always_true_obj); + + clip.add_optional("desc", "Description of the series", ""); + + clip.add_required_state(state_name, + "Internal container"); + clip.add_required_state>( + sel_state_name, "Internal container for selectors"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto sel_json = clip.get("selector"); + auto sel_str_opt = obj_to_val(sel_json); + if (!sel_str_opt.has_value()) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + std::string sel_str = sel_str_opt.value().as_string().c_str(); + + auto sel = selector(sel_str); + bool is_node_sel = sel.headeq("node"); + if (!is_node_sel && !sel.headeq("edge")) { + std::cerr << "Selector must start with \"node\" or \"edge\"" << std::endl; + return 1; + } + + auto sel_tail_opt = sel.tail(); + if (!sel_tail_opt.has_value()) { + std::cerr << "Selector must have a tail" << std::endl; + return 1; + } + + selector subsel = sel_tail_opt.value(); + auto the_graph = clip.get_state(state_name); + + auto selectors = + clip.get_state>(sel_state_name); + if (!selectors.contains(sel)) { + std::cerr << "Selector not found" << std::endl; + return 1; + } + + auto desc = clip.get("desc"); + + auto val = clip.get("value"); + + auto where_exp = clip.get("where"); + + if (where_exp["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << "!! ERROR in where statement !!" << std::endl; + exit(-1); + } + + boost::json::object submission_data; + + std::cerr << "val = " << val << ", val.kind() = " << val.kind() << std::endl; + if (is_node_sel) { + if (the_graph.has_node_series(subsel)) { + std::cerr << "Selector already populated" << std::endl; + return 1; + } + + auto nodemap = the_graph.nodemap(); + auto where = parse_where_expression(nodemap, where_exp, submission_data); + if (std::holds_alternative(val)) { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = std::get(val); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + } else if (std::holds_alternative(val)) { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = std::get(val); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + } else if (std::holds_alternative(val)) { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = std::get(val); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + } else if (std::holds_alternative(val)) { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = std::get(val).c_str(); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + } else if (std::holds_alternative(val)) { + auto col_opt = + the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + case boost::json::kind::bool_: { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_bool(); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + + break; + } + case boost::json::kind::double_: { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_double(); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + + break; + } + + case boost::json::kind::int64: { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_int64(); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + break; + } + + case boost::json::kind::string: { + auto col_opt = the_graph.add_node_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_string().c_str(); + the_graph.for_all_nodes( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + break; + } + default: + std::cerr << "Unsupported type" << std::endl; + return 1; + } + } else { // edge map + if (the_graph.has_edge_series(subsel)) { + std::cerr << "Selector already populated" << std::endl; + return 1; + } + + auto edgemap = the_graph.edgemap(); + auto where = parse_where_expression(edgemap, where_exp, submission_data); + switch (val.kind()) { + case boost::json::kind::bool_: { + auto col_opt = the_graph.add_edge_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest edge series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_bool(); + the_graph.for_all_edges( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + break; + } + case boost::json::kind::double_: { + auto col_opt = the_graph.add_edge_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest edge series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_double(); + the_graph.for_all_edges( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + break; + } + + case boost::json::kind::int64: { + auto col_opt = the_graph.add_edge_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest edge series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_int64(); + the_graph.for_all_edges( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + + break; + } + + case boost::json::kind::string: { + auto col_opt = the_graph.add_edge_series(subsel, desc); + if (!col_opt.has_value()) { + std::cerr << "Unable to manifest edge series" << std::endl; + return 1; + } + auto col = col_opt.value(); + auto v = val.as_string().c_str(); + the_graph.for_all_edges( + [&col, &v, &where](auto /* unused */, mvmap::locator l) { + if (where(l)) { + col[l] = v; + } + }); + break; + } + default: + std::cerr << "Unsupported type" << std::endl; + return 1; + } + } + + clip.set_state(state_name, the_graph); + clip.set_state(sel_state_name, selectors); + clip.update_selectors(selectors); + + clip.return_self(); + return 0; +} diff --git a/cpp/test/TestGraph/bfs b/cpp/test/TestGraph/bfs new file mode 100755 index 0000000000000000000000000000000000000000..fad136a1f092e45cec454a7b12bbc79c25b9b33d GIT binary patch literal 204440 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 +#include +#include + +int main() { + std::vector> adj{{1}, {0, 2}, {1}, {4}, {3}}; + std::vector components(adj.size()); + std::iota(components.begin(), components.end(), 0); + + std::vector curr_level{0}; + std::vector next_level{}; + std::vector visited(adj.size(), false); + + long int u{}; + + while (!curr_level.empty()) { + u = curr_level.back(); + std::cout << "u = " << u << "\n"; + curr_level.pop_back(); + for (long int v : adj[u]) { + std::cout << " testing " << v << "\n"; + if (!visited[v]) { + std::cout << " visiting " << v << "\n"; + visited[v] = true; + components[v] = components[u]; + std::cout << " components[" << v << "] = " << components[u] << "\n"; + next_level.push_back(v); + } + std::cout << "swapping\n"; + std::swap(next_level, curr_level); + next_level.clear(); + } + } +} diff --git a/cpp/test/TestGraph/connected_components.cpp b/cpp/test/TestGraph/connected_components.cpp new file mode 100644 index 0000000..3316f0a --- /dev/null +++ b/cpp/test/TestGraph/connected_components.cpp @@ -0,0 +1,124 @@ + + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include + +#include "testgraph.hpp" + +static const std::string method_name = "connected_components"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{ + method_name, + "Populates a column containing the component id of each node in a graph"}; + clip.add_required( + "selector", + "Existing selector name into which the component id will be written"); + clip.add_required_state(state_name, + "Internal container"); + clip.add_required_state>( + sel_state_name, "Internal container for selectors"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto sel_json = clip.get("selector"); + + std::string sel; + try { + if (sel_json["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + sel = sel_json["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + if (!sel.starts_with("node.")) { + std::cerr << "Selector must be a node subselector" << std::endl; + return 1; + } + auto the_graph = clip.get_state(state_name); + + auto selectors = + clip.get_state>(sel_state_name); + if (!selectors.contains(sel)) { + std::cerr << "Selector not found" << std::endl; + return 1; + } + auto subsel = sel.substr(5); + if (the_graph.has_node_series(subsel)) { + std::cerr << "Selector already populated" << std::endl; + return 1; + } + + auto cc_o = the_graph.add_node_series(subsel, selectors.at(sel)); + if (!cc_o) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + + auto cc = cc_o.value(); + std::map ccmap; + + int64_t i = 0; + for (auto &node : the_graph.nodes()) { + ccmap[node] = i++; + } + std::vector> adj(the_graph.nv()); + the_graph.for_all_edges([&adj, &ccmap](auto edge, mvmap::locator /*unused*/) { + long i = ccmap[edge.first]; + long j = ccmap[edge.second]; + adj[i].push_back(j); + adj[j].push_back(i); + }); + + std::vector visited(the_graph.nv(), false); + std::vector components(the_graph.nv()); + std::iota(components.begin(), components.end(), 0); + + for (int64_t i = 0; i < the_graph.nv(); ++i) { + if (!visited[i]) { + std::queue q; + q.push(i); + while (!q.empty()) { + int64_t v = q.front(); + q.pop(); + visited[v] = true; + for (int64_t u : adj[v]) { + if (!visited[u]) { + q.push(u); + components[u] = components[i]; + } + } + } + } + } + + the_graph.for_all_nodes( + [&components, &ccmap, &cc](auto node, mvmap::locator /*unused*/) { + int64_t i = ccmap[node]; + cc[node] = components[i]; + }); + + clip.set_state(state_name, the_graph); + // clip.set_state(sel_state_name, selectors); + // clip.update_selectors(selectors); + + clip.return_self(); + return 0; +} diff --git a/cpp/test/TestGraph/copy_series.cpp b/cpp/test/TestGraph/copy_series.cpp new file mode 100644 index 0000000..8c0f766 --- /dev/null +++ b/cpp/test/TestGraph/copy_series.cpp @@ -0,0 +1,92 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include + +#include "clippy/clippy.hpp" +#include "clippy/selector.hpp" +#include "testgraph.hpp" + +namespace boostjsn = boost::json; + +static const std::string method_name = "copy_series"; +static const std::string graph_state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Copies a subselector to a new subselector"}; + clip.add_required("from_sel", "Source Selector"); + clip.add_required("to_sel", "Target Selector"); + + clip.add_required_state>( + sel_state_name, "Internal container for pending selectors"); + clip.add_required_state(graph_state_name, + "Internal state for the graph"); + + if (clip.parse(argc, argv)) { + return 0; + } + + auto from_selector = clip.get("from_sel"); + auto to_selector = clip.get("to_sel"); + + auto the_graph = clip.get_state(graph_state_name); + + bool edge_sel = false; // if false, then node selector + if (testgraph::testgraph::is_edge_selector(from_selector)) { + edge_sel = true; + } else if (!testgraph::testgraph::is_node_selector(from_selector)) { + std::cerr << "!! ERROR: from_selector must start with either \"edge\" or " + "\"node\" (received " + << from_selector << ") !!"; + exit(-1); + } + + if (the_graph.has_series(to_selector)) { + std::cerr << "!! ERROR: Selector name " << to_selector + << " already exists in graph !!" << std::endl; + exit(-1); + } + + if (clip.has_state(sel_state_name)) { + auto selectors = + clip.get_state>(sel_state_name); + if (selectors.contains(to_selector)) { + std::cerr << "Warning: Using unmanifested selector." << std::endl; + selectors.erase(to_selector); + } + auto from_selector_tail = from_selector.tail(); + auto to_selector_tail = to_selector.tail(); + if (!from_selector_tail.has_value() || !to_selector_tail.has_value()) { + std::cerr + << "!! ERROR: from_selector and to_selector must have content !!" + << std::endl; + exit(1); + } + from_selector = from_selector_tail.value(); + to_selector = to_selector_tail.value(); + if (edge_sel) { + if (!the_graph.copy_edge_series(from_selector, to_selector)) { + std::cerr << "!! ERROR: copy failed from " << from_selector << " to " + << to_selector << "!!" << std::endl; + exit(1); + }; + } else { + if (!the_graph.copy_node_series(from_selector, to_selector)) { + std::cerr << "!! ERROR: copy failed from " << from_selector << " to " + << to_selector << "!!" << std::endl; + exit(1); + }; + } + } + + clip.set_state(graph_state_name, the_graph); + clip.return_self(); + + return 0; +} diff --git a/cpp/test/TestGraph/count.cpp b/cpp/test/TestGraph/count.cpp new file mode 100644 index 0000000..e7407fc --- /dev/null +++ b/cpp/test/TestGraph/count.cpp @@ -0,0 +1,124 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include + +#include "clippy/selector.hpp" +#include "testgraph.hpp" + +static const std::string method_name = "count"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, + "returns a map containing the count of values in a " + "series based on selector"}; + clip.add_required("selector", + "Existing selector name to calculate extrema"); + clip.add_required_state(state_name, + "Internal container"); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + auto sel_str = clip.get("selector"); + selector sel{sel_str}; + + auto the_graph = clip.get_state(state_name); + + bool is_edge_sel = testgraph::testgraph::is_edge_selector(sel); + bool is_node_sel = testgraph::testgraph::is_node_selector(sel); + + if (!is_edge_sel && !is_node_sel) { + std::cerr << "Selector must start with either \"edge\" or \"node\"" + << std::endl; + return 1; + } + + auto tailsel_opt = sel.tail(); + if (!tailsel_opt) { + std::cerr << "no tail" << std::endl; + return 1; + } + + auto tail_sel = tailsel_opt.value(); + if (is_edge_sel) { + if (the_graph.has_series(sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else { + std::cerr << "UNKNOWN TYPE" << std::endl; + return 1; + } + } else if (is_node_sel) { + if (the_graph.has_series(sel)) { + sel = tailsel_opt.value(); + auto series = the_graph.get_node_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_node_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_node_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else if (the_graph.has_series(sel)) { + auto series = the_graph.get_node_series(tail_sel); + if (series) { + clip.to_return>(series.value().count()); + } else { + clip.to_return>({}); + } + } else { + std::cerr << "UNKNOWN TYPE" << std::endl; + return 1; + } + } + clip.set_state(state_name, the_graph); + return 0; +} diff --git a/cpp/test/TestGraph/degree.cpp b/cpp/test/TestGraph/degree.cpp new file mode 100644 index 0000000..ec761bd --- /dev/null +++ b/cpp/test/TestGraph/degree.cpp @@ -0,0 +1,78 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include + +#include "clippy/selector.hpp" +#include "testgraph.hpp" + +static const std::string method_name = "degree"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{ + method_name, + "Populates a column containing the degree of each node in a graph"}; + clip.add_required( + "selector", + "Existing selector name into which the degree will be written"); + clip.add_required_state(state_name, + "Internal container"); + clip.add_required_state>( + sel_state_name, "Internal container for pending selectors"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + selector sel = clip.get("selector"); + + if (!sel.headeq("node")) { + std::cerr << "Selector must be a node subselector" << std::endl; + return 1; + } + auto the_graph = clip.get_state(state_name); + + auto selectors = + clip.get_state>(sel_state_name); + if (!selectors.contains(sel)) { + std::cerr << "Selector not found" << std::endl; + return 1; + } + auto subsel = sel.tail().value(); + if (the_graph.has_node_series(subsel)) { + std::cerr << "Selector already populated" << std::endl; + return 1; + } + + auto deg_o = the_graph.add_node_series(subsel, "Degree"); + if (!deg_o) { + std::cerr << "Unable to manifest node series" << std::endl; + return 1; + } + + auto deg = deg_o.value(); + + the_graph.for_all_edges([°](auto edge, mvmap::locator /*unused*/) { + deg[edge.first]++; + if (edge.first != edge.second) { + deg[edge.second]++; + } + }); + + clip.set_state(state_name, the_graph); + clip.set_state(sel_state_name, selectors); + clip.update_selectors(selectors); + + clip.return_self(); + return 0; +} diff --git a/cpp/test/TestGraph/drop_series.cpp b/cpp/test/TestGraph/drop_series.cpp new file mode 100644 index 0000000..3a6298d --- /dev/null +++ b/cpp/test/TestGraph/drop_series.cpp @@ -0,0 +1,66 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include + +#include "clippy/clippy.hpp" +#include "clippy/selector.hpp" +#include "testgraph.hpp" + +namespace boostjsn = boost::json; + +static const std::string method_name = "drop_series"; +static const std::string graph_state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; +static const std::string sel_name = "selector"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Drops a selector"}; + clip.add_required(sel_name, "Selector to drop"); + + clip.add_required_state>( + sel_state_name, "Internal container for pending selectors"); + clip.add_required_state(graph_state_name, + "Internal state for the graph"); + + if (clip.parse(argc, argv)) { + return 0; + } + + auto jo = clip.get(sel_name); + + selector sel = clip.get(sel_name); + + auto sel_state = + clip.get_state>(sel_state_name); + if (!sel_state.contains(sel)) { + std::cerr << "Selector name not found!" << std::endl; + exit(-1); + } + auto the_graph = clip.get_state(graph_state_name); + auto subsel = sel.tail().value(); + if (sel.headeq("edge")) { + if (the_graph.has_edge_series(subsel)) { + the_graph.drop_edge_series(sel); + } + } else if (sel.headeq("node")) { + if (the_graph.has_node_series(subsel)) { + the_graph.drop_node_series(subsel); + } + } else { + std::cerr << "Selector name must start with either \"edge.\" or \"node.\"" + << std::endl; + exit(-1); + } + sel_state.erase(sel); + clip.set_state(graph_state_name, the_graph); + clip.set_state(sel_state_name, sel_state); + clip.update_selectors(sel_state); + + return 0; +} diff --git a/cpp/test/TestGraph/dump.cpp b/cpp/test/TestGraph/dump.cpp new file mode 100644 index 0000000..2b4b46d --- /dev/null +++ b/cpp/test/TestGraph/dump.cpp @@ -0,0 +1,129 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include +#include + +#include "clippy/selector.hpp" +#include "testgraph.hpp" + +static const std::string method_name = "dump"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +static const std::string always_true = R"({"rule":{"==":[1,1]}})"; +static const std::string never_true = R"({"rule":{"==":[2,1]}})"; + +static const boost::json::object always_true_obj = + boost::json::parse(always_true).as_object(); + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, + "returns a map of key: value " + "corresponding to the selector."}; + clip.add_required("selector", + "Existing selector name to obtain values"); + + clip.add_optional("where", "where filter", + always_true_obj); + + clip.add_required_state(state_name, + "Internal container"); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto sel_str = clip.get("selector"); + selector sel{sel_str}; + + auto the_graph = clip.get_state(state_name); + + bool is_edge_sel = testgraph::testgraph::is_edge_selector(sel); + bool is_node_sel = testgraph::testgraph::is_node_selector(sel); + + if (!is_edge_sel && !is_node_sel) { + std::cerr << "Selector must start with either \"edge\" or \"node\"" + << std::endl; + return 1; + } + + auto tailsel_opt = sel.tail(); + if (!tailsel_opt) { + std::cerr << "no tail" << std::endl; + return 1; + } + + auto expression = clip.get("where"); + // auto expression = boost::json::parse(always_true).as_object(); + std::cerr << "expression: " << expression << std::endl; + + // we need to make a copy here because translateNode modifies the object + boost::json::object exp2(expression); + auto [_a /*unused*/, vars, _b /*unused*/] = + jsonlogic::translateNode(exp2["rule"]); + std::cerr << "post-translate expression: " << expression << std::endl; + std::cerr << "post-translate expression['rule']: " << expression["rule"] + << std::endl; + auto apply_jl = [&expression, &vars](int value) { + boost::json::object data; + boost::json::value val = value; + std::cerr << " apply_jl expression: " << expression << std::endl; + for (auto var : vars) { + data[var] = val; + std::cerr << " apply_jl: var: " << var << " val: " << val << std::endl; + } + + jsonlogic::any_expr res = jsonlogic::apply(expression["rule"], data); + std::cerr << " apply_jl: res: " << res << std::endl; + return jsonlogic::unpack_value(res); + }; + + std::string tail_sel = tailsel_opt.value(); + if (is_node_sel) { + if (the_graph.has_node_series(tail_sel)) { + auto pxy = the_graph.get_node_series(tail_sel).value(); + std::map filtered_data; + pxy.for_all( + [&filtered_data, &apply_jl](const auto &key, auto, const auto &val) { + std::cerr << "key: " << key << " val: " << val << std::endl; + if (apply_jl(val)) { // apply the where clause + filtered_data[key] = val; + std::cerr << "applied!" << std::endl; + } + }); + + clip.returns>( + "map of key: value corresponding to the selector"); + clip.to_return(filtered_data); + return 0; + } else if (the_graph.has_node_series(tail_sel)) { + // clip.returns>( + // "map of key: value corresponding to the selector"); + } else if (the_graph.has_node_series(tail_sel)) { + // clip.returns>( + // "map of key: value corresponding to the selector"); + } else if (the_graph.has_node_series(tail_sel)) { + // clip.returns>( + // "map of key: value corresponding to the selector"); + } else { + std::cerr << "Node series is an invalid type" << std::endl; + return 1; + } + } + + clip.returns>( + "map of key: value corresponding to the selector"); + + clip.to_return(std::map{}); + clip.set_state(state_name, the_graph); + return 0; +} diff --git a/cpp/test/TestGraph/dump2.cpp b/cpp/test/TestGraph/dump2.cpp new file mode 100644 index 0000000..6bde2b1 --- /dev/null +++ b/cpp/test/TestGraph/dump2.cpp @@ -0,0 +1,89 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +// #include +#include + +#include "clippy/selector.hpp" +#include "testgraph.hpp" +#include "where.cpp" + +static const std::string method_name = "dump2"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +static const std::string always_true = R"({"rule":{"==":[1,1]}})"; +static const std::string never_true = R"({"rule":{"==":[2,1]}})"; + +static const boost::json::object always_true_obj = + boost::json::parse(always_true).as_object(); + +int main(int argc, char **argv) { + std::cerr << "starting" << std::endl; + clippy::clippy clip{method_name, + "returns a map of key: value " + "corresponding to the selector."}; + clip.add_required("selector", + "Existing selector name to obtain values"); + + clip.add_optional("where", "where filter", + always_true_obj); + + clip.add_required_state(state_name, + "Internal container"); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + std::cerr << "past parse" << std::endl; + + auto sel_str = clip.get("selector"); + selector sel{sel_str}; + + auto the_graph = clip.get_state(state_name); + + bool is_edge_sel = testgraph::testgraph::is_edge_selector(sel); + bool is_node_sel = testgraph::testgraph::is_node_selector(sel); + + if (!is_edge_sel && !is_node_sel) { + std::cerr << "Selector must start with either \"edge\" or \"node\"" + << std::endl; + return 1; + } + + std::cerr << "before tailsel_opt" << std::endl; + auto tailsel_opt = sel.tail(); + if (!tailsel_opt) { + std::cerr << "no tail" << std::endl; + return 1; + } + + auto expression = clip.get("where"); + // auto expression = boost::json::parse(always_true).as_object(); + std::cerr << "expression: " << expression << std::endl; + + std::string tail_sel = tailsel_opt.value(); + if (is_node_sel) { + clip.returns>( + "vector of node keys that match the selector"); + auto filtered_data = where_nodes(the_graph, expression); + clip.to_return(filtered_data); + return 0; + } + + clip.returns>( + "map of key: value corresponding to the selector"); + + clip.to_return(std::map{}); + clip.set_state(state_name, the_graph); + return 0; +} diff --git a/cpp/test/TestGraph/extrema.cpp b/cpp/test/TestGraph/extrema.cpp new file mode 100644 index 0000000..fd537b2 --- /dev/null +++ b/cpp/test/TestGraph/extrema.cpp @@ -0,0 +1,155 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include + +#include "clippy/selector.hpp" +#include "testgraph.hpp" + +static const std::string method_name = "extrema"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, + "returns the extrema of a series based on selector"}; + clip.add_required("selector", + "Existing selector name to calculate extrema"); + clip.add_required_state(state_name, + "Internal container"); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + auto sel_str = clip.get("selector"); + selector sel{sel_str}; + + bool is_edge_sel = testgraph::testgraph::is_edge_selector(sel); + bool is_node_sel = testgraph::testgraph::is_node_selector(sel); + + if (!is_edge_sel && !is_node_sel) { + std::cerr << "Selector must start with either \"edge\" or \"node\"" + << std::endl; + return 1; + } + + auto tail_opt = sel.tail(); + if (!tail_opt) { + std::cerr << "Selector must have a tail" << std::endl; + return 1; + } + auto tail_sel = tail_opt.value(); + + auto the_graph = clip.get_state(state_name); + + if (is_edge_sel) { + clip.returns>>( + "min and max keys and values of the series"); + if (the_graph.has_edge_series(tail_sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (!series) { + std::cerr << "Edge series not found" << std::endl; + return 1; + } + auto series_val = series.value(); + auto [min_tup, max_tup] = series_val.extrema(); + + std::map> extrema; + if (min_tup) { + extrema["min"] = std::make_pair(std::get<1>(min_tup.value()), + std::get<0>(min_tup.value())); + } + + if (max_tup) { + extrema["max"] = std::make_pair(std::get<1>(max_tup.value()), + std::get<0>(max_tup.value())); + } + clip.to_return(extrema); + } else if (the_graph.has_edge_series(tail_sel)) { + auto series = the_graph.get_edge_series(tail_sel); + if (!series) { + std::cerr << "Edge series not found" << std::endl; + return 1; + } + auto series_val = series.value(); + auto [min_tup, max_tup] = series_val.extrema(); + + std::map> extrema; + if (min_tup) { + extrema["min"] = std::make_pair(std::get<1>(min_tup.value()), + std::get<0>(min_tup.value())); + } + + if (max_tup) { + extrema["max"] = std::make_pair(std::get<1>(max_tup.value()), + std::get<0>(max_tup.value())); + } + clip.to_return(extrema); + } else { + std::cerr << "Edge series is an invalid type" << std::endl; + return 1; + } + } else if (is_node_sel) { + if (the_graph.has_edge_series(tail_sel)) { + clip.returns>>( + "min and max keys and values of the series"); + + auto series = the_graph.get_node_series(tail_sel); + if (!series) { + std::cerr << "Edge series not found" << std::endl; + return 1; + } + + auto series_val = series.value(); + auto [min_tup, max_tup] = series_val.extrema(); + + std::map> extrema; + if (min_tup) { + extrema["min"] = std::make_pair(std::get<1>(min_tup.value()), + std::get<0>(min_tup.value())); + } + if (max_tup) { + extrema["max"] = std::make_pair(std::get<1>(max_tup.value()), + std::get<0>(max_tup.value())); + } + + clip.to_return(extrema); + } else if (the_graph.has_node_series(tail_sel)) { + clip.returns< + std::map>>( + "min and max keys and values of the series"); + + auto series = the_graph.get_node_series(tail_sel); + if (!series) { + return 1; + } + auto series_val = series.value(); + auto [min_tup, max_tup] = series_val.extrema(); + + std::map> extrema; + if (min_tup) { + extrema["min"] = std::make_pair(std::get<1>(min_tup.value()), + std::get<0>(min_tup.value())); + } + if (max_tup) { + extrema["max"] = std::make_pair(std::get<1>(max_tup.value()), + std::get<0>(max_tup.value())); + } + + clip.to_return(extrema); + } else { + std::cerr << "Node series is an invalid type" << std::endl; + return 1; + } + } + + clip.set_state(state_name, the_graph); + return 0; +} diff --git a/cpp/test/TestGraph/for_all_edges.cpp b/cpp/test/TestGraph/for_all_edges.cpp new file mode 100644 index 0000000..bd4e90d --- /dev/null +++ b/cpp/test/TestGraph/for_all_edges.cpp @@ -0,0 +1,52 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include +#include + +#include "testgraph.hpp" + +static const std::string method_name = "add_node"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, + "Adds a new column to a graph based on a lambda"}; + clip.add_required("name", "New column name"); + clip.add_required("expression", "Lambda Expression"); + clip.add_required_state(state_name, + "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto name = clip.get("name"); + auto expression = clip.get("expression"); + auto the_graph = clip.get_state(state_name); + + // + // Expression here + auto apply_jl = [&expression](const testgraph::edge_t &value, + mvmap::locator loc) { + boost::json::object data; + data["src"] = value.first; + data["dst"] = value.second; + data["loc"] = boost::json::value_from(loc); + jsonlogic::any_expr res = jsonlogic::apply(expression["rule"], data); + return jsonlogic::unpack_value(res); + }; + + the_graph.for_all_edges(apply_jl); + + clip.set_state(state_name, the_graph); + clip.return_self(); + return 0; +} diff --git a/cpp/test/TestGraph/meta.json b/cpp/test/TestGraph/meta.json new file mode 100644 index 0000000..0af2980 --- /dev/null +++ b/cpp/test/TestGraph/meta.json @@ -0,0 +1,7 @@ +{ + "__doc__" : "A graph data structure", + "initial_selectors" : { + "edge" : "The edges of the graph", + "node": "The nodes of the graph" + } +} diff --git a/cpp/test/TestGraph/mvmap.hpp b/cpp/test/TestGraph/mvmap.hpp new file mode 100644 index 0000000..2643888 --- /dev/null +++ b/cpp/test/TestGraph/mvmap.hpp @@ -0,0 +1,609 @@ +#pragma once +#include +// #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +template +std::ostream &operator<<(std::ostream &os, const std::pair &p) { + os << "(" << p.first << ", " << p.second << ")"; + return os; +} + +namespace mvmap { +using index = uint64_t; + +class locator { + static const index INVALID_LOC = std::numeric_limits::max(); + index loc; + locator(index loc) : loc(loc) {}; + + public: + template + friend class mvmap; + friend std::ostream &operator<<(std::ostream &os, const locator &l) { + if (l.is_valid()) { + os << "locator: " << l.loc; + } else { + os << "locator: invalid"; + } + return os; + } + friend void tag_invoke(boost::json::value_from_tag /*unused*/, + boost::json::value &v, locator l); + friend locator tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value &v); + locator() : loc(INVALID_LOC) {}; + [[nodiscard]] bool is_valid() const { return loc != INVALID_LOC; } + + void print() const { + if (is_valid()) { + std::cout << "locator: " << loc << std::endl; + } else { + std::cout << "locator: invalid" << std::endl; + } + } +}; +void tag_invoke(boost::json::value_from_tag /*unused*/, boost::json::value &v, + locator l) { + v = l.loc; +} + +locator tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value &v) { + return boost::json::value_to(v); +} +template +class mvmap { + template + using series = std::map; + using key_to_idx = std::map; + using idx_to_key = std::map; + using variants = std::variant; + + template + static void print_series(const series &s) { + for (auto el : s) { + std::cout << el.first << " -> " << el.second << std::endl; + } + } + + // A locator is an opaque handle to a key in a series. + + idx_to_key itk; + key_to_idx kti; + std::map...>> data; + std::map series_desc; + + public: + // A series_proxy is a reference to a series in an mvmap. + template + class series_proxy { + std::string m_id; + std::string m_desc; + key_to_idx &kti_r; + idx_to_key &itk_r; + series &series_r; + + using series_type = V; + + // returns true if there is an index assigned to a given key + bool has_idx_at_key(K k) const { return kti_r.contains(k); } + // returns true if there is a key assigned to a given locator + bool has_key_at_index(locator l) const { return itk_r.contains(l.loc); } + + // returns or creates the index for a key. + index get_idx(K k) { + if (!has_idx_at_key(k)) { + index i{kti_r.size()}; + kti_r[k] = i; + itk_r[i] = k; + return i; + } + return kti_r[k]; + } + + public: + series_proxy(std::string id, series &ser, mvmap &m) + : m_id(std::move(id)), kti_r(m.kti), itk_r(m.itk), series_r(ser) {} + + series_proxy(std::string id, const std::string &desc, series &ser, + mvmap &m) + : m_id(std::move(id)), + m_desc(desc), + kti_r(m.kti), + itk_r(m.itk), + series_r(ser) {} + + bool is_string_v() const { return std::is_same_v; } + bool is_double_v() const { return std::is_same_v; } + bool is_int64_t_v() const { return std::is_same_v; } + bool is_bool_v() const { return std::is_same_v; } + + std::string id() const { return m_id; } + std::string desc() const { return m_desc; } + V &operator[](K k) { return series_r[get_idx(k)]; } + const V &operator[](K k) const { return series_r[get_idx(k)]; } + + // this assumes the key exists. + V &operator[](locator l) { return series_r[l.loc]; } + const V &operator[](locator l) const { return series_r[l.loc]; } + + std::optional> at(locator l) { + if (!has_key_at_index(l) || !series_r.contains(l.loc)) { + return std::nullopt; + } + return series_r[l.loc]; + }; + std::optional> at(locator l) const { + if (!has_key_at_index(l) || !series_r.contains(l.loc)) { + return std::nullopt; + } + return series_r[l.loc]; + }; + + std::optional> at(K k) { + if (!has_idx_at_key(k) || !series_r.contains(get_idx(k))) { + return std::nullopt; + } + return series_r[get_idx(k)]; + }; + + std::optional> at(K k) const { + if (!has_idx_at_key(k) || !series_r.contains(get_idx(k))) { + return std::nullopt; + } + return series_r[get_idx(k)]; + }; + + // this will create the key/index if it doesn't exist. + locator get_loc(K k) { return locator(get_idx(k)); } + + // F takes (K key, locator, V value) + template + void for_all(F f) { + for (auto el : series_r) { + f(itk_r[el.first], locator(el.first), el.second); + } + }; + + template + void for_all(F f) const { + for (auto el : series_r) { + f(itk_r[el.first], locator(el.first), el.second); + } + }; + + // F takes (K key, locator, V value) + template + void remove_if(F f) { + auto indices_to_delete = std::vector{}; + for (auto el : series_r) { + if (f(itk_r[el.first], locator(el.first), el.second)) { + indices_to_delete.emplace_back(el.first); + } + } + + for (auto ltd : indices_to_delete) { + erase(locator(ltd)); + } + }; + + void erase(const locator &l) { + auto i = l.loc; + kti_r.erase(itk_r[i]); + itk_r.erase(i); + series_r.erase(i); + } + + // if the key doesn't exist, do nothing. + void erase(const K &k) { + if (!has_idx_at_key(k)) { + return; + } + auto i = kti_r[k]; + erase(locator(i)); + } + + // this returns the key for a given locator in a series, or nullopt if the + // locator is invalid. + std::optional> get_key( + const locator &l) const { + if (!has_key_at_index(l.loc)) { + return std::nullopt; + } + return itk_r[l.loc]; + } + + std::pair>, + std::optional>> + extrema() { + V min = std::numeric_limits::max(); + V max = std::numeric_limits::min(); + bool found_min = false; + bool found_max = false; + locator min_loc; + locator max_loc; + K min_key; + K max_key; + for_all([&min, &max, &found_min, &found_max, &min_loc, &max_loc, &min_key, + &max_key](auto k, auto l, auto v) { + if (v < min) { + min = v; + min_loc = l; + min_key = k; + found_min = true; + } + if (v > max) { + max = v; + max_loc = l; + max_key = k; + found_max = true; + } + }); + std::optional> min_opt, max_opt; + if (found_min) { + min_opt = std::make_tuple(min, min_key, min_loc); + } else { + min_opt = std::nullopt; + } + if (found_max) { + max_opt = std::make_tuple(max, max_key, max_loc); + } else { + max_opt = std::nullopt; + } + return std::make_pair(min_opt, max_opt); + } + + std::map count() { + std::map ct; + for_all([&ct](auto /*unused*/, auto /*unused*/, auto v) { ct[v]++; }); + return ct; + } + + void print() { + std::cout << "id: " << m_id << ", "; + std::cout << "desc: " << m_desc << ", "; + std::string dtype = "unknown"; + if (is_string_v()) { + dtype = "string"; + } else if (is_double_v()) { + dtype = "double"; + } else if (is_int64_t_v()) { + dtype = "int64_t"; + } else if (is_bool_v()) { + dtype = "bool"; + } + std::cout << "dtype: " << dtype << ", "; + // std::cout << "kti_r.size(): " << kti_r.size() << std::endl; + // std::cout << "itk_r.size(): " << itk_r.size() << std::endl; + std::cout << series_r.size() << " entries" << std::endl; + // std::cout << "elements: " << std::endl; + for (auto el : series_r) { + std::cout << " " << itk_r[el.first] << " -> " << el.second + << std::endl; + } + } + + }; // end of series + ///////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////// + mvmap(const idx_to_key &itk, const key_to_idx &kti, + const std::map...>> &data) + : itk(itk), kti(kti), data(data) {} + + mvmap() = default; + friend void tag_invoke(boost::json::value_from_tag /*unused*/, + boost::json::value &v, const mvmap &m) { + v = {{"itk", boost::json::value_from(m.itk)}, + {"kti", boost::json::value_from(m.kti)}, + {"data", boost::json::value_from(m.data)}}; + } + + friend mvmap tag_invoke( + boost::json::value_to_tag> /*unused*/, + const boost::json::value &v) { + const auto &obj = v.as_object(); + using index = uint64_t; + // template using series = std::map; + using key_to_idx = std::map; + using idx_to_key = std::map; + return {boost::json::value_to(obj.at("itk")), + boost::json::value_to(obj.at("kti")), + boost::json::value_to< + std::map...>>>( + obj.at("data"))}; + } + + [[nodiscard]] size_t size() const { return kti.size(); } + bool add_key(const K &k) { + if (kti.count(k) > 0) { + return false; + } + auto i = kti.size(); + kti[k] = i; + itk[i] = k; + return true; + } + + [[nodiscard]] std::vector> list_series() { + std::vector> ser_pairs; + for (auto el : series_desc) { + ser_pairs.push_back(el); + } + return ser_pairs; + } + + [[nodiscard]] bool has_series(const std::string &id) const { + return data.contains(id); + } + + template + [[nodiscard]] bool has_series(const std::string &id) const { + // std::cerr << "has_series: " << id << std::endl; + // std::cerr << "V = " << typeid(V).name() << std::endl; + // std::cerr << "data.contains(id): " << data.contains(id) << std::endl; + // if (data.contains(id)) { + // std::cerr << "std::holds_alternative>(data.at(id)): " + // << std::holds_alternative>(data.at(id)) << + // std::endl; + // } + return data.contains(id) && std::holds_alternative>(data.at(id)); + } + bool contains(const K &k) { return kti.contains(k); } + auto keys() const { return std::views::keys(kti); } + + void add_row(const K &key, + const std::map> &row) { + auto loc = locator(kti.size()); + for (const auto &el : row) { + if (!has_series(el.first)) { + continue; + } + std::visit( + [&el, &key, this](auto &ser) { + std::cout << "adding to series " << el.first << std::endl; + using variant_type = std::decay_tsecond)>; + auto sproxy = get_series>(el.first) + .value(); // this is a series_proxy + // TODO: make sure this actually sets itk and kti correctly. + sproxy[key] = std::get(el.second); + }, + data[el.first]); + } + } + + void rem_row(const K &key) { + auto index = kti[key]; + for (auto &el : data) { + std::visit([&key, &index, this](auto &ser) { ser.erase(index); }, + el.second); + } + + kti.erase(key); + itk.erase(index); + } + // adds a new column (series) to the mvmap and returns true. If already + // exists, return false + template + std::optional> add_series(const std::string &sel, + const std::string &desc = "") { + if (has_series(sel)) { + return std::nullopt; + } + data[sel] = series{}; + series_desc[sel] = desc; + return series_proxy(sel, desc, std::get>(data[sel]), *this); + } + + // copies an existing column (series) to a new (unmanifested) column and + // returns true. If the new column already exists, or if the existing column + // doesn't, return false. + bool copy_series(const std::string &from, const std::string &to, + const std::optional &desc = std::nullopt) { + if (has_series(to) || !has_series(from)) { + std::cerr << "copy_series failed from " << from << " to " << to + << std::endl; + return false; + } + // std::cerr << "copying series from " << from << " to " << to << std::endl; + data[to] = data[from]; + series_desc[to] = desc.has_value() ? desc.value() : series_desc[from]; + return true; + } + + template + std::optional> get_series(const std::string &sel) { + if (!has_series(sel)) { + // series doesn't exist or is of the wrong type. + return std::nullopt; + } + // return series_proxy(sel, this->series_desc.at(sel), + // std::get>(data[sel]), *this); + auto foo = series_desc[sel]; + return series_proxy(sel, foo, std::get>(data[sel]), *this); + } + + bool series_is_string(const std::string &sel) const { + return has_series(sel); + } + + bool series_is_double(const std::string &sel) const { + return has_series(sel); + } + + bool series_is_int64_t(const std::string &sel) const { + return has_series(sel); + } + + bool series_is_bool(const std::string &sel) const { + return has_series(sel); + } + + // std::optional> get_variant_series( + // const std::string &sel) { + // if (!has_series(sel)) { + // return std::nullopt; + // } + + // using vtype = decltype(data[sel]); + // return series_proxy(sel, this->series_desc.at(sel), + // data.at(sel), + // this); + // } + + void drop_series(const std::string &sel) { + if (!has_series(sel)) { + return; + } + data.erase(sel); + series_desc.erase(sel); + } + + std::optional get_as_variant(const std::string &sel, + const locator &loc) { + auto col = data[sel]; + std::optional val; + std::visit( + [&val, sel, loc, this](auto &ser) { + using T = std::decay_tsecond)>; + auto sproxy = get_series>(sel) + .value(); // this is a series_proxy + val = sproxy.at(loc); + }, + col); + return val; + // return data[sel][loc.loc]; + } + + std::optional get_as_variant(const std::string &sel, + const K &key) { + auto col = data[sel]; + std::optional val; + std::visit( + [&val, sel, key, this](auto &ser) { + using T = std::decay_tsecond)>; + auto sproxy = get_series>(sel) + .value(); // this is a series_proxy + val = sproxy.at(key); + }, + col); + return val; + } + + // F is a function that takes a key and a locator. + // Users will need to close over series_proxies that they want to use. + template + void for_all(F f) { + for (auto &idx : kti) { + f(idx.first, locator(idx.second)); + } + } + + template + void remove_if(F f) { + std::vector indices_to_delete; + for (auto &idx : kti) { + if (f(idx.first, locator(idx.second))) { + indices_to_delete.emplace_back(idx.second); + } + } + + for (auto &idx : indices_to_delete) { + kti.erase(itk[idx]); + itk.erase(idx); + for (auto &id_ser : data) { + std::visit([&idx](auto &ser) { ser.erase(idx); }, id_ser.second); + } + } + } + + void print() { + std::cout << "mvmap with " << data.size() << " series: " << std::endl; + for (auto &el : data) { + std::cout << "series " << el.first << ":" << std::endl; + std::visit( + [&el, this](auto &ser) { + using T = std::decay_tsecond)>; + auto sproxy = get_series>(el.first) + .value(); // this is a series_proxy + sproxy.print(); + }, + el.second); + + // std::cout << " second: " << el.second << std::endl; + // auto foo = get_variant_series(el.first).value(); + // foo.visit([](auto &ser) { print_series(ser); }); + // print_series(el.second); + } + } + + std::string str_cols(const std::vector &cols) { + std::stringstream sstr; + for_all([this, &cols, &sstr](auto key, auto loc) { + sstr << key << "@" << loc.loc; + for (auto &col : cols) { + auto v = get_as_variant(col, loc); + if (v.has_value()) { + std::visit( + [&col, &sstr](auto &&arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + sstr << " (str) " << col << ": " << arg; + } else if constexpr (std::is_same_v) { + sstr << " (dbl) " << col << ": " << arg; + } else if constexpr (std::is_same_v) { + sstr << " (int) " << col << ": " << arg; + } else if constexpr (std::is_same_v) { + sstr << " (bool) " << col << ": " << arg; + } + }, + v.value()); + } + } + sstr << std::endl; + }); + return sstr.str(); + } + + std::map get_series_vals_at( + const K &key, const std::vector &cols) { + std::vector...>> proxies; + + for (auto &col : cols) { + if (has_series(col)) { + std::visit( + [this, &col, &proxies](auto &coldata) { + using T = std::decay_t::mapped_type; + auto sproxy = + get_series(col).value(); // this is a series_proxy + proxies.push_back(sproxy); + }, + data[col]); + } + } + + std::map row; + for (auto &sproxy : proxies) { + std::visit( + [&row, &key](auto &&arg) { + auto v = arg.at(key); + if (v.has_value()) { + row[arg.id()] = arg[key]; + } + }, + sproxy); + } + return row; + } +}; +}; // namespace mvmap diff --git a/cpp/test/TestGraph/ne.cpp b/cpp/test/TestGraph/ne.cpp new file mode 100644 index 0000000..cb2ded3 --- /dev/null +++ b/cpp/test/TestGraph/ne.cpp @@ -0,0 +1,29 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "ne"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Returns the number of edges in the graph"}; + + clip.returns("Number of edges."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_graph = clip.get_state(state_name); + + clip.to_return(the_graph.ne()); + return 0; +} diff --git a/cpp/test/TestGraph/nv.cpp b/cpp/test/TestGraph/nv.cpp new file mode 100644 index 0000000..fafd0d5 --- /dev/null +++ b/cpp/test/TestGraph/nv.cpp @@ -0,0 +1,29 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "nv"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Returns the number of nodes in the graph"}; + + clip.returns("Number of nodes."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_graph = clip.get_state(state_name); + + clip.to_return(the_graph.nv()); + return 0; +} diff --git a/cpp/test/TestGraph/remove.cpp b/cpp/test/TestGraph/remove.cpp new file mode 100644 index 0000000..f24422b --- /dev/null +++ b/cpp/test/TestGraph/remove.cpp @@ -0,0 +1,36 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include "testgraph.hpp" +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "remove_edge"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + + clippy::clippy clip{method_name, "Removes a string from a TestSet"}; + + clip.add_required("item", "Item to remove"); + clip.add_required_state>(state_name, "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto item = clip.get("item"); + auto the_set = clip.get_state>(state_name); + the_set.erase(item); + clip.set_state(state_name, the_set); + clip.return_self(); + return 0; +} diff --git a/cpp/test/TestGraph/series_str.cpp b/cpp/test/TestGraph/series_str.cpp new file mode 100644 index 0000000..c7120f7 --- /dev/null +++ b/cpp/test/TestGraph/series_str.cpp @@ -0,0 +1,65 @@ + +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include + +#include "clippy/selector.hpp" +#include "testgraph.hpp" + +static const std::string method_name = "series_str"; +static const std::string state_name = "INTERNAL"; +static const std::string sel_state_name = "selectors"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, + "returns the values of a series based on selector"}; + clip.add_required("selector", "Existing selector name"); + clip.add_required_state(state_name, + "Internal container"); + + clip.returns("String of data."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + auto sel_str = clip.get("selector"); + selector sel{sel_str}; + + bool is_edge_sel = testgraph::testgraph::is_edge_selector(sel); + bool is_node_sel = testgraph::testgraph::is_node_selector(sel); + + if (!is_edge_sel && !is_node_sel) { + std::cerr << "Selector must start with either \"edge\" or \"node\"" + << std::endl; + return 1; + } + + auto tail_opt = sel.tail(); + if (!tail_opt) { + std::cerr << "Selector must have a tail" << std::endl; + return 1; + } + auto tail_sel = tail_opt.value(); + + auto the_graph = clip.get_state(state_name); + + if (is_edge_sel) { + clip.to_return(the_graph.str_edge_col(tail_sel)); + } else if (is_node_sel) { + clip.to_return(the_graph.str_node_col(tail_sel)); + } else { + std::cerr << "Selector must start with either \"edge\" or \"node\"" + << std::endl; + return 1; + } + + // clip.set_state(state_name, the_graph); + return 0; +} diff --git a/cpp/test/TestGraph/testconst.cpp b/cpp/test/TestGraph/testconst.cpp new file mode 100644 index 0000000..badd1f1 --- /dev/null +++ b/cpp/test/TestGraph/testconst.cpp @@ -0,0 +1,8 @@ +class Foo { + int x; + +public: + int get_x() const { return x; } + + int get_x() { return x + 1; } +}; diff --git a/cpp/test/TestGraph/testgraph.cpp b/cpp/test/TestGraph/testgraph.cpp new file mode 100644 index 0000000..03f4641 --- /dev/null +++ b/cpp/test/TestGraph/testgraph.cpp @@ -0,0 +1,48 @@ +#include "testgraph.hpp" + +#include + +int main() { + auto g = testgraph::testgraph{}; + + g.add_node("a"); + g.add_node("b"); + g.add_edge("a", "b"); + g.add_edge("b", "c"); + + assert(g.has_node("a")); + assert(g.has_node("b")); + assert(g.has_node("c")); + assert(!g.has_node("d")); + + assert(g.has_edge({"a", "b"})); + assert(g.has_edge({"b", "c"})); + assert(!g.has_edge({"a", "c"})); + + assert(g.out_degree("a") == 1); + assert(g.in_degree("a") == 0); + assert(g.in_degree("b") == 1); + assert(g.out_degree("b") == 1); + + auto colref = + g.add_node_series("color", "The color of the nodes"); + colref.value()["a"] = "blue"; + colref.value()["c"] = "red"; + + auto weightref = g.add_edge_series("weight", "edge weights"); + + weightref.value()[std::pair("a", "b")] = 5.5; + weightref.value()[std::pair("b", "c")] = 3.3; + + std::cout << "g.nv: " << g.nv() << ", g.ne: " << g.ne() << "\n"; + std::cout << boost::json::value_from(g) << "\n"; + + auto val = boost::json::value_from(g); + auto str = boost::json::serialize(val); + std::cout << "here it is: " << str << "\n"; + + auto g2 = boost::json::value_to(val); + auto e2 = weightref.value().extrema(); + std::cout << "extrema: " << std::get<0>(e2.first.value()) << ", " + << std::get<0>(e2.second.value()) << "\n"; +} diff --git a/cpp/test/TestGraph/testgraph.hpp b/cpp/test/TestGraph/testgraph.hpp new file mode 100644 index 0000000..644f23d --- /dev/null +++ b/cpp/test/TestGraph/testgraph.hpp @@ -0,0 +1,232 @@ +#pragma once +#include "mvmap.hpp" +#include +#include +#include +#include +#include +#include + +namespace testgraph { +// map of (src, dst) : weight +using node_t = std::string; +using edge_t = std::pair; + +template using sparsevec = std::map; + +enum series_type { ser_bool, ser_int64, ser_double, ser_string, ser_invalid }; + +using variants = std::variant; +class testgraph { + using edge_mvmap = mvmap::mvmap; + using node_mvmap = mvmap::mvmap; + template using edge_series_proxy = edge_mvmap::series_proxy; + template using node_series_proxy = node_mvmap::series_proxy; + node_mvmap node_table; + edge_mvmap edge_table; + +public: + const mvmap::mvmap & + nodemap() const { + return node_table; + } + + const mvmap::mvmap & + edgemap() const { + return edge_table; + } + static inline bool is_edge_selector(const std::string &sel) { + return sel.starts_with("edge."); + } + + static inline bool is_node_selector(const std::string &sel) { + return sel.starts_with("node."); + } + + static inline bool is_valid_selector(const std::string &sel) { + return is_edge_selector(sel) || is_node_selector(sel); + } + + friend void tag_invoke(boost::json::value_from_tag /*unused*/, + boost::json::value &v, testgraph const &g) { + v = {{"node_table", boost::json::value_from(g.node_table)}, + {"edge_table", boost::json::value_from(g.edge_table)}}; + } + + friend testgraph tag_invoke(boost::json::value_to_tag /*unused*/, + const boost::json::value &v) { + const auto &obj = v.as_object(); + auto nt = boost::json::value_to(obj.at("node_table")); + auto et = boost::json::value_to(obj.at("edge_table")); + return {nt, et}; + } + testgraph() = default; + testgraph(node_mvmap nt, edge_mvmap et) + : node_table(std::move(nt)), edge_table(std::move(et)) {}; + + // this function requires that the "edge." prefix be removed from the name. + template + std::optional> + add_edge_series(const std::string &sel, const std::string &desc = "") { + return edge_table.add_series(sel, desc); + } + + template + std::optional> + add_edge_series(const std::string &sel, const edge_series_proxy &from, + const std::string &desc = "") { + return edge_table.add_series(sel, from, desc); + } + + void drop_edge_series(const std::string &sel) { edge_table.drop_series(sel); } + + // this function requires that the "node." prefix be removed from the name. + void drop_node_series(const std::string &sel) { node_table.drop_series(sel); } + + // this function requires that the "node." prefix be removed from the name. + template + std::optional> + add_node_series(const std::string &sel, const std::string &desc = "") { + return node_table.add_series(sel, desc); + } + + template + std::optional> + add_node_series(const std::string &sel, const node_series_proxy &from, + const std::string &desc = "") { + return node_table.add_series(sel, from, desc); + } + template + std::optional> get_edge_series(const std::string &sel) { + return edge_table.get_series(sel); + } + + bool copy_edge_series(const std::string &from, const std::string &to, + const std::optional &desc = std::nullopt) { + return edge_table.copy_series(from, to, desc); + } + + bool copy_node_series(const std::string &from, const std::string &to, + const std::optional &desc = std::nullopt) { + return node_table.copy_series(from, to, desc); + } + + template + std::optional> get_node_series(const std::string &sel) { + return node_table.get_series(sel); + } + + [[nodiscard]] size_t nv() const { return node_table.size(); } + [[nodiscard]] size_t ne() const { return edge_table.size(); } + + template void for_all_edges(F f) { edge_table.for_all(f); } + template void for_all_nodes(F f) { node_table.for_all(f); } + + [[nodiscard]] std::vector edges() const { + auto kv = edge_table.keys(); + return {kv.begin(), kv.end()}; + } + [[nodiscard]] std::vector nodes() const { + auto kv = node_table.keys(); + return {kv.begin(), kv.end()}; + } + + bool add_node(const node_t &node) { return node_table.add_key(node); }; + bool add_edge(const node_t &src, const node_t &dst) { + node_table.add_key(src); + node_table.add_key(dst); + return edge_table.add_key({src, dst}); + } + + bool has_node(const node_t &node) { return node_table.contains(node); }; + bool has_edge(const edge_t &edge) { return edge_table.contains(edge); }; + bool has_edge(const node_t &src, const node_t &dst) { + return edge_table.contains({src, dst}); + }; + + // strips the head off the std::string and passes the tail to the appropriate + // method. + [[nodiscard]] bool has_series(const std::string &sel) const { + auto tail = sel.substr(5); + + if (is_node_selector(sel)) { + return has_node_series(tail); + } + if (is_edge_selector(sel)) { + return has_edge_series(tail); + } + return false; + } + + template + [[nodiscard]] bool has_series(const std::string &sel) const { + auto tail = sel.substr(5); + + if (is_node_selector(sel)) { + return has_node_series(tail); + } + if (is_edge_selector(sel)) { + return has_edge_series(tail); + } + return false; + } + + // assumes sel has already been tail'ed. + [[nodiscard]] bool has_node_series(const std::string &sel) const { + return node_table.has_series(sel); + } + + template + [[nodiscard]] bool has_node_series(const std::string &sel) const { + return node_table.has_series(sel); + } + + // assumes sel has already been tail'ed. + [[nodiscard]] bool has_edge_series(const std::string &sel) const { + return edge_table.has_series(sel); + } + template + [[nodiscard]] bool has_edge_series(const std::string &sel) const { + return edge_table.has_series(sel); + } + + [[nodiscard]] std::vector out_neighbors(const node_t &node) const { + std::vector neighbors; + for (const auto &[src, dst] : edge_table.keys()) { + if (src == node) { + neighbors.emplace_back(dst); + } + } + return neighbors; + } + + [[nodiscard]] std::vector in_neighbors(const node_t &node) const { + std::vector neighbors; + for (const auto &[src, dst] : edge_table.keys()) { + if (dst == node) { + neighbors.emplace_back(src); + } + } + return neighbors; + } + + [[nodiscard]] size_t in_degree(const node_t &node) const { + return in_neighbors(node).size(); + } + [[nodiscard]] size_t out_degree(const node_t &node) const { + return out_neighbors(node).size(); + } + + std::string str_edge_col(const std::string &col) { + std::vector cols{col}; + return edge_table.str_cols(cols); + } + + std::string str_node_col(const std::string &col) { + std::vector cols{col}; + return node_table.str_cols(cols); + } + +}; // class testgraph + +} // namespace testgraph diff --git a/cpp/test/TestGraph/testlocator.cpp b/cpp/test/TestGraph/testlocator.cpp new file mode 100644 index 0000000..a91b02c --- /dev/null +++ b/cpp/test/TestGraph/testlocator.cpp @@ -0,0 +1,7 @@ +class locator { + int loc; +} + +int main() { + auto l = locator{5}; +} diff --git a/cpp/test/TestGraph/testmvmap.cpp b/cpp/test/TestGraph/testmvmap.cpp new file mode 100644 index 0000000..c040fd1 --- /dev/null +++ b/cpp/test/TestGraph/testmvmap.cpp @@ -0,0 +1,71 @@ +#include "mvmap.hpp" +#include +// #include +#include +#include +#include + +using mymap_t = mvmap::mvmap; +int main() { + + mymap_t m{}; + + m.add_series("weight"); + std::cout << "added series weight\n"; + m.add_series("name"); + std::cout << "added series name\n"; + + auto hmap = m.get_or_create_series("age"); + std::cout << "created hmap\n"; + + hmap["seth"] = 5; + std::cout << "added seth\n"; + hmap["roger"] = 8; + std::cout << "added roger\n"; + + assert(hmap["seth"] == 5); + assert(hmap["roger"] == 8); + + auto v = boost::json::value_from(m); + std::string j = boost::json::serialize(v); + std::cout << "j = " << j << '\n'; + auto jv = boost::json::parse(j); + std::cout << boost::json::serialize(jv) << "\n"; + auto n = boost::json::value_to(jv); + + std::cout << "created n\n"; + auto hmap2 = n.get_or_create_series("age"); + std::cout << "created hmap2\n"; + assert(hmap2["seth"] == 5); + assert(hmap2["roger"] == 8); + + size_t age_sum = 0; + hmap2.for_all([&age_sum](const auto &k, auto, auto &v) { age_sum += v; }); + + std::cout << "sum of ages = " << age_sum << "\n"; + assert(age_sum == 13); + + hmap2.remove_if([](const auto &k, auto, auto &v) { return v > 6; }); + + assert(hmap2.at("roger") == std::nullopt); + assert(hmap2.at("seth") == 5); + + age_sum = 0; + hmap2.for_all([&age_sum](const auto &k, auto, auto &v) { age_sum += v; }); + std::cout << "sum of ages = " << age_sum << "\n"; + assert(age_sum == 5); + hmap2["roger"] = 8; + + age_sum = 0; + hmap2.for_all([&age_sum](const auto &k, auto, auto &v) { age_sum += v; }); + + std::cout << "sum of ages = " << age_sum << "\n"; + assert(age_sum == 13); + n.remove_if([&hmap2](const auto &k, auto) { return hmap2[k] == 5; }); + + age_sum = 0; + hmap2.for_all([&age_sum](const auto &k, auto, auto &v) { age_sum += v; }); + + std::cout << "sum of ages = " << age_sum << "\n"; + assert(age_sum == 8); +} diff --git a/cpp/test/TestGraph/testselector.cpp b/cpp/test/TestGraph/testselector.cpp new file mode 100644 index 0000000..9b87720 --- /dev/null +++ b/cpp/test/TestGraph/testselector.cpp @@ -0,0 +1,20 @@ +#include "../../include/clippy/selector.hpp" +#include + +int main() { + selector s = selector("foo.bar.baz"); + + selector zzz = "foo.zoo.boo"; + + selector s2{"x.y.z"}; + std::cout << "s = " << s << "\n"; + assert(s.headeq("foo")); + assert(!s.headeq("bar")); + + auto val = boost::json::value_from(s); + auto str = boost::json::serialize(val); + + auto t = boost::json::value_to(val); + assert(t.headeq("foo")); + std::cout << str << "\n"; +} diff --git a/cpp/test/TestGraph/where.cpp b/cpp/test/TestGraph/where.cpp new file mode 100644 index 0000000..86f3061 --- /dev/null +++ b/cpp/test/TestGraph/where.cpp @@ -0,0 +1,114 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "clippy/selector.hpp" +#include "jsonlogic/logic.hpp" +#include "testgraph.hpp" + +template +auto parse_where_expression(M& mvmap_, boost::json::object& expression, + boost::json::object& submission_data) { + // std::cerr << " parse_where_expression: expression: " << expression + // << std::endl; + boost::json::object exp2(expression); + + std::shared_ptr jlrule = + std::make_shared( + jsonlogic::create_logic(exp2["rule"])); + // std::cerr << "past create_logic\n"; + auto vars = jlrule->variable_names(); + // boost::json::object submission_data{}; + // jsonlogic::any_expr expression_rule_; + + // we use expression_rule_ (and expression_rule; see below) in order to avoid + // having to recompute this every time we call the lambda. + // std::tie(expression_rule_, vars, std::ignore) = + // jsonlogic::create_logic(exp2["rule"]); + + // this works around a deficiency in C++ compilers where + // unique pointers moved into a lambda cannot be moved into + // an std::function. + // jsonlogic::expr* rawexpr = expression_rule_.release(); + // std::shared_ptr expression_rule{rawexpr}; + // std::shared_ptr jlshared{&jlrule}; + + // std::cerr << "parse_where: # of vars: " << vars.size() << std::endl; + // for (const auto& var : vars) { + // std::cerr << " apply_jl var dump: var: " << var << std::endl; + // } + + auto apply_jl = [&expression, jlrule, &mvmap_, + &submission_data](mvmap::locator loc) mutable { + auto vars = jlrule->variable_names(); + // std::cerr << " apply_jl: # of vars: " << vars.size() << std::endl; + for (const auto& var : vars) { + // std::cerr << " apply_jl: var: " << var << std::endl; + auto var_sel = selector(std::string(var)); + // std::cerr << " apply_jl: var_sel = " << var_sel << std::endl; + // if (!var_sel.headeq("node")) { + // std::cerr << "selector is not a node selector; skipping." << + // std::endl; continue; + // } + auto var_tail = var_sel.tail().value(); + std::string var_str = std::string(var_sel); + // std::cerr << " apply_jl: var: " << var_sel << std::endl; + if (mvmap_.has_series(var_tail)) { + // std::cerr << " apply_jl: has series: " << var_sel << std::endl; + auto val = mvmap_.get_as_variant(var_tail, loc); + if (val.has_value()) { + // std::cerr << " apply_jl: val has value" << std::endl; + std::visit( + [&submission_data, &loc, &var_str](auto&& v) { + submission_data[var_str] = boost::json::value(v); + // std::cerr << " apply_jl: submission_data[" << var_str + // << "] = " << v << " at loc " << loc << "." + // << std::endl; + }, + *val); + } else { + std::cerr << " apply_jl: no value for " << var_sel << std::endl; + submission_data[var_str] = boost::json::value(); + } + } else { + std::cerr << " apply_jl: no series for " << var_sel << std::endl; + } + } + // std::cerr << " apply_jl: submission_data: " << submission_data + // << std::endl; + auto res = jlrule->apply(jsonlogic::json_accessor(submission_data)); + // jsonlogic::apply( + // *expression_rule, jsonlogic::data_accessor(submission_data)); + // std::cerr << " apply_jl: res: " << res << std::endl; + return jsonlogic::unpack_value(res); + }; + + return apply_jl; +} + +std::vector where_nodes(const testgraph::testgraph& g, + boost::json::object& expression) { + std::vector filtered_results; + // boost::json::object exp2(expression); + + // std::cerr << " where: expression: " << expression << std::endl; + + auto nodemap = g.nodemap(); + boost::json::object submission_data; + auto apply_jl = parse_where_expression(nodemap, expression, submission_data); + nodemap.for_all([&filtered_results, &apply_jl, &nodemap, + &expression](const auto &key, const auto &loc) { + // std::cerr << " where for_all key: " << key << std::endl; + if (apply_jl(loc)) { + // std::cerr << " where: applied!" << std::endl; + filtered_results.push_back(key); + } + }); + + return filtered_results; +} diff --git a/cpp/test/TestSelector/CMakeLists.txt b/cpp/test/TestSelector/CMakeLists.txt new file mode 100644 index 0000000..8a138a2 --- /dev/null +++ b/cpp/test/TestSelector/CMakeLists.txt @@ -0,0 +1,8 @@ +add_test(TestSelector __init__) +add_test(TestSelector add) +# add_test(TestSelector drop) +add_custom_command( + TARGET TestSelector_add POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/meta.json + ${CMAKE_CURRENT_BINARY_DIR}/meta.json) \ No newline at end of file diff --git a/cpp/test/TestSelector/__init__.cpp b/cpp/test/TestSelector/__init__.cpp new file mode 100644 index 0000000..2574832 --- /dev/null +++ b/cpp/test/TestSelector/__init__.cpp @@ -0,0 +1,29 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "__init__"; +static const std::string state_name = "selector_state"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Initializes a TestSelector"}; + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + std::map map_selectors; + clip.set_state(state_name, map_selectors); + + return 0; +} diff --git a/cpp/test/TestSelector/add.cpp b/cpp/test/TestSelector/add.cpp new file mode 100644 index 0000000..ac44485 --- /dev/null +++ b/cpp/test/TestSelector/add.cpp @@ -0,0 +1,58 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "add"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Adds a subselector"}; + clip.add_required("selector", "Parent Selector"); + clip.add_required("subname", "Description of new selector"); + clip.add_optional("desc", "Description", "EMPTY DESCRIPTION"); + clip.add_required_state>("selector_state", + "Internal container"); + + if (clip.parse(argc, argv)) { + return 0; + } + + + std::map sstate; + if(clip.has_state("selector_state")) { + sstate = clip.get_state>("selector_state"); + } + + auto jo = clip.get("selector"); + std::string subname = clip.get("subname"); + std::string desc = clip.get("desc"); + + std::string parentname; + try { + if(jo["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + parentname = jo["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + sstate[parentname+"."+subname] = desc; + + + clip.set_state("selector_state", sstate); + clip.update_selectors(sstate); + clip.return_self(); + + return 0; +} diff --git a/cpp/test/TestSelector/drop.cpp b/cpp/test/TestSelector/drop.cpp new file mode 100644 index 0000000..f451413 --- /dev/null +++ b/cpp/test/TestSelector/drop.cpp @@ -0,0 +1,52 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "drop"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Drops a subselector"}; + clip.add_required("selector", "Parent Selector"); + clip.add_required_state>("selector_state", + "Internal container"); + + if (clip.parse(argc, argv)) { + return 0; + } + + std::map sstate; + if(clip.has_state("selector_state")) { + sstate = clip.get_state>("selector_state"); + } + + auto jo = clip.get("selector"); + + std::string parentname; + try { + if(jo["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY " << std::endl; + exit(-1); + } + parentname = jo["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!" << std::endl; + exit(-1); + } + + sstate.erase(parentname); + + clip.set_state("selector_state", sstate); + clip.update_selectors(sstate); + clip.return_self(); + + return 0; +} diff --git a/cpp/test/TestSelector/meta.json b/cpp/test/TestSelector/meta.json new file mode 100644 index 0000000..d66d783 --- /dev/null +++ b/cpp/test/TestSelector/meta.json @@ -0,0 +1,7 @@ +{ + "__doc__" : "For testing selectors", + "initial_selectors" : { + "nodes" : "Top level for nodes", + "edges" : "Top level for edges" + } +} \ No newline at end of file diff --git a/cpp/test/TestSelector/selector.hpp b/cpp/test/TestSelector/selector.hpp new file mode 100644 index 0000000..2da089f --- /dev/null +++ b/cpp/test/TestSelector/selector.hpp @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include + +namespace testgraph { +class selector { + std::string name; + +public: + selector(boost::json::object &jo) { + try { + if (jo["expression_type"].as_string() != std::string("jsonlogic")) { + std::cerr << " NOT A THINGY\n"; + exit(-1); + } + name = jo["rule"].as_object()["var"].as_string().c_str(); + } catch (...) { + std::cerr << "!! ERROR !!\n"; + exit(-1); + } + } + [[nodiscard]] std::string to_string() const { return name; } +}; + +} // namespace testgraph diff --git a/cpp/test/TestSet/CMakeLists.txt b/cpp/test/TestSet/CMakeLists.txt new file mode 100644 index 0000000..54f28a1 --- /dev/null +++ b/cpp/test/TestSet/CMakeLists.txt @@ -0,0 +1,11 @@ +add_test(TestSet __init__) +add_test(TestSet __str__) +add_test(TestSet insert) +add_test(TestSet remove) +add_test(TestSet remove_if) +add_test(TestSet size) +add_custom_command( + TARGET TestSet_size POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/meta.json + ${CMAKE_CURRENT_BINARY_DIR}/meta.json) \ No newline at end of file diff --git a/cpp/test/TestSet/__init__.cpp b/cpp/test/TestSet/__init__.cpp new file mode 100644 index 0000000..c443221 --- /dev/null +++ b/cpp/test/TestSet/__init__.cpp @@ -0,0 +1,30 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include + +namespace boostjsn = boost::json; + + +static const std::string method_name = "__init__"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Initializes a TestSet of strings"}; + + + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + std::vector the_bag; + clip.set_state(state_name, the_bag); + + return 0; +} diff --git a/cpp/test/TestSet/__str__.cpp b/cpp/test/TestSet/__str__.cpp new file mode 100644 index 0000000..27b0f9a --- /dev/null +++ b/cpp/test/TestSet/__str__.cpp @@ -0,0 +1,38 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "__str__"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Str method for TestBag"}; + + clip.add_required_state>(state_name, + "Internal container"); + + clip.returns("String of data."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_set = clip.get_state>(state_name); + clip.set_state(state_name, the_set); + + std::stringstream sstr; + for (auto item : the_set) { + sstr << item << " "; + } + clip.to_return(sstr.str()); + + return 0; +} diff --git a/cpp/test/TestSet/insert.cpp b/cpp/test/TestSet/insert.cpp new file mode 100644 index 0000000..9a2090a --- /dev/null +++ b/cpp/test/TestSet/insert.cpp @@ -0,0 +1,34 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "insert"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Inserts a string into a TestSet"}; + clip.add_required("item", "Item to insert"); + clip.add_required_state>(state_name, + "Internal container"); + clip.returns_self(); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto item = clip.get("item"); + auto the_set = clip.get_state>(state_name); + the_set.insert(item); + clip.set_state(state_name, the_set); + clip.return_self(); + return 0; +} diff --git a/cpp/test/TestSet/meta.json b/cpp/test/TestSet/meta.json new file mode 100644 index 0000000..29135de --- /dev/null +++ b/cpp/test/TestSet/meta.json @@ -0,0 +1,6 @@ +{ + "__doc__" : "A set data structure", + "initial_selectors" : { + "value" : "A value in the container" + } +} \ No newline at end of file diff --git a/cpp/test/TestSet/remove.cpp b/cpp/test/TestSet/remove.cpp new file mode 100644 index 0000000..2ada005 --- /dev/null +++ b/cpp/test/TestSet/remove.cpp @@ -0,0 +1,36 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "remove"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + + clippy::clippy clip{method_name, "Removes a string from a TestSet"}; + + clip.add_required("item", "Item to remove"); + clip.add_required_state>(state_name, + "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto item = clip.get("item"); + auto the_set = clip.get_state>(state_name); + the_set.erase(item); + clip.set_state(state_name, the_set); + clip.return_self(); + return 0; +} diff --git a/cpp/test/TestSet/remove_if.cpp b/cpp/test/TestSet/remove_if.cpp new file mode 100644 index 0000000..2b86284 --- /dev/null +++ b/cpp/test/TestSet/remove_if.cpp @@ -0,0 +1,48 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "remove_if"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Removes a string from a TestSet"}; + clip.add_required("expression", "Remove If Expression"); + clip.add_required_state>(state_name, "Internal container"); + clip.returns_self(); + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto expression = clip.get("expression"); + auto the_set = clip.get_state>(state_name); + + // + // + // Expression here + jsonlogic::logic_rule jlrule = jsonlogic::create_logic(expression["rule"]); + + auto apply_jl = [&jlrule](int value) { + return truthy(jlrule.apply(jsonlogic::json_accessor({{"value", value}}))); + }; + + for (auto first = the_set.begin(), last = the_set.end(); first != last;) { + if (apply_jl(*first)) + first = the_set.erase(first); + else + ++first; + } + + clip.set_state(state_name, the_set); + clip.return_self(); + return 0; +} diff --git a/cpp/test/TestSet/size.cpp b/cpp/test/TestSet/size.cpp new file mode 100644 index 0000000..4fdf8a7 --- /dev/null +++ b/cpp/test/TestSet/size.cpp @@ -0,0 +1,31 @@ +// Copyright 2021 Lawrence Livermore National Security, LLC and other CLIPPy +// Project Developers. See the top-level COPYRIGHT file for details. +// +// SPDX-License-Identifier: MIT + +#include "clippy/clippy.hpp" +#include +#include +#include +#include + +namespace boostjsn = boost::json; + +static const std::string method_name = "size"; +static const std::string state_name = "INTERNAL"; + +int main(int argc, char **argv) { + clippy::clippy clip{method_name, "Returns the size of the set"}; + + clip.returns("Size of set."); + + // no object-state requirements in constructor + if (clip.parse(argc, argv)) { + return 0; + } + + auto the_set = clip.get_state>(state_name); + + clip.to_return(the_set.size()); + return 0; +} diff --git a/cpp/test/requirements.txt b/cpp/test/requirements.txt new file mode 100644 index 0000000..e13a1ff --- /dev/null +++ b/cpp/test/requirements.txt @@ -0,0 +1,2 @@ +llnl-clippy >= 0.4.2 +pytest>=7,<8 \ No newline at end of file diff --git a/cpp/test/test_clippy.py b/cpp/test/test_clippy.py new file mode 100644 index 0000000..585c114 --- /dev/null +++ b/cpp/test/test_clippy.py @@ -0,0 +1,205 @@ +# This should mirror test_clippy.py from the llnl-clippy repo. +import pytest +import sys + +sys.path.append("src") + +import jsonlogic as jl +import clippy +from clippy.error import ClippyValidationError, ClippyInvalidSelectorError + +import logging + +clippy.logger.setLevel(logging.WARN) +logging.getLogger().setLevel(logging.WARN) + + +@pytest.fixture() +def testbag(): + return clippy.TestBag() + + +@pytest.fixture() +def testset(): + return clippy.TestSet() + + +@pytest.fixture() +def testfun(): + return clippy.TestFunctions() + + +@pytest.fixture() +def testsel(): + return clippy.TestSelector() + + +@pytest.fixture() +def testgraph(): + return clippy.TestGraph() + + +def test_imports(): + assert "TestBag" in clippy.__dict__ + + +def test_bag(testbag): + + testbag.insert(41) + assert testbag.size() == 1 + testbag.insert(42) + assert testbag.size() == 2 + testbag.insert(41) + assert testbag.size() == 3 + testbag.remove(41) + assert testbag.size() == 2 + testbag.remove(99) + assert testbag.size() == 2 + + +def test_clippy_call_with_string(testfun): + assert testfun.call_with_string("Seth") == "Howdy, Seth" + with pytest.raises(ClippyValidationError): + testfun.call_with_string() + + +def test_expression_gt_gte(testbag): + testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + assert testbag.size() == 6 + testbag.remove_if(testbag.value > 51) + assert testbag.size() == 5 + testbag.remove_if(testbag.value >= 50) + assert testbag.size() == 3 + testbag.remove_if(testbag.value >= 99) + assert testbag.size() == 3 + + +def test_expression_lt_lte(testbag): + testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + testbag.remove_if(testbag.value < 42) + assert testbag.size() == 4 + testbag.remove_if(testbag.value <= 51) + assert testbag.size() == 1 + + +def test_expression_eq_neq(testbag): + testbag.insert(10).insert(11).insert(12) + assert testbag.size() == 3 + testbag.remove_if(testbag.value != 11) + assert testbag.size() == 1 + testbag.remove_if(testbag.value == 11) + assert testbag.size() == 0 + + +def test_expresssion_add(testbag): + testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + testbag.remove_if(testbag.value + 30 > 70) + assert testbag.size() == 1 + + +def test_expression_sub(testbag): + testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + testbag.remove_if(testbag.value - 30 > 0) + assert testbag.size() == 1 + + +def test_expression_mul_div(testbag): + testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + testbag.remove_if(testbag.value * 2 / 4 > 10) + assert testbag.size() == 1 + + +def test_expression_or(testbag): + testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + testbag.remove_if((testbag.value < 41) | (testbag.value > 49)) + assert testbag.size() == 2 # 41, 42 + + +def test_expression_and(testbag): + testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + testbag.remove_if((testbag.value > 40) & (testbag.value < 50)) + assert testbag.size() == 4 # 10, 50, 51, 52 + + +# TODO: not yet implemented +# def test_expression_floordiv(testbag): +# testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) +# testbag.remove_if(testbag.value * 2 // 4.2 > 10) +# assert testbag.size() == 1 + + +def test_expression_mod(testbag): + testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) + testbag.remove_if(testbag.value % 2 == 0) + assert testbag.size() == 2 + + +# TODO: not yet implemented +# def test_expression_pow(testbag): +# testbag.insert(10).insert(41).insert(42).insert(50).insert(51).insert(52) +# testbag.remove_if(testbag.value**2 > 1000) +# assert testbag.size() == 2 + + +def test_clippy_returns_int(testfun): + assert testfun.returns_int() == 42 + + +def test_clippy_returns_string(testfun): + assert testfun.returns_string() == "asdf" + + +def test_clippy_returns_bool(testfun): + assert testfun.returns_bool() + + +def test_clippy_returns_dict(testfun): + d = testfun.returns_dict() + assert len(d) == 3 + assert d.get("a") == 1 + assert d.get("b") == 2 + assert d.get("c") == 3 + + +def test_clippy_returns_vec_int(testfun): + assert testfun.returns_vec_int() == [0, 1, 2, 3, 4, 5] + + +def test_clippy_returns_optional_string(testfun): + assert testfun.call_with_optional_string() == "Howdy, World" + assert testfun.call_with_optional_string(name="Seth") == "Howdy, Seth" + + +def test_selectors(testsel): + assert hasattr(testsel, "nodes") + + testsel.add(testsel.nodes, "b", desc="docstring for nodes.b").add( + testsel.nodes.b, "c", desc="docstring for nodes.b.c" + ) + assert hasattr(testsel.nodes, "b") + assert hasattr(testsel.nodes.b, "c") + assert testsel.nodes.b.__doc__ == "docstring for nodes.b" + assert testsel.nodes.b.c.__doc__ == "docstring for nodes.b.c" + + assert isinstance(testsel.nodes.b, jl.Variable) + assert isinstance(testsel.nodes.b.c, jl.Variable) + + with pytest.raises(ClippyInvalidSelectorError): + testsel.add(testsel.nodes, "_bad", desc="this is a bad selector name") + + # with pytest.raises(ClippyInvalidSelectorError): + # testsel.add(testsel, 'bad', desc="this is a top-level selector") + + +def test_graph(testgraph): + testgraph.add_edge("a", "b").add_edge("b", "c").add_edge("a", "c").add_edge( + "c", "d" + ).add_edge("d", "e").add_edge("e", "f").add_edge("f", "g").add_edge("e", "g") + + assert testgraph.nv() == 7 + assert testgraph.ne() == 8 + + testgraph.add_series(testgraph.node, "degree", desc="node degrees") + testgraph.degree(testgraph.node.degree) + c_e_only = testgraph.dump2(testgraph.node.degree, where=testgraph.node.degree > 2) + assert "c" in c_e_only and "e" in c_e_only and len(c_e_only) == 2 From 6a5f7346c08d70f67424a147e3240e7d2c824b23 Mon Sep 17 00:00:00 2001 From: Seth Bromberger Date: Thu, 18 Dec 2025 16:23:52 -0800 Subject: [PATCH 10/10] move methods and variables to underline - requires accompanying jsonlogic PR --- py/src/clippy/backends/serialization.py | 2 +- py/src/clippy/selectors.py | 42 ++++++++++++------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/py/src/clippy/backends/serialization.py b/py/src/clippy/backends/serialization.py index 7a8f626..f1f8257 100644 --- a/py/src/clippy/backends/serialization.py +++ b/py/src/clippy/backends/serialization.py @@ -118,7 +118,7 @@ def encode_clippy_json(o: Any) -> Any: json encoder that is clippy-object aware. """ if isinstance(o, jl.Operand): # expression or variable - return {"expression_type": "jsonlogic", "rule": o.prepare()} + return {"expression_type": "jsonlogic", "rule": o._prepare()} return o diff --git a/py/src/clippy/selectors.py b/py/src/clippy/selectors.py index 5854606..f30d7c4 100644 --- a/py/src/clippy/selectors.py +++ b/py/src/clippy/selectors.py @@ -13,51 +13,51 @@ class Selector(jl.Variable): def __init__(self, parent: Selector | None, name: str, docstr: str): super().__init__(name, docstr) # op and o2 are None to represent this as a variable. - self.parent = parent - self.name = name - self.fullname: str = self.name if self.parent is None else f"{self.parent.fullname}.{self.name}" - self.subselectors: set[Selector] = set() + self._parent = parent + self._name = name + self._fullname: str = self._name if self._parent is None else f"{self._parent._fullname}.{self._name}" + self._subselectors: set[Selector] = set() def __hash__(self): - return hash(self.fullname) + return hash(self._fullname) - def prepare(self): - return {"var": self.fullname} + def _prepare(self): + return {"var": self._fullname} - def hierarchy(self, acc: list[tuple[str, str]] | None = None): + def _hierarchy(self, acc: list[tuple[str, str]] | None = None): if acc is None: acc = [] - acc.append((self.fullname, self.__doc__ or "")) - for subsel in self.subselectors: - subsel.hierarchy(acc) + acc.append((self._fullname, self.__doc__ or "")) + for subsel in self._subselectors: + subsel._hierarchy(acc) return acc - def describe(self): - hier = self.hierarchy() + def _describe(self): + hier = self._hierarchy() maxlen = max(len(sub_desc[0]) for sub_desc in hier) return "\n".join(f"{sub_desc[0]:<{maxlen + 2}} {sub_desc[1]}" for sub_desc in hier) def __str__(self): - return repr(self.prepare()) + return repr(self._prepare()) - def to_serial(self): - return {"var": self.fullname} + def _to_serial(self): + return {"var": self._fullname} def _add_subselector(self, name: str, docstr: str): """add a subselector to this selector""" subsel = Selector(self, name, docstr) setattr(self, name, subsel) - self.subselectors.add(subsel) + self._subselectors.add(subsel) def _del_subselector(self, name: str): delattr(self, name) - self.subselectors.remove(getattr(self, name)) + self._subselectors.remove(getattr(self, name)) def _clear_subselectors(self): """removes all subselectors""" - for subsel in self.subselectors: - delattr(self, subsel.name) - self.subselectors = set() + for subsel in self._subselectors: + delattr(self, subsel._name) + self._subselectors = set() def _import_from_dict(self, d: AnyDict, merge: bool = False): """Imports subselectors from a dictionary.