diff --git a/phreeqc-stubs/__init__.pyi b/phreeqc-stubs/__init__.pyi new file mode 100644 index 0000000..c827ff8 --- /dev/null +++ b/phreeqc-stubs/__init__.pyi @@ -0,0 +1,44 @@ +from typing import List + + +class Phreeqc: + def __init__(self) -> None: + """ + Create a Phreeqc instance. + """ + ... + + def get_selected_output_string(self) -> str: ... + + def load_database(self, path: str) -> None: + """ + Load the specified database file into phreeqc. + + :param path: The path of the phreeqc database to load. The full path (or relative path \ + with respect to the working directory) will be required if the file is not in the current working directory + """ + ... + + def run_file(self, path: str) -> None: + """ + Runs the specified phreeqc input file. + + :param path: The path of the phreeqc input file to run. + """ + ... + + def run_string(self, input: str) -> None: + """ + Runs the specified string as input to phreeqc. + + :param input: String containing phreeqc input. + """ + ... + + def get_selected_output(self) -> List[List]: + """ + Get the selected output. + + :return: A 2d-array consisting of each row from the selected output + """ + ... diff --git a/setup.py b/setup.py index a0db7e2..36a0896 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,8 @@ def build_extension(self, ext: CMakeExtension) -> None: # Using this requires trailing slash for auto-detection & inclusion of # auxiliary "native" libs - debug = int(os.environ.get("DEBUG", 0)) if self.debug is None else self.debug + debug = int(os.environ.get("DEBUG", 0) + ) if self.debug is None else self.debug cfg = "Debug" if debug else "Release" # CMake lets you override the generator - we need to check this. @@ -55,10 +56,12 @@ def build_extension(self, ext: CMakeExtension) -> None: # Adding CMake arguments set as environment variable # (needed e.g. to build for ARM OSx on conda-forge) if "CMAKE_ARGS" in os.environ: - cmake_args += [item for item in os.environ["CMAKE_ARGS"].split(" ") if item] + cmake_args += [ + item for item in os.environ["CMAKE_ARGS"].split(" ") if item] # In this example, we pass in the version to C++. You might not need to. - cmake_args += [f"-DEXAMPLE_VERSION_INFO={self.distribution.get_version()}"] + cmake_args += [f"-DEXAMPLE_VERSION_INFO={ + self.distribution.get_version()}"] if self.compiler.compiler_type != "msvc": # Using Ninja-build since it a) is available as a wheel and b) @@ -73,14 +76,16 @@ def build_extension(self, ext: CMakeExtension) -> None: ninja_executable_path = Path(ninja.BIN_DIR) / "ninja" cmake_args += [ "-GNinja", - f"-DCMAKE_MAKE_PROGRAM:FILEPATH={ninja_executable_path}", + f"-DCMAKE_MAKE_PROGRAM:FILEPATH={ + ninja_executable_path}", ] except ImportError: pass else: # Single config generators are handled "normally" - single_config = any(x in cmake_generator for x in {"NMake", "Ninja"}) + single_config = any( + x in cmake_generator for x in {"NMake", "Ninja"}) # CMake allows an arch-in-generator style for backward compatibility contains_arch = any(x in cmake_generator for x in {"ARM", "Win64"}) @@ -102,7 +107,8 @@ def build_extension(self, ext: CMakeExtension) -> None: # Cross-compile support for macOS - respect ARCHFLAGS if set archs = re.findall(r"-arch (\S+)", os.environ.get("ARCHFLAGS", "")) if archs: - cmake_args += ["-DCMAKE_OSX_ARCHITECTURES={}".format(";".join(archs))] + cmake_args += [ + "-DCMAKE_OSX_ARCHITECTURES={}".format(";".join(archs))] # Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level # across all generators. @@ -132,10 +138,12 @@ def build_extension(self, ext: CMakeExtension) -> None: version="0.0.1", author="Haohan Yang", description="Python bindings for PHREEQC Version 3", - long_description="", + long_description="Python bindings for PHREEQC Version 3", + packages=["phreeqc-stubs"], + package_data={"phreeqc-stubs": ["*.pyi"]}, ext_modules=[CMakeExtension("phreeqc")], cmdclass={"build_ext": CMakeBuild}, zip_safe=False, extras_require={"test": ["pytest>=6.0"]}, python_requires=">=3.7", -) \ No newline at end of file +) diff --git a/src/main.cpp b/src/main.cpp index bbaa17d..6b936dc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,6 @@ #include #include -#include "IPhreeqc.h" +#include "IPhreeqc.hpp" namespace py = pybind11; @@ -8,18 +8,20 @@ struct Phreeqc { Phreeqc() { - int res = CreateIPhreeqc(); - if (res < 0) + instance = new IPhreeqc(); + } + + ~Phreeqc() + { + if (instance != nullptr) { - throw std::runtime_error("Failed to create an IPhreeqc instance"); + delete instance; } - - id = res; } void loadDatabase(const char *path) { - int errors = LoadDatabase(id, path); + int errors = instance->LoadDatabase(path); if (errors > 0) { throw std::runtime_error("Failed to load database"); @@ -28,7 +30,7 @@ struct Phreeqc void runString(const char *input) { - int errors = RunString(id, input); + int errors = instance->RunString(input); if (errors > 0) { throw std::runtime_error("Failed to run string"); @@ -37,19 +39,60 @@ struct Phreeqc void runFile(const char *path) { - int errors = RunFile(id, path); + int errors = instance->RunFile(path); if (errors > 0) { throw std::runtime_error("Failed to run path"); } } - const char *getSelectedOutputString() + const py::list getSelectedOutput() { - return GetSelectedOutputString(id); + py::list output; + + VAR var; + VarInit(&var); + + const int colCount = instance->GetSelectedOutputColumnCount(); + const int rowCount = instance->GetSelectedOutputRowCount(); + + for (int row = 0; row < rowCount; row++) + { + py::list rowData; + for (int col = 0; col < colCount; col++) + { + if (instance->GetSelectedOutputValue(row, col, &var) != VR_OK) + { + rowData.append(py::str("error")); + } + else + { + if (var.type == TT_LONG) + { + rowData.append(py::int_(var.lVal)); + } + else if (var.type == TT_DOUBLE) + { + rowData.append(py::float_(var.dVal)); + } + else if (var.type == TT_STRING) + { + rowData.append(py::str(var.sVal)); + } + else + { + rowData.append(py::none()); + } + } + } + + output.append(rowData); + } + + return output; } - int id; + IPhreeqc *instance; }; PYBIND11_MODULE(phreeqc, m) @@ -60,5 +103,5 @@ PYBIND11_MODULE(phreeqc, m) .def("load_database", &Phreeqc::loadDatabase) .def("run_string", &Phreeqc::runString) .def("run_file", &Phreeqc::runFile) - .def("get_selected_output_string", &Phreeqc::getSelectedOutputString); + .def("get_selected_output", &Phreeqc::getSelectedOutput); } \ No newline at end of file