├── .gitignore ├── .gitmodules ├── .readthedocs.yaml ├── Binaries ├── Win32 │ └── Release │ │ ├── excelbind.pdb │ │ └── excelbind.xll └── x64 │ └── Release │ ├── excelbind.pdb │ └── excelbind.xll ├── LICENSE ├── Pipfile ├── README.md ├── TODO.md ├── azure-pipelines.yml ├── docs ├── Makefile ├── _static │ └── execute_python_simple_ex.png ├── building.rst ├── conf.py ├── examples.rst ├── getting_started.rst ├── index.rst ├── make.bat ├── troubleshooting.rst └── type_conversion.rst ├── examples ├── basic_functions.py ├── excelbind.conf └── excelbind_export.py ├── make.cmd ├── pytest.ini ├── src ├── CMakeLists.txt ├── excelbind │ ├── chrono.h │ ├── configuration.cpp │ ├── configuration.h │ ├── execute_python.cpp │ ├── fct_exports.asm │ ├── function_registration.cpp │ ├── python_function_adapter.cpp │ ├── python_function_adapter.h │ ├── script_manager.cpp │ ├── script_manager.h │ ├── type_conversion.cpp │ └── type_conversion.h └── thirdparty │ └── date.h └── test ├── __init__.py ├── conftest.py ├── test_execute_python.py ├── test_type_handling.py └── utilities ├── __init__.py ├── env_vars.py └── excel.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Python 35 | *.pyc 36 | .idea/ 37 | Pipfile.lock 38 | 39 | # Various VS stuff 40 | *.user 41 | .vs/ 42 | Debug/ 43 | Release/ 44 | !Binaries/x64/Release/ 45 | !Binaries/Win32/Release/ 46 | 47 | # sphinx docs 48 | docs/_build 49 | 50 | # cmake 51 | build/ 52 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/thirdparty/pybind11"] 2 | path = src/thirdparty/pybind11 3 | url = https://github.com/pybind/pybind11.git 4 | [submodule "src/thirdparty/inih"] 5 | path = src/thirdparty/inih 6 | url = https://github.com/jtilly/inih.git 7 | [submodule "src/thirdparty/xll12"] 8 | path = src/thirdparty/xll12 9 | url = https://github.com/RuneLjungmann/xll12.git 10 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.11" 12 | 13 | sphinx: 14 | configuration: docs/conf.py 15 | -------------------------------------------------------------------------------- /Binaries/Win32/Release/excelbind.pdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuneLjungmann/excelbind/bde6dbfce32f1b68e8b2179c82549f34ea2eb5b4/Binaries/Win32/Release/excelbind.pdb -------------------------------------------------------------------------------- /Binaries/Win32/Release/excelbind.xll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuneLjungmann/excelbind/bde6dbfce32f1b68e8b2179c82549f34ea2eb5b4/Binaries/Win32/Release/excelbind.xll -------------------------------------------------------------------------------- /Binaries/x64/Release/excelbind.pdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuneLjungmann/excelbind/bde6dbfce32f1b68e8b2179c82549f34ea2eb5b4/Binaries/x64/Release/excelbind.pdb -------------------------------------------------------------------------------- /Binaries/x64/Release/excelbind.xll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuneLjungmann/excelbind/bde6dbfce32f1b68e8b2179c82549f34ea2eb5b4/Binaries/x64/Release/excelbind.xll -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rune Ljungmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | pywin32 = "*" 10 | pandas = "*" 11 | numpy = "*" 12 | pytest = "*" 13 | sphinx = "==1.8.5" 14 | sphinx_rtd_theme = "*" 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Excelbind - expose your python code in Excel 2 | 3 | [![Documentation Status](https://readthedocs.org/projects/excelbind/badge/?version=latest)](https://excelbind.readthedocs.io/en/latest/?badge=latest) 4 | [![Build Status](https://dev.azure.com/runeljungmann/excelbind/_apis/build/status/RuneLjungmann.excelbind?branchName=master)](https://dev.azure.com/runeljungmann/excelbind/_build/latest?definitionId=1&branchName=master) 5 | 6 | Excelbind is a free open-source Excel Add-in, that expose your python code to Excel users in an easy and userfriendly way. 7 | 8 | Excelbind uses the Excel xll api together with an embedded python interpreter, 9 | so your python code runs within the Excel process. This means that the overhead of calling a python function is just a few nano seconds. 10 | 11 | You can both expose already existing python functions in Excel as well as run python code directly from Excel. 12 | 13 | To expose a python function in Excel as a user-defined function (UDFs) just use the excelbind function decorator: 14 | 15 | import excelbind 16 | 17 | @excelbind.function 18 | def my_simple_python_function(x: float, y: float) -> float: 19 | return x + y 20 | 21 | To write python code directly inside Excel just use the execute_python function: 22 | 23 | =execute_python("return arg1 + arg2", A1, B1) 24 | 25 | ## Documentation 26 | 27 | The Excelbind documentation can be found at https://excelbind.readthedocs.io/en/latest/ 28 | 29 | ## Roadmap 30 | See the [TODO](TODO.md) file. 31 | 32 | ## Author 33 | Rune Ljungmann 34 | 35 | ## License 36 | [MIT](LICENSE.txt) 37 | 38 | ## Acknowledgments 39 | The project mainly piggybacks on two other projects: 40 | 41 | - [pybind11](https://github.com/pybind/pybind11), which provides a modern C++ api towards python 42 | - [xll12](https://github.com/keithalewis/xll12), which provides a modern C++ wrapper around the xll api and makes it easy to build Excel Add-ins. 43 | 44 | Excelbind basically ties these two together to expose python to Excel users. 45 | 46 | A big source of inspiration is the [ExcelDNA](https://github.com/Excel-DNA/ExcelDna>) project. 47 | A very mature project, which provides Excel xll bindings for C#. 48 | Especially the semi-dynamic exposure of functions in the dll api 49 | using a thunks table is inspired by the way this is handled in ExcelDNA. 50 | 51 | Excelbind also uses: 52 | 53 | - A [headers only version](https://github.com/jtilly/inih) 54 | of [inih](https://github.com/benhoyt/inih) for parsing ini files 55 | - The [date library](https://github.com/HowardHinnant/date) by Howard Hinnant. 56 | 57 | There are at least two other projects solving a similar problem to Excelbind: 58 | 59 | - [xlwings](https://www.xlwings.org/) using a COM-server approach rather than using xll+an embedded interpreter. 60 | - [pyxll](https://www.pyxll.com/) which is closed-source. 61 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - Expose general xll api to python when it makes sense such as: 2 | - menu functions 3 | - control pop-ups from python 4 | - async functions 5 | - cancellations 6 | - flags indicating if a function is called from the function wizard 7 | - Streaming api / RTD (COM server) 8 | - Ribbon configuration 9 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # The Azure pipeline builds the x86 and x64 release builds. 2 | # No testing is performed in the pipeline, as that would need Excel installed on the build agent. 3 | 4 | trigger: 5 | - master 6 | 7 | variables: 8 | solution: '**/excelbind.sln' 9 | buildConfiguration: 'Release' 10 | 11 | jobs: 12 | - job: 'Build' 13 | strategy: 14 | matrix: 15 | 2022_Release_Win32: 16 | pythonVersion: '3.11' 17 | python_Version: 'python311' 18 | buildPlatformPython: 'x86' 19 | buildPlatform: 'Win32' 20 | buildConfiguration: 'Release' 21 | vsNumber: 2022 22 | vsName: 'Visual Studio 17 2022' 23 | imageName: 'windows-latest' 24 | 25 | 2022_Release_x64_py311: 26 | pythonVersion: '3.11' 27 | python_Version: 'python311' 28 | buildPlatformPython: 'x64' 29 | buildPlatform: 'x64' 30 | buildConfiguration: 'Release' 31 | vsNumber: 2022 32 | vsName: 'Visual Studio 17 2022' 33 | imageName: 'windows-latest' 34 | 35 | 36 | 2022_Release_x64_py310: 37 | pythonVersion: '3.10' 38 | python_Version: 'python310' 39 | buildPlatformPython: 'x64' 40 | buildPlatform: 'x64' 41 | buildConfiguration: 'Release' 42 | vsNumber: 2022 43 | vsName: 'Visual Studio 17 2022' 44 | imageName: 'windows-latest' 45 | 46 | pool: 47 | vmImage: $(imageName) 48 | 49 | steps: 50 | - checkout: self 51 | submodules: true 52 | persistCredentials: true 53 | 54 | - task: UsePythonVersion@0 55 | inputs: 56 | versionSpec: $(pythonVersion) 57 | addToPath: true 58 | architecture: $(buildPlatformPython) 59 | 60 | - script: set 61 | displayName: View env vars 62 | 63 | - task: NuGetToolInstaller@1 64 | 65 | - task: CmdLine@2 66 | inputs: 67 | script: | 68 | cmake -S src -B build/$(vsNumber)$(buildPlatform) -G "$(vsName)" -A $(buildPlatform) 69 | 70 | - task: VSBuild@1 71 | inputs: 72 | solution: 'build/$(vsNumber)$(buildPlatform)/excelbind.sln' 73 | platform: '$(buildPlatform)' 74 | configuration: '$(buildConfiguration)' 75 | 76 | - task: CmdLine@2 77 | condition: and(eq(variables['vsNumber'], 2019), eq(variables['pythonVersion'], '3.7')) 78 | inputs: 79 | script: | 80 | git config user.email "53918463+RuneLjungmann@users.noreply.github.com" 81 | git config user.name "RuneLjungmann" 82 | git checkout master 83 | git commit -a -m "Azure Pipeline: Updated binaries" -m "[skip ci]" 84 | git push origin -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/_static/execute_python_simple_ex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuneLjungmann/excelbind/bde6dbfce32f1b68e8b2179c82549f34ea2eb5b4/docs/_static/execute_python_simple_ex.png -------------------------------------------------------------------------------- /docs/building.rst: -------------------------------------------------------------------------------- 1 | Building the xll Add-in yourself 2 | ================================ 3 | 4 | The project uses CMake to properly support different versions of Visual Studio. 5 | 6 | There is a small batch file available to run cmake and build the solution. 7 | 8 | To use this, start a visual studio command prompt (Search for 'Developer Command Prompt') and type e.g.:: 9 | 10 | make 2022 Release x64 11 | 12 | To create solution files and build the solution using Visual Studio 2022. 13 | 14 | The solution file will now be located in build/2022x64 and you can open it in the Visual Studio IDE. 15 | 16 | The excelbind.xll is now located under Binaries/x64/Release subfolder. 17 | 18 | Running the tests 19 | ----------------- 20 | If you haven't already done so, start by installing the pipenv tool in your python environment:: 21 | 22 | pip install pipenv 23 | 24 | Now create the virtual environment used for testing. Go to the top excelbind folder and type:: 25 | 26 | pipenv install 27 | 28 | You can now run the test by typing:: 29 | 30 | pipenv run pytest 31 | 32 | The test assumes that you have build the excelbind.xll in x64 release mode. 33 | You can manually change the tests to use the debug build or one of the x86 builds, by changing the xll path in test/conftest.py. 34 | 35 | Note however, that unless you have a debug build of numpy, 36 | anything that imports numpy in debug mode will fail due to issues loading the underlying numpy dlls. 37 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | import sphinx_rtd_theme 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'excelbind' 24 | copyright = '2019, Rune Ljungmann' 25 | author = 'Rune Ljungmann' 26 | 27 | # The short X.Y version 28 | version = '' 29 | # The full version, including alpha/beta/rc tags 30 | release = '' 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | 'sphinx_rtd_theme' 44 | ] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # The suffix(es) of source filenames. 50 | # You can specify multiple suffix as a list of string: 51 | # 52 | # source_suffix = ['.rst', '.md'] 53 | source_suffix = '.rst' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | # 61 | # This is also used if you do content translation via gettext catalogs. 62 | # Usually you set "language" from the command line for these cases. 63 | language = None 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | # This pattern also affects html_static_path and html_extra_path. 68 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 69 | 70 | # The name of the Pygments (syntax highlighting) style to use. 71 | pygments_style = None 72 | 73 | 74 | # -- Options for HTML output ------------------------------------------------- 75 | 76 | # The theme to use for HTML and HTML Help pages. See the documentation for 77 | # a list of builtin themes. 78 | # 79 | html_theme = "sphinx_rtd_theme" 80 | 81 | # Theme options are theme-specific and customize the look and feel of a theme 82 | # further. For a list of options available for each theme, see the 83 | # documentation. 84 | # 85 | # html_theme_options = {} 86 | 87 | # Add any paths that contain custom static files (such as style sheets) here, 88 | # relative to this directory. They are copied after the builtin static files, 89 | # so a file named "default.css" will overwrite the builtin "default.css". 90 | html_static_path = ['_static'] 91 | 92 | # Custom sidebar templates, must be a dictionary that maps document names 93 | # to template names. 94 | # 95 | # The default sidebars (for documents that don't match any pattern) are 96 | # defined by theme itself. Builtin themes are using these templates by 97 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 98 | # 'searchbox.html']``. 99 | # 100 | # html_sidebars = {} 101 | 102 | 103 | # -- Options for HTMLHelp output --------------------------------------------- 104 | 105 | # Output file base name for HTML help builder. 106 | htmlhelp_basename = 'excelbinddoc' 107 | 108 | 109 | # -- Options for LaTeX output ------------------------------------------------ 110 | 111 | latex_elements = { 112 | # The paper size ('letterpaper' or 'a4paper'). 113 | # 114 | # 'papersize': 'letterpaper', 115 | 116 | # The font size ('10pt', '11pt' or '12pt'). 117 | # 118 | # 'pointsize': '10pt', 119 | 120 | # Additional stuff for the LaTeX preamble. 121 | # 122 | # 'preamble': '', 123 | 124 | # Latex figure (float) alignment 125 | # 126 | # 'figure_align': 'htbp', 127 | } 128 | 129 | # Grouping the document tree into LaTeX files. List of tuples 130 | # (source start file, target name, title, 131 | # author, documentclass [howto, manual, or own class]). 132 | latex_documents = [ 133 | (master_doc, 'excelbind.tex', 'excelbind Documentation', 134 | 'Rune Ljungmann', 'manual'), 135 | ] 136 | 137 | 138 | # -- Options for manual page output ------------------------------------------ 139 | 140 | # One entry per manual page. List of tuples 141 | # (source start file, name, description, authors, manual section). 142 | man_pages = [ 143 | (master_doc, 'excelbind', 'excelbind Documentation', 144 | [author], 1) 145 | ] 146 | 147 | 148 | # -- Options for Texinfo output ---------------------------------------------- 149 | 150 | # Grouping the document tree into Texinfo files. List of tuples 151 | # (source start file, target name, title, author, 152 | # dir menu entry, description, category) 153 | texinfo_documents = [ 154 | (master_doc, 'excelbind', 'excelbind Documentation', 155 | author, 'excelbind', 'One line description of project.', 156 | 'Miscellaneous'), 157 | ] 158 | 159 | 160 | # -- Options for Epub output ------------------------------------------------- 161 | 162 | # Bibliographic Dublin Core info. 163 | epub_title = project 164 | 165 | # The unique identifier of the text. This can be a ISBN number 166 | # or the project homepage. 167 | # 168 | # epub_identifier = '' 169 | 170 | # A unique identification for the text. 171 | # 172 | # epub_uid = '' 173 | 174 | # A list of files that should not be packed into the epub file. 175 | epub_exclude_files = ['search.html'] 176 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | Below we go through a few basic examples on how to use Excelbind. You can find more examples in the examples folder in the `Github source repository `_. 5 | 6 | Exposing python functions 7 | ------------------------- 8 | 9 | To get started, just import the ``excelbind`` library in your code and 10 | then use the ``excelbind.function`` decorator to mark the functions you want to export. 11 | The Excelbind library is automatically part of the builtin libraries in the embedded interpreter that Excelbind uses, 12 | so you don't need to install any packages yourself. You can simply write:: 13 | 14 | import excelbind 15 | 16 | @excelbind.function 17 | def add(x, y): 18 | return x + y 19 | 20 | As described in the :doc:`type_conversion`, it is better to include type hints in your function when possibly. 21 | If you add comments to your code, Excelbind will also automatically pick these up and expose them in the function wizard:: 22 | 23 | import excelbind 24 | 25 | @excelbind.function 26 | def add(x: float, y: float) -> float: 27 | """ Adds two numbers together! 28 | 29 | :param x: The first number 30 | :param y: The second number 31 | :return: The sum! 32 | """ 33 | return x + y 34 | 35 | You can easily combine Excelbind with other libraries such as numpy:: 36 | 37 | import excelbind 38 | import numpy as np 39 | 40 | @excelbind.function 41 | def inv(x: np.ndarray) -> np.ndarray: 42 | """Matrix inversion 43 | 44 | :param x: An invertible matrix 45 | :return: The inverse 46 | """ 47 | return np.linalg.inv(x) 48 | 49 | 50 | Execute python code directly from Excel 51 | --------------------------------------- 52 | Excelbind lets you execute python code directly in Excel using the *excelbind.execute_python* function. 53 | 54 | .. image:: _static/execute_python_simple_ex.png 55 | 56 | The function wraps your input code in a simple python function which takes up to 10 arguments. 57 | The arguments can be referenced in the function using the names *arg0*, *arg1*, ..., *arg9*. 58 | The function accept any of the auto discoverable types such as floats, strings, lists and dictionaries. 59 | 60 | You can also perform more advanced operations inside the *execute_python* function, such as import libraries, open files etc. 61 | The execution happens inside a local python function, so import statements or local variables in one function will not affect other functions. 62 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | Before you get started, you need Excel and Python installed on your machine. 5 | 6 | - You need Excel 2007 or higher and Python 3.10 or higher. 7 | - Make sure they are either both x86 or x64 - see the :doc:`troubleshooting` section if in doubt. 8 | - You need the location of Python in your PATH environment variable 9 | 10 | You are now ready to get up and running in a few steps: 11 | 12 | - Download the Excelbind repository from `Github `_ 13 | Choose the green 'Clone or download' button on the right hand side to download the code. 14 | 15 | - You need to set up a config file in your user directory. 16 | You can just copy the example config file examples/excelbind.conf to get started. 17 | The example file contains explanations of the various settings. 18 | 19 | Your user directory will most likely be something like 'C:/users//' 20 | 21 | 22 | - You need some python functions that you want to run from Excel. 23 | As a first exmaple you can just pick the example file examples/excelbind_export.py 24 | and copy that to your user directory along with the config file above. 25 | 26 | You can later change the location and name of the python file you want 27 | to export by updating the corresponding settings in the config file. 28 | 29 | 30 | - Open Excel and open the excelbind.xll file. 31 | The file is either:: 32 | 33 | Binaries/x86/excelbind.xll or Binaries/x64/excelbind.xll 34 | 35 | Depending on your Excel and Python installations being x86 or x64. 36 | 37 | If you are in doubt about what to choose look in the :doc:`troubleshooting` section. 38 | 39 | If you want to build the excelbind.xll file from scratch, have a look in the :doc:`building` section. 40 | 41 | You can now call one of the python functions from Excel like:: 42 | 43 | =excelbind.add(2, 2) 44 | 45 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. meta:: 2 | :google-site-verification: 3-t0RDVvZu0ybpiusc6No1abuvIRCuKAygdTMBsJWdc 3 | 4 | Excelbind - Expose your python code in Excel 5 | ============================================ 6 | 7 | Excelbind is a free open-source Excel Add-in, that expose your python code to Excel users in an easy and userfriendly way. 8 | 9 | Excelbind uses the Excel xll api together with an embedded python interpreter, 10 | so your python code runs within the Excel process. This means that the overhead of calling a python function is just a few nano seconds. 11 | 12 | You can both expose already existing python functions in Excel as well as run python code directly from Excel. 13 | 14 | To expose a python function in Excel as a user-defined function (UDFs) just use the excelbind function decorator:: 15 | 16 | import excelbind 17 | 18 | @excelbind.function 19 | def my_simple_python_function(x: float, y: float) -> float: 20 | return x + y 21 | 22 | To write python code directly inside Excel just use the execute_python function:: 23 | 24 | =execute_python("return arg1 + arg2", A1, B1) 25 | 26 | For more info on how to use Excelbind see the :doc:`getting_started` section. 27 | 28 | The code and Add-in available at `GitHub `_. 29 | 30 | Requirements 31 | ------------ 32 | - Excel 2007 or higher 33 | - Python 3.10 or higher 34 | 35 | If you want to build the project yourself you will also need: 36 | 37 | - Visual Studio 2022 38 | - CMake version 3.10 or higher 39 | - The pipenv python package installed 40 | 41 | Acknowledgments 42 | --------------- 43 | The project mainly piggybacks on two other projects: 44 | 45 | - `pybind11 `_, which provides a modern C++ api towards python 46 | - `xll12 `_, which provides a modern C++ wrapper around the xll api and makes it easy to build Excel Add-ins. 47 | 48 | Excelbind basically ties these two together to expose python to Excel users. 49 | 50 | A big source of inspiration is the 51 | `ExcelDNA `_ project. 52 | A very mature project, which provides Excel xll bindings for C#. 53 | Especially the semi-dynamic exposure of functions in the dll api 54 | using a thunks table is inspired by the way this is handled in ExcelDNA. 55 | 56 | Excelbind also uses: 57 | 58 | - A `headers only version `_ of `inih `_ for parsing ini files. 59 | - The `date library `_ by Howard Hinnant. 60 | 61 | There are at least two other projects solving a similar problem to Excelbind: 62 | 63 | - `xlwings `_ using a COM-server approach rather than using xll+an embedded interpreter. 64 | - `pyxll `_ which is closed-source. 65 | 66 | 67 | .. toctree:: 68 | :hidden: 69 | :maxdepth: 2 70 | :caption: Contents: 71 | 72 | getting_started 73 | examples 74 | type_conversion 75 | building 76 | troubleshooting -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | Troubleshooting 2 | =============== 3 | 4 | - When you open the excelbind.xll in Excel, you get the error: 'The file format and extension of 'excelbind.xll' does not match... 5 | This is usually because the xll can't find a dependency. Most likely you don't have your Python installation in your path, or there is a mismatch between the xll and the python installation in your path. I.e. one is x86 and the other x64. 6 | 7 | - I don't know if I should use the x86 or x64 version of excelbind.xll 8 | Basically this is determined by your Excel installation. 9 | 10 | The way to identify if your Excel installation is x86 or x64 differs 11 | a bit between different Excel versions. 12 | In Excel 365 you go File -> Account -> About Excel and then line 2 will tell you if it is a 13 | 32-bit installation (= x86) or a 64-bit installation (= x64). 14 | 15 | Note that your Python installation (the one in your PATH) needs to match the Excel installation. 16 | 17 | To identify what Python installation you are running, open a command prompt and type:: 18 | 19 | python 20 | 21 | Then the first line will say if it is 32-bit (= x86) or 64-bit (= x64). 22 | 23 | - You get a build error 'The project file ".../src/thirdparty/xll12/xll12.vcxproj" was not found. 24 | You most likely didn't pull the git submodules in src/thirdparty. 25 | 26 | - You get a popup with an import error related to numpy when you load the excelbind.xll. The error message contains 'No module named "numpy.core._multiarray_umath"' 27 | This happens when you import numpy from a debug build. You will either have to skip the import while debugging, use release build or maybe try to build numpy manually in debug mode. 28 | 29 | -------------------------------------------------------------------------------- /docs/type_conversion.rst: -------------------------------------------------------------------------------- 1 | Type conversion 2 | =============== 3 | One of the main jobs for Excelbind is to convert data types between Excel and the exposed python code. 4 | If you provide type annotations with the python functions you expose, 5 | then Excelbind will use that information to convert Excel types to Python types and vice versa. 6 | 7 | If a function is missing type annotation, then Excelbind will try to infer the type from the input. 8 | 9 | Currently Excelbind handles floats, strings, datetimes, lists, dictionaries and ndarrays. 10 | 11 | Auto discovery 12 | -------------- 13 | The ``float``, ``string``, ``list`` and ``dict`` types are all auto discovered. 14 | 15 | You should however strive to use type annotations when possible. 16 | 17 | - It will give better performance, as Excelbind does not need to guess the type from the input, but knows it up front. 18 | - For floats and strings Excel will do an initial type check, so if you tried to pass a string when your type annotation said float, Excel will not try to call the function, but return an error right away. 19 | 20 | Note that the *execute_python* function (See the :doc:`examples` section) only works with types that are auto discoverable. 21 | 22 | Type details 23 | ------------ 24 | 25 | Lists and dictionaries 26 | ^^^^^^^^^^^^^^^^^^^^^^ 27 | Excelbind will recognise a type as a `` list`` or ``dict`` if the type annotation says *List* or *Dict*. 28 | So specifically you should not specify the element/key/value types, 29 | instead these are assumed to be either floats or strings and a given container can hold a mix of these. 30 | 31 | Excelbinds auto discovery rules will recognise a range as a ``list``, if either the number of rows or the number columns is one. 32 | It will recognise a range as a ``dict``, if either the number of rows or number of columns is two. 33 | 34 | Numpy arrays 35 | ^^^^^^^^^^^^ 36 | Excelbind will recognise a type as ``ndarray`` if the type annotation says *ndarray*, *np.ndarray* or *numpy.ndarray*. 37 | The numpy arrays are assumed to be (dense) two-dimensional matrices. 38 | 39 | datetime 40 | ^^^^^^^^ 41 | Excelbind will recognise a type as ``datetime`` if the type annotation says *datetime* or *datetime.datetime*. 42 | Auto discovery is currently not supported, as Excel passes dates and datetimes as numbers to the xll layer. 43 | 44 | Note that due to `Excels special handling of the year 1900 as a leap year 45 | `_, 46 | and in order to simplify the internal implementation, Excelbind will not convert days between 1900-01-01 and 1900-03-01 correctly. 47 | In an Excel context this is most likely not a problem. 48 | 49 | Pandas data structures 50 | ^^^^^^^^^^^^^^^^^^^^^^ 51 | Excelbind supports both Pandas Series and DataFrames. 52 | It will recognise a type as ``Series`` if the type annotation says *Series*, *pd.Series* or *pandas.Series*. 53 | It will recognise a type as ``DataFrame`` if the type annotation says *DataFrame*, *pd.DataFrame* or *pandas.DataFrame* 54 | 55 | For both data structures it is optional to supply a row/column of indices.. 56 | -------------------------------------------------------------------------------- /examples/basic_functions.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict 2 | import time 3 | import datetime 4 | import numpy as np 5 | import pandas as pd 6 | 7 | import excelbind 8 | 9 | 10 | @excelbind.function 11 | def add(x: float, y: float) -> float: 12 | return x + y 13 | 14 | 15 | @excelbind.function 16 | def mult(x: float, y: float) -> float: 17 | return x*y 18 | 19 | 20 | @excelbind.function 21 | def det(x: np.ndarray) -> float: 22 | return np.linalg.det(x) 23 | 24 | 25 | @excelbind.function 26 | def inv(x: np.ndarray) -> np.ndarray: 27 | """Matrix inversion 28 | 29 | :param x: An invertible matrix 30 | :return: The inverse 31 | """ 32 | return np.linalg.inv(x) 33 | 34 | 35 | @excelbind.function 36 | def concat(s1: str, s2: str) -> str: 37 | """Concatenates two strings 38 | 39 | :param s1: String 1 40 | :param s2: String 2 41 | :return: The two strings concatenated 42 | """ 43 | return s1 + s2 44 | 45 | 46 | @excelbind.function 47 | def add_without_type_info(x, y): 48 | return x + y 49 | 50 | 51 | @excelbind.function 52 | def listify(x, y, z) -> List: 53 | return [x, y, z] 54 | 55 | 56 | @excelbind.function 57 | def filter_dict(d: Dict, filter: str) -> Dict: 58 | return {key: val for key, val in d.items() if key != filter} 59 | 60 | 61 | @excelbind.function 62 | def dot(x: List, y: List) -> float: 63 | return sum(a*b for a, b in zip(x, y)) 64 | 65 | 66 | @excelbind.function 67 | def no_arg() -> str: 68 | return 'Hello world!' 69 | 70 | 71 | @excelbind.function 72 | def date_as_string(d: datetime.datetime) -> str: 73 | return str(d) 74 | 75 | 76 | @excelbind.function 77 | def just_the_date(d: datetime.datetime) -> datetime.datetime: 78 | return d 79 | 80 | 81 | @excelbind.function 82 | def pandas_series(s: pd.Series) -> pd.Series: 83 | return s.abs() 84 | 85 | 86 | @excelbind.function 87 | def pandas_series_sum(s: pd.Series) -> float: 88 | return s.sum() 89 | 90 | 91 | @excelbind.function 92 | def pandas_dataframe(df: pd.DataFrame) -> pd.DataFrame: 93 | return df + 2 94 | 95 | 96 | @excelbind.function(is_volatile=True) 97 | def the_time(): 98 | return time.clock() 99 | -------------------------------------------------------------------------------- /examples/excelbind.conf: -------------------------------------------------------------------------------- 1 | # Example config file for excelbind. 2 | # This config file is used to override excelbind default behaviour in terms of virtual environment, search paths etc. 3 | # Excelbind expects it to be named 'excelbind.conf' and be located in the users profile folder specified by the environment variable USERPROFILE. 4 | 5 | [excelbind] 6 | 7 | 8 | # Specify where to look for python modules to expose in Excel. 9 | # If a virtual environment is not set, it is also possible to control this though the PYTHONPATH environment variable. 10 | # If the environment variable EXCELBIND_MODULEDIR is set, it takes precedence over the setting here 11 | ModuleDir= 12 | 13 | 14 | # Name of the python module to expose in Excel. 15 | # Note that other modules can be exposed through imports into this module 16 | # default is 'excelbind_export' - I.e. your module should be named 'excelbind_export.py' and be placed in ModuleDir or in a directory included in PYTHONPATH 17 | # If the environment variable EXCELBIND_MODULENAME is set, it takes precedence over the setting here 18 | ModuleName= 19 | 20 | 21 | # Specify if a virtual environment should be used instead of the default environment (as specified in the PATH env). 22 | # Note that if a virtual environment is specified, PYTHONPATH will not be used. 23 | # If the environment variable EXCELBIND_VIRTUALENV is set, it takes precedence over the setting here 24 | VirtualEnv= 25 | 26 | 27 | # Toggle pop-up error messages when underlying code throws an exception. 28 | # Default is 'false'. In this case functions will just return #NUM when an exception is thrown. 29 | EnableErrorMessages=false 30 | 31 | 32 | # Add a common prefix to all functions exposed through excelbind. 33 | # Useful to avoid name collisions and this can make it easier for users to locate your functions. 34 | # If the environment variable EXCELBIND_FUNCTIONPREFIX is set, it takes precedence over the setting here 35 | FunctionPrefix= 36 | 37 | 38 | # Category name used in Excels function wizard 39 | # If the environment variable EXCELBIND_EXCELCATEGORY is set, it takes precedence over the setting here 40 | ExcelCategory=Excelbind -------------------------------------------------------------------------------- /examples/excelbind_export.py: -------------------------------------------------------------------------------- 1 | import excelbind 2 | 3 | 4 | @excelbind.function 5 | def add(x: float, y: float) -> float: 6 | return x + y 7 | 8 | 9 | @excelbind.function 10 | def mult(x: float, y: float) -> float: 11 | return x*y 12 | -------------------------------------------------------------------------------- /make.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM %1 is visual studio version (2019 or 2022) 3 | REM %2 is configuration (Release or Debug) 4 | REM %3 is platform (x86 or x64) 5 | 6 | REM Validating input 7 | IF "%~1" == "2019" ( 8 | SET VISUAL_STUDIO="Visual Studio 16 2019" 9 | ) ELSE ( 10 | IF "%~1" == "2022" ( 11 | SET VISUAL_STUDIO="Visual Studio 17 2022" 12 | ) ELSE ( 13 | GOTO Error 14 | ) 15 | ) 16 | 17 | IF "%~2" NEQ "Release" ( 18 | IF "%~2" NEQ "Debug" ( 19 | GOTO Error 20 | ) 21 | ) 22 | 23 | IF "%~3" NEQ "x64" ( 24 | IF "%~3" NEQ "Win32" ( 25 | GOTO Error 26 | ) 27 | ) 28 | 29 | cmake -S src -B build/%1%3 -G %VISUAL_STUDIO% -A %3 30 | if %errorlevel% NEQ 0 exit /b 1 31 | 32 | msbuild build/%1%3/excelbind.sln /t:Rebuild /p:Configuration=%2 /p:Platform=%3 33 | if %errorlevel% NEQ 0 exit /b 1 34 | 35 | GOTO EOF 36 | 37 | :Error 38 | echo "Calling convention is 'make compilerVersion configuration platform'" 39 | echo "Where compilerVersion in [2019, 2022], configuration in [Release, Debug] and platform in [Win32, x64]" 40 | exit /b 1 41 | :EOF -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore::DeprecationWarning:pywintypes.*: 4 | 5 | testpaths = test -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.10) 2 | 3 | project(excelbind CXX ASM_MASM) 4 | 5 | set(CMAKE_CXX_STANDARD 17) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | set(CMAKE_CXX_EXTENSIONS OFF) 8 | 9 | # set env var PYTHON_VERSION or change the lines below if you plan to build against python35 or python36. 10 | if(DEFINED ENV{PYTHON_VERSION}) 11 | set(PYTHON_VERSION $ENV{PYTHON_VERSION}) 12 | else() 13 | set(PYTHON_VERSION python311) 14 | endif() 15 | 16 | 17 | 18 | # Checks if the USEPYTHONVERSION_PYTHONLOCATION env var is set 19 | # (by e.g. Azure pipeline) otherwise default to standard location for python installation. 20 | if(DEFINED ENV{USEPYTHONVERSION_PYTHONLOCATION}) 21 | set(PYTHON_HOME $ENV{USEPYTHONVERSION_PYTHONLOCATION}) 22 | elseif("${CMAKE_GENERATOR_PLATFORM}" STREQUAL "Win32") 23 | set(PYTHON_HOME $ENV{USERPROFILE}/AppData/Local/Programs/Python/${PYTHON_VERSION}-32) 24 | else() 25 | set(PYTHON_HOME $ENV{USERPROFILE}/AppData/Local/Programs/Python/${PYTHON_VERSION}) 26 | endif() 27 | 28 | file(GLOB SOURCES 29 | ${CMAKE_SOURCE_DIR}/excelbind/chrono.h 30 | ${CMAKE_SOURCE_DIR}/excelbind/configuration.h 31 | ${CMAKE_SOURCE_DIR}/excelbind/python_function_adapter.h 32 | ${CMAKE_SOURCE_DIR}/excelbind/script_manager.h 33 | ${CMAKE_SOURCE_DIR}/excelbind/type_conversion.h 34 | 35 | ${CMAKE_SOURCE_DIR}/excelbind/configuration.cpp 36 | ${CMAKE_SOURCE_DIR}/excelbind/execute_python.cpp 37 | ${CMAKE_SOURCE_DIR}/excelbind/function_registration.cpp 38 | ${CMAKE_SOURCE_DIR}/excelbind/python_function_adapter.cpp 39 | ${CMAKE_SOURCE_DIR}/excelbind/script_manager.cpp 40 | ${CMAKE_SOURCE_DIR}/excelbind/type_conversion.cpp 41 | 42 | ${CMAKE_SOURCE_DIR}/excelbind/fct_exports.asm 43 | ) 44 | 45 | include_directories(${CMAKE_SOURCE_DIR}/thirdparty) 46 | include_directories(${CMAKE_SOURCE_DIR}/thirdparty/pybind11/include) 47 | include_directories(${PYTHON_HOME}/include) 48 | 49 | add_library(excelbind SHARED ${SOURCES}) 50 | 51 | add_compile_definitions(_LIB) 52 | add_compile_definitions(_WINDOWS) 53 | add_compile_definitions(EXCELBIND_EXPORTS) 54 | add_compile_definitions(_USRDLL) 55 | 56 | add_compile_definitions($<$:_DEBUG>) 57 | add_compile_definitions($<$>:NDEBUG>) 58 | 59 | add_compile_definitions($<$:WIN32>) 60 | 61 | target_compile_options(excelbind PUBLIC -WX -D_SCL_SECURE_NO_WARNINGS) 62 | 63 | add_subdirectory(${CMAKE_SOURCE_DIR}/thirdparty/xll12) 64 | 65 | 66 | target_link_options(excelbind PRIVATE /SAFESEH:NO /DEBUG:FULL) 67 | 68 | target_link_libraries(excelbind ${PYTHON_HOME}/libs/${PYTHON_VERSION}.lib) 69 | 70 | target_link_libraries(excelbind xll12) 71 | 72 | add_custom_command(TARGET excelbind POST_BUILD 73 | COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_SOURCE_DIR}/../Binaries/${CMAKE_GENERATOR_PLATFORM}/$/excelbind.xll 74 | COMMAND ${CMAKE_COMMAND} -E copy $/excelbind.pdb ${CMAKE_SOURCE_DIR}/../Binaries/${CMAKE_GENERATOR_PLATFORM}/$/excelbind.pdb 75 | ) -------------------------------------------------------------------------------- /src/excelbind/chrono.h: -------------------------------------------------------------------------------- 1 | /* 2 | pybind11/chrono.h: Transparent conversion between std::chrono and python's datetime 3 | 4 | Copyright (c) 2016 Trent Houliston and 5 | Wenzel Jakob 6 | 7 | All rights reserved. Use of this source code is governed by a 8 | BSD-style license that can be found in the LICENSE file. 9 | */ 10 | 11 | /* 12 | This is a slightly altered version of the original pybind11/chrono.h file. 13 | The original file uses std::mktime and similar methods, which in Visual Studio doesn't support dates before 1-1-1970. 14 | The below version uses the date.h date implementation by Howard Hinnant - which will (slightly modified) be part of C++20. 15 | */ 16 | 17 | #pragma once 18 | 19 | #include "pybind11/pybind11.h" 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | #include "date.h" 28 | 29 | using namespace date; 30 | 31 | PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) 32 | PYBIND11_NAMESPACE_BEGIN(detail) 33 | 34 | template 35 | class duration_caster { 36 | public: 37 | using rep = typename type::rep; 38 | using period = typename type::period; 39 | 40 | // signed 25 bits required by the standard. 41 | using days = std::chrono::duration>; 42 | 43 | bool load(handle src, bool) { 44 | using namespace std::chrono; 45 | 46 | // Lazy initialise the PyDateTime import 47 | if (!PyDateTimeAPI) { 48 | PyDateTime_IMPORT; 49 | } 50 | 51 | if (!src) { 52 | return false; 53 | } 54 | // If invoked with datetime.delta object 55 | if (PyDelta_Check(src.ptr())) { 56 | value = type(duration_cast>( 57 | days(PyDateTime_DELTA_GET_DAYS(src.ptr())) 58 | + seconds(PyDateTime_DELTA_GET_SECONDS(src.ptr())) 59 | + microseconds(PyDateTime_DELTA_GET_MICROSECONDS(src.ptr())))); 60 | return true; 61 | } 62 | // If invoked with a float we assume it is seconds and convert 63 | if (PyFloat_Check(src.ptr())) { 64 | value = type(duration_cast>( 65 | duration(PyFloat_AsDouble(src.ptr())))); 66 | return true; 67 | } 68 | return false; 69 | } 70 | 71 | // If this is a duration just return it back 72 | static const std::chrono::duration& 73 | get_duration(const std::chrono::duration& src) { 74 | return src; 75 | } 76 | 77 | // If this is a time_point get the time_since_epoch 78 | template 79 | static std::chrono::duration 80 | get_duration(const std::chrono::time_point>& src) { 81 | return src.time_since_epoch(); 82 | } 83 | 84 | static handle cast(const type& src, return_value_policy /* policy */, handle /* parent */) { 85 | using namespace std::chrono; 86 | 87 | // Use overloaded function to get our duration from our source 88 | // Works out if it is a duration or time_point and get the duration 89 | auto d = get_duration(src); 90 | 91 | // Lazy initialise the PyDateTime import 92 | if (!PyDateTimeAPI) { 93 | PyDateTime_IMPORT; 94 | } 95 | 96 | // Declare these special duration types so the conversions happen with the correct 97 | // primitive types (int) 98 | using dd_t = duration>; 99 | using ss_t = duration>; 100 | using us_t = duration; 101 | 102 | auto dd = duration_cast(d); 103 | auto subd = d - dd; 104 | auto ss = duration_cast(subd); 105 | auto us = duration_cast(subd - ss); 106 | return PyDelta_FromDSU(dd.count(), ss.count(), us.count()); 107 | } 108 | 109 | PYBIND11_TYPE_CASTER(type, const_name("datetime.timedelta")); 110 | }; 111 | 112 | inline std::tm* localtime_thread_safe(const std::time_t* time, std::tm* buf) { 113 | #if (defined(__STDC_LIB_EXT1__) && defined(__STDC_WANT_LIB_EXT1__)) || defined(_MSC_VER) 114 | if (localtime_s(buf, time)) 115 | return nullptr; 116 | return buf; 117 | #else 118 | static std::mutex mtx; 119 | std::lock_guard lock(mtx); 120 | std::tm* tm_ptr = std::localtime(time); 121 | if (tm_ptr != nullptr) { 122 | *buf = *tm_ptr; 123 | } 124 | return tm_ptr; 125 | #endif 126 | } 127 | 128 | // This is for casting times on the system clock into datetime.datetime instances 129 | template 130 | class type_caster> { 131 | public: 132 | using type = std::chrono::time_point; 133 | bool load(handle src, bool) { 134 | using namespace std::chrono; 135 | 136 | // Lazy initialise the PyDateTime import 137 | if (!PyDateTimeAPI) { 138 | PyDateTime_IMPORT; 139 | } 140 | 141 | if (!src) { 142 | return false; 143 | } 144 | 145 | if (PyDateTime_Check(src.ptr())) { 146 | year_month_day input_date = year_month_day(year(PyDateTime_GET_YEAR(src.ptr())), month(PyDateTime_GET_MONTH(src.ptr())), day(PyDateTime_GET_DAY(src.ptr()))); 147 | value = sys_days(input_date) 148 | + hours(PyDateTime_DATE_GET_HOUR(src.ptr())) 149 | + minutes(PyDateTime_DATE_GET_MINUTE(src.ptr())) 150 | + seconds(PyDateTime_DATE_GET_SECOND(src.ptr())) 151 | + microseconds(PyDateTime_DATE_GET_MICROSECOND(src.ptr())); 152 | } 153 | else if (PyDate_Check(src.ptr())) { 154 | year_month_day input_date = year_month_day(year(PyDateTime_GET_YEAR(src.ptr())), month(PyDateTime_GET_MONTH(src.ptr())), day(PyDateTime_GET_DAY(src.ptr()))); 155 | value = sys_days(input_date); 156 | } 157 | else if (PyTime_Check(src.ptr())) { 158 | year_month_day input_date = { January / 1 / 1970 }; 159 | value = sys_days(input_date) 160 | + hours(PyDateTime_DATE_GET_HOUR(src.ptr())) 161 | + minutes(PyDateTime_DATE_GET_MINUTE(src.ptr())) 162 | + seconds(PyDateTime_DATE_GET_SECOND(src.ptr())) 163 | + microseconds(PyDateTime_DATE_GET_MICROSECOND(src.ptr())); 164 | } 165 | else { 166 | return false; 167 | } 168 | 169 | return true; 170 | } 171 | 172 | static handle cast(const std::chrono::time_point& src, 173 | return_value_policy /* policy */, 174 | handle /* parent */) { 175 | using namespace std::chrono; 176 | 177 | // Lazy initialise the PyDateTime import 178 | if (!PyDateTimeAPI) { 179 | PyDateTime_IMPORT; 180 | } 181 | 182 | // Get out microseconds, and make sure they are positive, to avoid bug in eastern 183 | // hemisphere time zones (cfr. https://github.com/pybind/pybind11/issues/2417) 184 | using us_t = duration; 185 | auto us = duration_cast(src.time_since_epoch() % seconds(1)); 186 | if (us.count() < 0) { 187 | us += seconds(1); 188 | } 189 | 190 | auto input_date = year_month_day{ floor(src) }; 191 | auto time = make_time(src - sys_days(input_date)); 192 | return PyDateTime_FromDateAndTime( 193 | static_cast(input_date.year()), 194 | static_cast(input_date.month()), 195 | static_cast(input_date.day()), 196 | time.hours().count(), 197 | time.minutes().count(), 198 | static_cast(time.seconds().count()), 199 | us.count()); 200 | } 201 | PYBIND11_TYPE_CASTER(type, const_name("datetime.datetime")); 202 | }; 203 | 204 | // Other clocks that are not the system clock are not measured as datetime.datetime objects 205 | // since they are not measured on calendar time. So instead we just make them timedeltas 206 | // Or if they have passed us a time as a float we convert that 207 | template 208 | class type_caster> 209 | : public duration_caster> {}; 210 | 211 | template 212 | class type_caster> 213 | : public duration_caster> {}; 214 | 215 | PYBIND11_NAMESPACE_END(detail) 216 | PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) 217 | -------------------------------------------------------------------------------- /src/excelbind/configuration.cpp: -------------------------------------------------------------------------------- 1 | #pragma warning(push) 2 | #pragma warning(disable: 4996) 3 | #pragma warning(disable: 4244) 4 | 5 | #include 6 | #include "inih/INIReader.h" 7 | #include "configuration.h" 8 | 9 | 10 | INIReader load_config_file() 11 | { 12 | std::string user_home_path = std::getenv("USERPROFILE"); 13 | INIReader reader(user_home_path + "/excelbind.conf"); 14 | return reader; 15 | } 16 | 17 | std::string get_single_setting(const INIReader& reader, const std::string& settings_name, const std::string& default_setting) 18 | { 19 | std::string env_var_name = "EXCELBIND_" + settings_name; 20 | std::transform(env_var_name.begin(), env_var_name.end(), env_var_name.begin(), [](unsigned char c) { return std::toupper(c); }); 21 | char* env_var = std::getenv(env_var_name.c_str()); 22 | return env_var ? env_var : reader.Get("excelbind", settings_name, default_setting); 23 | } 24 | 25 | Configuration::Configuration() 26 | : ini_header("excelbind") 27 | { 28 | INIReader reader = load_config_file(); 29 | is_error_messages_enabled_ = reader.GetBoolean(ini_header, "EnableErrorMessages", false); 30 | virtual_env_ = get_single_setting(reader, "VirtualEnv", ""); 31 | module_dir_ = get_single_setting(reader, "ModuleDir", ""); 32 | module_name_ = get_single_setting(reader, "ModuleName", "excelbind_export"); 33 | function_prefix_ = get_single_setting(reader, "FunctionPrefix", ""); 34 | excel_category_ = get_single_setting(reader, "ExcelCategory", "Excelbind"); 35 | } 36 | 37 | Configuration& Configuration::get() 38 | { 39 | static Configuration instance; 40 | return instance; 41 | } 42 | 43 | #pragma warning(pop) 44 | -------------------------------------------------------------------------------- /src/excelbind/configuration.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | class Configuration 3 | { 4 | private: 5 | Configuration(); 6 | 7 | static Configuration& get(); 8 | 9 | public: 10 | 11 | static bool is_error_messages_enabled() { return get().is_error_messages_enabled_; } 12 | 13 | static const std::string& virtual_env() { return get().virtual_env_; } 14 | static bool is_virtual_env_set() { return !virtual_env().empty(); } 15 | 16 | static const std::string& module_dir() { return get().module_dir_; } 17 | static bool is_module_dir_set() { return !module_dir().empty(); } 18 | 19 | static const std::string& module_name() { return get().module_name_; } 20 | 21 | static bool is_function_prefix_set() { return !function_prefix().empty(); } 22 | static const std::string& function_prefix() { return get().function_prefix_; } 23 | 24 | static const std::string& excel_category() { return get().excel_category_; } 25 | 26 | 27 | private: 28 | const std::string ini_header; 29 | bool is_error_messages_enabled_; 30 | std::string virtual_env_; 31 | std::string module_dir_; 32 | std::string module_name_; 33 | std::string function_prefix_; 34 | std::string excel_category_; 35 | }; 36 | 37 | -------------------------------------------------------------------------------- /src/excelbind/execute_python.cpp: -------------------------------------------------------------------------------- 1 | #include "pybind11/embed.h" 2 | 3 | #include "xll12/xll/xll.h" 4 | 5 | #include "type_conversion.h" 6 | #include "configuration.h" 7 | /* 8 | This piece of code registers an Excel function called 'execute_python', that makes it possible to run python code directly from Excel. 9 | The approach take, is that the provided code is wrapped internally as a function. I.e. the Excel call 10 | =execute_python("return arg0 + arg1", A1, A2) 11 | will create the script: 12 | def func(arg0, arg1): 13 | return arg0 + arg1 14 | 15 | out = func(arg0_in, arg1_in) 16 | 17 | where the value of cells A1 and A2 are passed to the local variables arg0_in and arg1_in. 18 | The local variable out is returned as the result of the call to the execute_python function. 19 | */ 20 | 21 | namespace py = pybind11; 22 | using namespace pybind11::literals; 23 | 24 | // unique str used to avoid name collisions when building internal python code 25 | #define SOME_UNIQUE_STR "89C86679C2D042AD966268A41890F4F1" 26 | 27 | 28 | 29 | 30 | py::str convert_script_to_function(const XCHAR* script) 31 | { 32 | py::str script_py = cast_string(std::wstring(script)); 33 | py::list script_split = script_py.attr("splitlines")(); 34 | py::list script_wrapped; 35 | script_wrapped.append("def func_" SOME_UNIQUE_STR "(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9):"); 36 | for (auto& i : script_split) 37 | { 38 | script_wrapped.append(" " + i.cast()); 39 | } 40 | script_wrapped.append("out_" SOME_UNIQUE_STR " = func_" SOME_UNIQUE_STR 41 | "(arg0_" SOME_UNIQUE_STR 42 | ", arg1_" SOME_UNIQUE_STR 43 | ", arg2_" SOME_UNIQUE_STR 44 | ", arg3_" SOME_UNIQUE_STR 45 | ", arg4_" SOME_UNIQUE_STR 46 | ", arg5_" SOME_UNIQUE_STR 47 | ", arg6_" SOME_UNIQUE_STR 48 | ", arg7_" SOME_UNIQUE_STR 49 | ", arg8_" SOME_UNIQUE_STR 50 | ", arg9_" SOME_UNIQUE_STR ")"); 51 | auto os = py::module::import("os"); 52 | py::str all = os.attr("linesep"); 53 | return all.attr("join")(script_wrapped); 54 | } 55 | 56 | 57 | 58 | 59 | 60 | 61 | #pragma warning(push) 62 | #pragma warning(disable: 4100) 63 | xll::LPOPER executePython( 64 | const XCHAR* script, 65 | xll::OPER* arg0, 66 | xll::OPER* arg1, 67 | xll::OPER* arg2, 68 | xll::OPER* arg3, 69 | xll::OPER* arg4, 70 | xll::OPER* arg5, 71 | xll::OPER* arg6, 72 | xll::OPER* arg7, 73 | xll::OPER* arg8, 74 | xll::OPER* arg9 75 | ) 76 | { 77 | #pragma XLLEXPORT 78 | static xll::OPER res_xll; 79 | try { 80 | py::dict locals; 81 | locals[py::str("arg0_" SOME_UNIQUE_STR)] = cast_oper_to_py(*arg0); 82 | locals[py::str("arg1_" SOME_UNIQUE_STR)] = cast_oper_to_py(*arg1); 83 | locals[py::str("arg2_" SOME_UNIQUE_STR)] = cast_oper_to_py(*arg2); 84 | locals[py::str("arg3_" SOME_UNIQUE_STR)] = cast_oper_to_py(*arg3); 85 | locals[py::str("arg4_" SOME_UNIQUE_STR)] = cast_oper_to_py(*arg4); 86 | locals[py::str("arg5_" SOME_UNIQUE_STR)] = cast_oper_to_py(*arg5); 87 | locals[py::str("arg6_" SOME_UNIQUE_STR)] = cast_oper_to_py(*arg6); 88 | locals[py::str("arg7_" SOME_UNIQUE_STR)] = cast_oper_to_py(*arg7); 89 | locals[py::str("arg8_" SOME_UNIQUE_STR)] = cast_oper_to_py(*arg8); 90 | locals[py::str("arg9_" SOME_UNIQUE_STR)] = cast_oper_to_py(*arg9); 91 | 92 | py::str script_py = convert_script_to_function(script); 93 | py::exec(script_py, py::globals(), locals); 94 | py::object res_py = locals["out_" SOME_UNIQUE_STR]; 95 | cast_py_to_oper(res_py, res_xll); 96 | } 97 | catch (const std::exception& e) 98 | { 99 | if (Configuration::is_error_messages_enabled()) 100 | { 101 | XLL_ERROR(e.what()); 102 | } 103 | res_xll = xll::OPER(xlerr::Num); 104 | } 105 | return &res_xll; 106 | } 107 | #pragma warning(pop) 108 | 109 | 110 | xll::AddIn execute_python( 111 | xll::Function(XLL_LPOPER, L"?executePython", 112 | Configuration::is_function_prefix_set() 113 | ? (cast_string(Configuration::function_prefix()) + L".execute_python").c_str() : L"execute_python") 114 | .Arg(XLL_CSTRING, L"Script") 115 | .Arg(XLL_LPOPER, L"arg0") 116 | .Arg(XLL_LPOPER, L"arg1") 117 | .Arg(XLL_LPOPER, L"arg2") 118 | .Arg(XLL_LPOPER, L"arg3") 119 | .Arg(XLL_LPOPER, L"arg4") 120 | .Arg(XLL_LPOPER, L"arg5") 121 | .Arg(XLL_LPOPER, L"arg6") 122 | .Arg(XLL_LPOPER, L"arg7") 123 | .Arg(XLL_LPOPER, L"arg8") 124 | .Arg(XLL_LPOPER, L"arg9") 125 | .Category(cast_string(Configuration::excel_category()).c_str()) 126 | .FunctionHelp(L"Execute python code") 127 | ); 128 | -------------------------------------------------------------------------------- /src/excelbind/fct_exports.asm: -------------------------------------------------------------------------------- 1 | ; The code here contains the actual functions exported from the dll. 2 | ; They use the thunks_objects and thunks_methods tables to redirect the calls from Excel 3 | ; to the correct method on the PythonFunctionAdapter object wrapping the python function 4 | ; The win32 functions are quite simple, as the (internal Visual Studio/Microsoft calling 5 | ; convention for calling a non-virtual method on an object, is the same as stdcall + ptr 6 | ; to the object in ECX. I.e. all function arguments are on the stack. 7 | ; The x64 functions are a bit more involved, as the Microsoft x64 calling conventions 8 | ; already use RCX for the first function argument. So we need to shift all function arguments 9 | ; and then put the ptr to the object in RCX. 10 | ; Note that to simplify the code, we always shift all 10 arguments even if the function actually has less. 11 | ; See (https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=vs-2019) 12 | ; for details on x64 calling conventions. 13 | 14 | IFDEF RAX; small hack - RAX is only defined in x64 - but it doesn't work with IFNDEF! 15 | 16 | ELSE 17 | .model flat, c 18 | ENDIF 19 | 20 | .data 21 | EXTERN thunks_objects:PTR PTRTYPE 22 | EXTERN thunks_methods:PTR PTRTYPE 23 | 24 | IFDEF RAX; small hack - RAX is only defined in x64 25 | fct macro i 26 | PUBLIC f&i 27 | f&i PROC EXPORT 28 | sub RSP, 058h 29 | 30 | mov RAX, QWORD PTR[RSP + 0A8h] 31 | mov [RSP + 050h], RAX 32 | 33 | mov RAX, QWORD PTR[RSP + 0A0h] 34 | mov [RSP + 048h], RAX 35 | 36 | mov RAX, QWORD PTR[RSP + 098h] 37 | mov [RSP + 040h], RAX 38 | 39 | mov RAX, QWORD PTR[RSP + 090h] 40 | mov [RSP + 038h], RAX 41 | 42 | mov RAX, QWORD PTR[RSP + 088h] 43 | mov [RSP + 030h], RAX 44 | 45 | mov RAX, QWORD PTR[RSP + 080h] 46 | mov [RSP + 028h], RAX 47 | mov [RSP + 020h], R9 48 | mov R9, R8 49 | mov R8, RDX 50 | mov RDX, RCX 51 | mov RCX, QWORD PTR [thunks_objects + i * 8] 52 | mov RAX, thunks_methods + i * 8 53 | call RAX 54 | 55 | add RSP, 058h 56 | ret 57 | f&i ENDP 58 | endm 59 | ELSE 60 | fct macro i 61 | PUBLIC f&i 62 | f&i PROC EXPORT 63 | mov ECX, DWORD PTR [thunks_objects + i * 4] 64 | jmp thunks_methods + i * 4 65 | f&i ENDP 66 | endm 67 | ENDIF 68 | .code 69 | 70 | 71 | ; Roll out 10000 functions - note the number of functions here should match the space allocated to 72 | ; the thunks tables in 'function_registration.cpp' 73 | 74 | counter = 0 75 | REPEAT 10000 76 | fct %counter 77 | counter = counter + 1 78 | ENDM 79 | 80 | 81 | 82 | end -------------------------------------------------------------------------------- /src/excelbind/function_registration.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | This code handles dynamic registration of a python function. 3 | 4 | The approach is very much inspired by the setup in ExcelDNA. 5 | However, to support the more dynamic approach to registering which you would want from python, 6 | the idea is extended a bit, using a non-standard C++ feature. 7 | So, when a python function is registered, a pythonFunctionAdapter object is created, 8 | which holds a reference to the python function name and a non-virtual method used to invoke the function. 9 | For this to work from Excel, we need to store both a pointer to the object and the member function. 10 | The Visual Studio calling conventions for non-virtual member functions are similar to calling free functions, 11 | except that the pointer to the object is in register ecx/rcx. 12 | 13 | More concretely, when a python function is registered, an adapter object is created, and 14 | pointers to both the object and the member function are stored in thunks_objects and thunks_methods respectively. 15 | The free function exposed to Excel (the f functions created in fct_exports.asm) then moves the object adress to ecx/rcx and jumps to the member function. 16 | */ 17 | #include "pybind11/embed.h" 18 | #include "xll12/xll/xll.h" 19 | 20 | #include "type_conversion.h" 21 | #include "python_function_adapter.h" 22 | #include "configuration.h" 23 | 24 | namespace py = pybind11; 25 | 26 | // thunk tables - note the space allocated here should match the number of functions exported in 'fct_exports.asm' 27 | extern "C" 28 | { 29 | void* thunks_objects[10000]; 30 | void* thunks_methods[10000]; 31 | } 32 | 33 | 34 | std::vector cast_list(const py::list& in) 35 | { 36 | std::vector out; 37 | for (auto& i : in) 38 | { 39 | out.push_back(i.cast()); 40 | } 41 | return out; 42 | } 43 | 44 | void register_python_function( 45 | const py::str& function_name_py, const py::list& argument_names_py, const py::list& argument_types_py, 46 | const py::list& argument_docs_py, const py::str& return_type_py, const py::str & function_doc, const py::bool_ is_volatile 47 | ) 48 | { 49 | static int function_index = 0; 50 | 51 | const std::string function_name = function_name_py; 52 | if (argument_names_py.size() > 10) 53 | { 54 | std::string err_msg = "The python function " + function_name + " has too many arguments. A maximum of 10 is supported"; 55 | XLL_ERROR(err_msg.c_str()); 56 | return; 57 | } 58 | 59 | const std::wstring export_name = L"f" + std::to_wstring(function_index); 60 | const std::wstring xll_name 61 | = Configuration::is_function_prefix_set() 62 | ? cast_string(Configuration::function_prefix()) + L"." + cast_string(function_name) : cast_string(function_name); 63 | 64 | std::vector argument_types; 65 | for (auto& i : argument_types_py) 66 | { 67 | argument_types.push_back(get_bind_type(i.cast())); 68 | } 69 | 70 | std::vector argument_names = cast_list(argument_names_py); 71 | std::vector argument_docs = cast_list(argument_docs_py); 72 | BindTypes return_type = get_bind_type(return_type_py); 73 | 74 | // create function object and register it in thunks 75 | thunks_objects[function_index] = new PythonFunctionAdapter(function_name, argument_types, return_type); 76 | thunks_methods[function_index] = create_function_ptr(argument_types.size()); 77 | 78 | // Information Excel needs to register add-in. 79 | xll::Args functionBuilder = xll::Function(XLL_LPOPER, export_name.c_str(), xll_name.c_str()) 80 | .Category(cast_string(Configuration::excel_category()).c_str()) 81 | .FunctionHelp(function_doc.cast().c_str()); 82 | 83 | for (size_t i = 0; i < argument_names.size(); ++i) 84 | { 85 | functionBuilder.Arg(get_xll_type(argument_types[i]).c_str(), argument_names[i].c_str(), argument_docs[i].c_str()); 86 | } 87 | if (is_volatile) 88 | { 89 | functionBuilder.Volatile(); 90 | } 91 | xll::AddIn function = xll::AddIn(functionBuilder); 92 | ++function_index; 93 | } 94 | 95 | PYBIND11_EMBEDDED_MODULE(excelbind, m) { 96 | // `m` is a `py::module` which is used to bind functions and classes 97 | m.def("register", ®ister_python_function); 98 | } 99 | -------------------------------------------------------------------------------- /src/excelbind/python_function_adapter.cpp: -------------------------------------------------------------------------------- 1 | #define NOMINMAX 2 | #include 3 | #include "xll12/xll/xll.h" 4 | 5 | #include "script_manager.h" 6 | #include "python_function_adapter.h" 7 | #include "configuration.h" 8 | 9 | PythonFunctionAdapter::PythonFunctionAdapter(const std::string& python_function_name, std::vector argument_types, BindTypes return_type) 10 | { 11 | python_function_name_ = python_function_name; 12 | argument_types_ = argument_types; 13 | return_type_ = return_type; 14 | } 15 | 16 | xll::LPOPER PythonFunctionAdapter::fct0() { return fct({ }); } 17 | xll::LPOPER PythonFunctionAdapter::fct1(void* p0) { return fct({ p0 }); } 18 | xll::LPOPER PythonFunctionAdapter::fct2(void* p0, void* p1) { return fct({ p0, p1 }); } 19 | xll::LPOPER PythonFunctionAdapter::fct3(void* p0, void* p1, void* p2) { return fct({ p0, p1, p2 }); } 20 | xll::LPOPER PythonFunctionAdapter::fct4(void* p0, void* p1, void* p2, void* p3) { return fct({ p0, p1, p2, p3 }); } 21 | xll::LPOPER PythonFunctionAdapter::fct5(void* p0, void* p1, void* p2, void* p3, void* p4) { return fct({ p0, p1, p2, p3, p4 }); } 22 | xll::LPOPER PythonFunctionAdapter::fct6(void* p0, void* p1, void* p2, void* p3, void* p4, void* p5) { return fct({ p0, p1, p2, p3, p4, p5 }); } 23 | xll::LPOPER PythonFunctionAdapter::fct7(void* p0, void* p1, void* p2, void* p3, void* p4, void* p5, void* p6) { return fct({ p0, p1, p2, p3, p4, p5, p6 }); } 24 | xll::LPOPER PythonFunctionAdapter::fct8(void* p0, void* p1, void* p2, void* p3, void* p4, void* p5, void* p6, void* p7) { return fct({ p0, p1, p2, p3, p4, p5, p6, p7 }); } 25 | xll::LPOPER PythonFunctionAdapter::fct9(void* p0, void* p1, void* p2, void* p3, void* p4, void* p5, void* p6, void* p7, void* p8) { return fct({ p0, p1, p2, p3, p4, p5, p6, p7, p8 }); } 26 | xll::LPOPER PythonFunctionAdapter::fct10(void* p0, void* p1, void* p2, void* p3, void* p4, void* p5, void* p6, void* p7, void* p8, void* p9) { return fct({ p0, p1, p2, p3, p4, p5, p6, p7, p8, p9 }); } 27 | 28 | xll::LPOPER PythonFunctionAdapter::fct(const std::initializer_list& args) 29 | { 30 | static xll::OPER res_xll; 31 | try 32 | { 33 | py::tuple args_py(args.size()); 34 | auto j = args.begin(); 35 | for (size_t i = 0; i < args.size(); ++i, ++j) 36 | { 37 | args_py[i] = cast_xll_to_py(*j, argument_types_[i]); 38 | } 39 | 40 | const py::module& scripts = ScriptManager::get_scripts(); 41 | 42 | py::object res_py = scripts.attr(python_function_name_.c_str())(*args_py); 43 | 44 | cast_py_to_xll(res_py, res_xll, return_type_); 45 | } 46 | catch (const std::exception& e) 47 | { 48 | if (Configuration::is_error_messages_enabled()) 49 | { 50 | XLL_ERROR(e.what()); 51 | } 52 | res_xll = xll::OPER(xlerr::Num); 53 | } 54 | return &res_xll; 55 | } 56 | 57 | 58 | #define FCT_POINTER(num_args, ...) \ 59 | xll::LPOPER(__thiscall PythonFunctionAdapter:: * pFunc)(##__VA_ARGS__) = &PythonFunctionAdapter::fct##num_args; \ 60 | return (void*&)pFunc; 61 | 62 | void* create_function_ptr(size_t num_arguments) 63 | { 64 | switch (num_arguments) 65 | { 66 | case 0: { FCT_POINTER(0) } 67 | case 1: { FCT_POINTER(1, void*) } 68 | case 2: { FCT_POINTER(2, void*, void*) } 69 | case 3: { FCT_POINTER(3, void*, void*, void*) } 70 | case 4: { FCT_POINTER(4, void*, void*, void*, void*) } 71 | case 5: { FCT_POINTER(5, void*, void*, void*, void*, void*) } 72 | case 6: { FCT_POINTER(6, void*, void*, void*, void*, void*, void*) } 73 | case 7: { FCT_POINTER(7, void*, void*, void*, void*, void*, void*, void*) } 74 | case 8: { FCT_POINTER(8, void*, void*, void*, void*, void*, void*, void*, void*) } 75 | case 9: { FCT_POINTER(9, void*, void*, void*, void*, void*, void*, void*, void*, void*) } 76 | case 10: { FCT_POINTER(10, void*, void*, void*, void*, void*, void*, void*, void*, void*, void*) } 77 | default: 78 | throw std::runtime_error("A maximum of 10 arguments is supported for python functions."); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/excelbind/python_function_adapter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "xll12/xll/xll.h" 3 | #include "type_conversion.h" 4 | 5 | class PythonFunctionAdapter 6 | { 7 | public: 8 | PythonFunctionAdapter(const std::string& python_function_name, std::vector argument_types, BindTypes return_type); 9 | 10 | xll::LPOPER fct0(); 11 | xll::LPOPER fct1(void* p0); 12 | xll::LPOPER fct2(void* p0, void* p1); 13 | xll::LPOPER fct3(void* p0, void* p1, void* p2); 14 | xll::LPOPER fct4(void* p0, void* p1, void* p2, void* p3); 15 | xll::LPOPER fct5(void* p0, void* p1, void* p2, void* p3, void* p4); 16 | xll::LPOPER fct6(void* p0, void* p1, void* p2, void* p3, void* p4, void* p5); 17 | xll::LPOPER fct7(void* p0, void* p1, void* p2, void* p3, void* p4, void* p5, void* p6); 18 | xll::LPOPER fct8(void* p0, void* p1, void* p2, void* p3, void* p4, void* p5, void* p6, void* p7); 19 | xll::LPOPER fct9(void* p0, void* p1, void* p2, void* p3, void* p4, void* p5, void* p6, void* p7, void* p8); 20 | xll::LPOPER fct10(void* p0, void* p1, void* p2, void* p3, void* p4, void* p5, void* p6, void* p7, void* p8, void* p9); 21 | 22 | private: 23 | xll::LPOPER fct(const std::initializer_list& args); 24 | 25 | std::string python_function_name_; 26 | std::vector argument_types_; 27 | BindTypes return_type_; 28 | }; 29 | 30 | void* create_function_ptr(size_t num_arguments); 31 | -------------------------------------------------------------------------------- /src/excelbind/script_manager.cpp: -------------------------------------------------------------------------------- 1 | // to support vs2015 - we import filesytem as experimental 2 | #define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING 3 | 4 | #include 5 | #include 6 | #include "xll12/xll/xll.h" 7 | #include "configuration.h" 8 | #include "type_conversion.h" 9 | #include "script_manager.h" 10 | 11 | 12 | ScriptManager& ScriptManager::get() 13 | { 14 | static ScriptManager instance; 15 | return instance; 16 | } 17 | 18 | int ScriptManager::finalize_python() 19 | { 20 | get().scripts = py::module(); 21 | py::finalize_interpreter(); 22 | return 1; 23 | } 24 | 25 | void add_python_helper_functions_to_excelbind_module() 26 | { 27 | py::dict excelbind_scope = py::module::import("excelbind").attr("__dict__").cast(); 28 | excelbind_scope[py::str("__builtins__")] = py::globals()[py::str("__builtins__")]; 29 | 30 | py::exec(R"( 31 | import os as _os 32 | import re as _re 33 | def _parse_doc_string(doc_string): 34 | param_regex = _re.compile( 35 | r'^:param (?P\w+): (?P.*)$' 36 | ) 37 | args_docs = {} 38 | function_doc_lines = [] 39 | for item in doc_string.splitlines(): 40 | g = param_regex.search(item.strip()) 41 | if g: 42 | args_docs[g.group('param')] = g.group('doc') 43 | elif ':return:' not in item: 44 | function_doc_lines.append(item) 45 | 46 | while function_doc_lines and function_doc_lines[-1].strip() == '': 47 | function_doc_lines = function_doc_lines[:-1] 48 | 49 | return _os.linesep.join(function_doc_lines), args_docs 50 | 51 | 52 | def _get_type_name(t): 53 | if hasattr(t, '__name__'): 54 | return t.__name__ 55 | else: 56 | return t._name 57 | 58 | 59 | def _wrapper(f, is_volatile=False): 60 | arguments = {arg_name: _get_type_name(f.__annotations__[arg_name]) if arg_name in f.__annotations__ else 'Any' for arg_name in f.__code__.co_varnames} 61 | return_type = _get_type_name(f.__annotations__['return']) if 'return' in f.__annotations__ else 'Any' 62 | raw_doc = f.__doc__ or ' ' 63 | function_doc, arg_docs_dict = _parse_doc_string(raw_doc) 64 | arg_docs = [arg_docs_dict.get(item, ' ') for item in arguments.keys()] 65 | register(f.__name__, list(arguments.keys()), list(arguments.values()), arg_docs, return_type, function_doc, is_volatile) 66 | return f 67 | 68 | 69 | def function(*args, **kwargs): 70 | if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): 71 | return _wrapper(args[0]) 72 | else: 73 | return lambda f: _wrapper(f, *args, **kwargs) 74 | 75 | )", excelbind_scope); 76 | } 77 | 78 | void set_virtual_env_python_interpreter() 79 | { 80 | const std::string virtual_env = Configuration::virtual_env(); 81 | if (!std::experimental::filesystem::exists(virtual_env + "/Scripts/python.exe")) 82 | { 83 | std::string error_msg = "Failed to find python interpreter " + virtual_env + "/Scripts/python.exe"; 84 | throw std::runtime_error(error_msg); 85 | } 86 | 87 | PyStatus status; 88 | PyConfig config; 89 | PyConfig_InitPythonConfig(&config); 90 | 91 | std::wstring virtual_env_w = cast_string(virtual_env); 92 | static std::wstring python_home = virtual_env_w + L"/Scripts"; 93 | status = PyConfig_SetString(&config, &config.home, const_cast(python_home.c_str())); 94 | 95 | static std::wstring program_name = virtual_env_w + L"/Scripts/python.exe"; 96 | status = PyConfig_SetString(&config, &config.program_name, const_cast(program_name.c_str())); 97 | 98 | static std::wstring python_path = virtual_env_w + L"/Lib;" + virtual_env_w + L"/Lib/site-packages"; 99 | status = PyConfig_SetString(&config, &config.pythonpath_env, const_cast(python_path.c_str())); 100 | } 101 | 102 | void init_virtual_env_paths() 103 | { 104 | py::module::import("sys").attr("path").cast().append(Configuration::virtual_env() + "/Scripts"); 105 | py::module m = py::module::import("activate_this"); 106 | } 107 | 108 | void add_module_dir_to_python_path() 109 | { 110 | py::module::import("sys").attr("path").cast().append(Configuration::module_dir()); 111 | } 112 | 113 | int ScriptManager::init_python() 114 | { 115 | if (Configuration::is_virtual_env_set()) 116 | { 117 | set_virtual_env_python_interpreter(); 118 | } 119 | 120 | py::initialize_interpreter(); 121 | 122 | if (Configuration::is_virtual_env_set()) 123 | { 124 | init_virtual_env_paths(); 125 | } 126 | if (Configuration::is_module_dir_set()) 127 | { 128 | add_module_dir_to_python_path(); 129 | } 130 | add_python_helper_functions_to_excelbind_module(); 131 | get().scripts = py::module::import(Configuration::module_name().c_str()); 132 | 133 | return 1; 134 | } 135 | 136 | const py::module& ScriptManager::get_scripts() 137 | { 138 | return get().scripts; 139 | } 140 | 141 | xll::Auto init = xll::Auto(&ScriptManager::init_python); 142 | xll::Auto finalize = xll::Auto(&ScriptManager::finalize_python); 143 | -------------------------------------------------------------------------------- /src/excelbind/script_manager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "pybind11/embed.h" 3 | 4 | 5 | namespace py = pybind11; 6 | 7 | 8 | class ScriptManager 9 | { 10 | private: 11 | ScriptManager() {} 12 | 13 | py::module scripts; 14 | 15 | static ScriptManager& get(); 16 | 17 | public: 18 | static const py::module& get_scripts(); 19 | static int init_python(); 20 | static int finalize_python(); 21 | 22 | }; 23 | 24 | -------------------------------------------------------------------------------- /src/excelbind/type_conversion.cpp: -------------------------------------------------------------------------------- 1 | #define _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING 2 | 3 | #include 4 | 5 | #include "pybind11/numpy.h" 6 | 7 | #include "date.h" 8 | #include "chrono.h" 9 | #include "type_conversion.h" 10 | 11 | using namespace date; 12 | 13 | typedef std::chrono::duration> excel_datetime; 14 | auto excel_base_time_point = 1899_y / December / 30; 15 | 16 | std::string cast_string(const std::wstring& in) 17 | { 18 | std::wstring_convert> convert; 19 | return convert.to_bytes(in); 20 | } 21 | 22 | std::wstring cast_string(const std::string& in) 23 | { 24 | std::wstring_convert> convert; 25 | return convert.from_bytes(in); 26 | } 27 | 28 | std::string cast_string(const xll::OPER& oper) 29 | { 30 | return cast_string(std::wstring(oper.val.str + 1, oper.val.str[0])); 31 | } 32 | 33 | bool has_dict_shape(const xll::OPER& in) 34 | { 35 | return in.isMulti() && in.rows() > 1 && in.columns() > 1 && (in.rows() == 2 || in.columns() == 2); 36 | } 37 | 38 | bool has_list_shape(const xll::OPER& in) 39 | { 40 | return in.isMulti() && (in.rows() == 1 || in.columns() == 1); 41 | } 42 | 43 | 44 | py::dict cast_oper_to_dict(const xll::OPER& in) 45 | { 46 | if (in.columns() == 2) 47 | { 48 | py::dict out; 49 | for (int i = 0; i < in.rows(); ++i) 50 | { 51 | py::object key = cast_oper_to_py(in(i, 0)); 52 | py::object val = cast_oper_to_py(in(i, 1)); 53 | out[key] = val; 54 | } 55 | return out; 56 | } 57 | if (in.rows() == 2) 58 | { 59 | py::dict out; 60 | for (int i = 0; i < in.columns(); ++i) 61 | { 62 | py::object key = cast_oper_to_py(in(0, i)); 63 | py::object val = cast_oper_to_py(in(1, i)); 64 | out[key] = val; 65 | } 66 | return out; 67 | } 68 | return py::none(); 69 | } 70 | 71 | void cast_dict_to_oper(const py::dict& in, xll::OPER& out) 72 | { 73 | out = xll::OPER(static_cast(in.size()), 2); 74 | int i = 0; 75 | for (auto item : in) 76 | { 77 | cast_py_to_oper(item.first, out(i, 0)); 78 | cast_py_to_oper(item.second, out(i++, 1)); 79 | } 80 | } 81 | 82 | py::list cast_oper_to_list(const xll::OPER& in) 83 | { 84 | py::list out; 85 | for (auto item : in) 86 | { 87 | out.append(cast_oper_to_py(item)); 88 | } 89 | return out; 90 | } 91 | 92 | void cast_list_to_oper(const py::list& in, xll::OPER& out) 93 | { 94 | out = xll::OPER(static_cast(in.size()), 1); 95 | auto oper_item = out.begin(); 96 | for (auto py_item : in) 97 | { 98 | cast_py_to_oper(py_item, *oper_item++); 99 | } 100 | } 101 | 102 | py::object cast_oper_to_dataframe(const xll::OPER& in) 103 | { 104 | bool hasIndices = in(0, 0).isMissing() || in(0, 0).isNil(); 105 | py::object indices = py::none(); 106 | if (hasIndices) 107 | { 108 | py::list indices_list; 109 | for (int i = 1; i < in.rows(); ++i) 110 | { 111 | indices_list.append(cast_oper_to_py(in(i, 0))); 112 | } 113 | indices = indices_list; 114 | } 115 | py::list columns; 116 | for (int i = hasIndices ? 1 : 0; i < in.columns(); ++i) 117 | { 118 | columns.append(cast_oper_to_py(in(0, i))); 119 | } 120 | py::list data; 121 | for (int i = 1; i(index.size() + 1), static_cast(columns.size() + 1)); 139 | for (size_t i = 0; i < index.size(); ++i) 140 | { 141 | cast_py_to_oper(index[i], out(static_cast(i + 1), 0)); 142 | } 143 | for (size_t i = 0; i < columns.size(); ++i) 144 | { 145 | cast_py_to_oper(columns[i], out(0, static_cast(i + 1))); 146 | } 147 | for (size_t i = 0; i < index.size(); ++i) 148 | { 149 | for (size_t j = 0; j < columns.size(); ++j) 150 | { 151 | cast_py_to_oper(in.attr("iat")[py::make_tuple(i, j)], out(static_cast(i + 1), static_cast(j + 1))); 152 | } 153 | } 154 | out(0, 0) = L""; 155 | } 156 | 157 | py::object cast_oper_to_py(const xll::OPER& in) 158 | { 159 | if (in.isBool()) 160 | { 161 | return py::bool_(in == TRUE); 162 | } 163 | else if (in.isInt()) 164 | { 165 | return py::int_(static_cast(in)); 166 | } 167 | else if (in.isNum()) 168 | { 169 | return py::float_(static_cast(in)); 170 | } 171 | else if (in.isStr()) 172 | { 173 | return py::str(cast_string(in)); 174 | } 175 | else if (has_dict_shape(in)) 176 | { 177 | return cast_oper_to_dict(in); 178 | } 179 | else if (has_list_shape(in)) 180 | { 181 | return cast_oper_to_list(in); 182 | } 183 | 184 | return py::none(); 185 | } 186 | 187 | void cast_py_to_oper(const py::handle& in, xll::OPER& out) 188 | { 189 | if (py::isinstance(in)) 190 | { 191 | out = in.cast().c_str(); 192 | } 193 | else if (py::isinstance(in)) 194 | { 195 | out = in.cast(); 196 | } 197 | else if (py::isinstance(in)) 198 | { 199 | cast_dict_to_oper(in.cast(), out); 200 | } 201 | else if (py::isinstance(in)) 202 | { 203 | cast_list_to_oper(in.cast(), out); 204 | } 205 | else 206 | { 207 | out = in.cast(); 208 | } 209 | } 210 | 211 | 212 | py::object cast_xll_to_py(void* p, BindTypes type) 213 | { 214 | switch (type) 215 | { 216 | case BindTypes::DOUBLE: 217 | { 218 | return py::float_(*(double*)(p)); 219 | } 220 | case BindTypes::STRING: 221 | { 222 | return py::str(cast_string(std::wstring((XCHAR*)(p)))); 223 | } 224 | case BindTypes::ARRAY: 225 | { 226 | FP12* fp = (FP12*)(p); 227 | auto data = py::buffer_info( 228 | fp->array, sizeof(double), py::format_descriptor::format(), 2, 229 | { fp->rows, fp->columns }, { sizeof(double) * fp->columns, sizeof(double) } 230 | ); 231 | return static_cast(py::array_t(data)); 232 | } 233 | case BindTypes::BOOLEAN: 234 | { 235 | return py::bool_(*(bool*)(p)); 236 | } 237 | case BindTypes::OPER: 238 | { 239 | return cast_oper_to_py(*(xll::OPER*)(p)); 240 | } 241 | case BindTypes::DICT: 242 | { 243 | return cast_oper_to_dict(*(xll::OPER*)(p)); 244 | } 245 | case BindTypes::LIST: 246 | { 247 | return cast_oper_to_list(*(xll::OPER*)(p)); 248 | } 249 | case BindTypes::DATETIME: 250 | { 251 | double excel_date = (*(double*)(p)); 252 | excel_datetime ndays = excel_datetime(excel_date); 253 | return py::cast(std::chrono::system_clock::time_point(sys_days(excel_base_time_point)) + round(ndays)); 254 | } 255 | case BindTypes::PD_SERIES: 256 | { 257 | const auto& oper = *(xll::OPER*)(p); 258 | py::object data = has_list_shape(oper) ? static_cast(cast_oper_to_list(oper)) : static_cast(cast_oper_to_dict(oper)); 259 | py::module pandas = py::module::import("pandas"); 260 | return pandas.attr("Series")(data); 261 | } 262 | case BindTypes::PD_DATAFRAME: 263 | { 264 | return cast_oper_to_dataframe(*(xll::OPER*)(p)); 265 | } 266 | default: 267 | return py::object(); 268 | } 269 | } 270 | 271 | void cast_py_to_xll(const py::object& in, xll::OPER& out, BindTypes type) 272 | { 273 | switch (type) 274 | { 275 | case BindTypes::DOUBLE: 276 | out = in.cast(); 277 | break; 278 | case BindTypes::STRING: 279 | out = in.cast().c_str(); 280 | break; 281 | case BindTypes::ARRAY: 282 | { 283 | py::buffer_info buffer_info = static_cast>(in).request(); 284 | out = xll::OPER(static_cast(buffer_info.shape[0]), static_cast(buffer_info.shape[1])); 285 | 286 | double* src = static_cast(buffer_info.ptr); 287 | auto i = out.begin(); 288 | while (i != out.end()) 289 | { 290 | *i++ = *src++; 291 | } 292 | break; 293 | } 294 | case BindTypes::BOOLEAN: 295 | out = in.cast(); 296 | break; 297 | case BindTypes::OPER: 298 | cast_py_to_oper(in, out); 299 | break; 300 | case BindTypes::DICT: 301 | cast_dict_to_oper(in.cast(), out); 302 | break; 303 | case BindTypes::LIST: 304 | cast_list_to_oper(in.cast(), out); 305 | break; 306 | case BindTypes::DATETIME: 307 | { 308 | std::chrono::system_clock::time_point time_point = in.cast(); 309 | auto duration = time_point - sys_days(excel_base_time_point); 310 | excel_datetime ndays = std::chrono::duration_cast(duration); 311 | out = ndays.count(); 312 | break; 313 | } 314 | case BindTypes::PD_SERIES: 315 | { 316 | cast_dict_to_oper((in.attr("to_dict")()).cast(), out); 317 | break; 318 | } 319 | case BindTypes::PD_DATAFRAME: 320 | { 321 | cast_dataframe_to_oper(in, out); 322 | break; 323 | } 324 | default: 325 | break; 326 | } 327 | } 328 | 329 | BindTypes get_bind_type(const std::string& py_type_name) 330 | { 331 | static const std::map typeConversionMap = 332 | { 333 | { "float", BindTypes::DOUBLE }, 334 | { "str", BindTypes::STRING }, 335 | { "ndarray", BindTypes::ARRAY }, 336 | { "np.ndarray", BindTypes::ARRAY }, 337 | { "numpy.ndarray", BindTypes::ARRAY }, 338 | { "bool", BindTypes::BOOLEAN }, 339 | { "Any", BindTypes::OPER }, 340 | { "Dict", BindTypes::DICT }, 341 | { "dict", BindTypes::DICT }, 342 | { "List", BindTypes::LIST }, 343 | { "list", BindTypes::LIST }, 344 | { "datetime", BindTypes::DATETIME }, 345 | { "datetime.datetime", BindTypes::DATETIME }, 346 | { "pd.Series", BindTypes::PD_SERIES }, 347 | { "pandas.Series", BindTypes::PD_SERIES }, 348 | { "Series", BindTypes::PD_SERIES }, 349 | { "pd.DataFrame", BindTypes::PD_DATAFRAME}, 350 | { "pandas.DataFrame", BindTypes::PD_DATAFRAME }, 351 | { "DataFrame", BindTypes::PD_DATAFRAME } 352 | }; 353 | 354 | auto i = typeConversionMap.find(py_type_name); 355 | if (i == typeConversionMap.end()) 356 | { 357 | return BindTypes::OPER; 358 | } 359 | return i->second; 360 | } 361 | 362 | std::wstring get_xll_type(BindTypes type) 363 | { 364 | static const std::map conversionMap = 365 | { 366 | { BindTypes::DOUBLE, XLL_DOUBLE_ }, 367 | { BindTypes::STRING, XLL_CSTRING }, 368 | { BindTypes::ARRAY, XLL_FP }, 369 | { BindTypes::BOOLEAN, XLL_BOOL_ }, 370 | { BindTypes::OPER, XLL_LPOPER }, 371 | { BindTypes::DICT, XLL_LPOPER }, 372 | { BindTypes::LIST, XLL_LPOPER }, 373 | { BindTypes::DATETIME, XLL_DOUBLE_ }, 374 | { BindTypes::PD_SERIES, XLL_LPOPER }, 375 | { BindTypes::PD_DATAFRAME, XLL_LPOPER } 376 | }; 377 | return conversionMap.find(type)->second; 378 | } 379 | -------------------------------------------------------------------------------- /src/excelbind/type_conversion.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "pybind11/embed.h" 5 | #include "xll12/xll/xll.h" 6 | 7 | 8 | namespace py = pybind11; 9 | 10 | 11 | enum class BindTypes { DOUBLE, STRING, ARRAY, BOOLEAN, OPER, DICT, LIST, DATETIME, PD_SERIES, PD_DATAFRAME }; 12 | 13 | 14 | std::string cast_string(const std::wstring& in); 15 | 16 | std::wstring cast_string(const std::string& in); 17 | 18 | BindTypes get_bind_type(const std::string& py_type_name); 19 | 20 | std::wstring get_xll_type(BindTypes type); 21 | 22 | py::object cast_oper_to_py(const xll::OPER& in); 23 | 24 | void cast_py_to_oper(const py::handle& in, xll::OPER& out); 25 | 26 | py::object cast_xll_to_py(void* p, BindTypes type); 27 | 28 | void cast_py_to_xll(const py::object& in, xll::OPER& out, BindTypes type); 29 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuneLjungmann/excelbind/bde6dbfce32f1b68e8b2179c82549f34ea2eb5b4/test/__init__.py -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import pytest 3 | import struct 4 | 5 | 6 | def interpreter_platform(): 7 | return 'x64' if struct.calcsize('P') == 8 else 'Win32' 8 | 9 | 10 | @pytest.fixture(scope='module') 11 | def xll_addin_path(): 12 | return str(pathlib.Path(__file__).parent / '..' / 'Binaries' / interpreter_platform() / 'Release' / 'excelbind.xll') 13 | -------------------------------------------------------------------------------- /test/test_execute_python.py: -------------------------------------------------------------------------------- 1 | from test.utilities.env_vars import set_env_vars 2 | from test.utilities.excel import Excel 3 | 4 | 5 | def test_simple_script_for_addition(xll_addin_path): 6 | with set_env_vars('basic_functions'): 7 | with Excel() as excel: 8 | excel.register_xll(xll_addin_path) 9 | 10 | ( 11 | excel.new_workbook() 12 | .range('A1').set(3.0) 13 | .range('A2').set(4.0) 14 | .range('B1').set_formula('=excelbind.execute_python("return arg0 + arg1", A1, A2)') 15 | .calculate() 16 | ) 17 | 18 | assert excel.range('B1').value == 7.0 19 | print("done testing") 20 | 21 | 22 | def test_combination_str_n_float(xll_addin_path): 23 | with set_env_vars('basic_functions'): 24 | with Excel() as excel: 25 | excel.register_xll(xll_addin_path) 26 | 27 | ( 28 | excel.new_workbook() 29 | .range('A1').set("Hello times ") 30 | .range('A2').set(3.0) 31 | .range('B1').set_formula('=excelbind.execute_python("return arg0 + str(arg1)", A1, A2)') 32 | .calculate() 33 | ) 34 | 35 | assert excel.range('B1').value == 'Hello times 3.0' 36 | print("done testing") 37 | -------------------------------------------------------------------------------- /test/test_type_handling.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import time 3 | from test.utilities.env_vars import set_env_vars 4 | from test.utilities.excel import Excel 5 | 6 | 7 | def test_simple_math_function_with_floats(xll_addin_path): 8 | with set_env_vars('basic_functions'): 9 | with Excel() as excel: 10 | excel.register_xll(xll_addin_path) 11 | 12 | ( 13 | excel.new_workbook() 14 | .range('A1').set(3.0) 15 | .range('A2').set(4.0) 16 | .range('B1').set_formula('=excelbind.add(A1, A2)') 17 | .range('B2').set_formula('=excelbind.mult(A1, A2)') 18 | .calculate() 19 | ) 20 | 21 | assert excel.range('B1').value == 7.0 22 | assert excel.range('B2').value == 12.0 23 | 24 | 25 | def test_simple_string_concatenation(xll_addin_path): 26 | with set_env_vars('basic_functions'): 27 | with Excel() as excel: 28 | excel.register_xll(xll_addin_path) 29 | 30 | ( 31 | excel.new_workbook() 32 | .range('A1').set('Hello ') 33 | .range('A2').set('World!') 34 | .range('B1').set_formula('=excelbind.concat(A1, A2)') 35 | .calculate() 36 | ) 37 | 38 | assert excel.range('B1').value == 'Hello World!' 39 | 40 | 41 | def test_matrix_operations_with_np_ndarray(xll_addin_path): 42 | with set_env_vars('basic_functions'): 43 | with Excel() as excel: 44 | excel.register_xll(xll_addin_path) 45 | 46 | ( 47 | excel.new_workbook() 48 | .range('A1').set(1.0) 49 | .range('B1').set(2.0) 50 | .range('A2').set(1.0) 51 | .range('B2').set(4.0) 52 | .range('A3').set_formula('=excelbind.det(A1:B2)') 53 | .range('A4:B5').set_formula('=excelbind.inv(A1:B2)') 54 | .calculate() 55 | ) 56 | 57 | assert excel.range('A3').value == 2.0 58 | 59 | assert excel.range('A4').value == 2.0 60 | assert excel.range('B4').value == -1.0 61 | assert excel.range('A5').value == -0.5 62 | assert excel.range('B5').value == 0.5 63 | 64 | 65 | def test_add_without_type_info(xll_addin_path): 66 | with set_env_vars('basic_functions'): 67 | with Excel() as excel: 68 | excel.register_xll(xll_addin_path) 69 | 70 | ( 71 | excel.new_workbook() 72 | .range('A1').set(3.0) 73 | .range('A2').set(4.0) 74 | .range('B1').set_formula('=excelbind.add_without_type_info(A1, A2)') 75 | .range('A3').set('Hello ') 76 | .range('A4').set('world!') 77 | .range('B2').set_formula('=excelbind.add_without_type_info(A3, A4)') 78 | .calculate() 79 | ) 80 | 81 | assert excel.range('B1').value == 7.0 82 | assert excel.range('B2').value == 'Hello world!' 83 | 84 | 85 | def test_list_output(xll_addin_path): 86 | with set_env_vars('basic_functions'): 87 | with Excel() as excel: 88 | excel.register_xll(xll_addin_path) 89 | 90 | ( 91 | excel.new_workbook() 92 | .range('A1').set(3.0) 93 | .range('A2').set(4.0) 94 | .range('A3').set(5.0) 95 | .range('D1:D3').set_formula('=excelbind.listify(A1, A2, A3)') 96 | .calculate() 97 | ) 98 | 99 | assert excel.range('D1').value == 3.0 100 | assert excel.range('D2').value == 4.0 101 | assert excel.range('D3').value == 5.0 102 | 103 | 104 | def test_dict_type(xll_addin_path): 105 | with set_env_vars('basic_functions'): 106 | with Excel() as excel: 107 | excel.register_xll(xll_addin_path) 108 | 109 | ( 110 | excel.new_workbook() 111 | .range('A1').set('x') 112 | .range('A2').set('y') 113 | .range('A3').set('z') 114 | .range('B1').set(5.0) 115 | .range('B2').set(6.0) 116 | .range('B3').set(7.0) 117 | .range('D1:E2').set_formula('=excelbind.filter_dict(A1:B3, A2)') 118 | .calculate() 119 | ) 120 | 121 | expected_dict = {'x': 5.0, 'z': 7.0} 122 | 123 | res_keys = sorted([excel.range('D1').value, excel.range('D2').value]) 124 | assert res_keys == sorted(expected_dict.keys()) 125 | assert excel.range('E1').value == expected_dict[excel.range('D1').value] 126 | assert excel.range('E2').value == expected_dict[excel.range('D2').value] 127 | 128 | 129 | def test_list_in_various_directions(xll_addin_path): 130 | with set_env_vars('basic_functions'): 131 | with Excel() as excel: 132 | excel.register_xll(xll_addin_path) 133 | 134 | ( 135 | excel.new_workbook() 136 | .range('A1').set(1.0) 137 | .range('A2').set(2.0) 138 | .range('A3').set(3.0) 139 | .range('B1').set(4.0) 140 | .range('C1').set(5.0) 141 | .range('D1').set_formula('=excelbind.dot(A1:A3, A1:C1)') 142 | .calculate() 143 | ) 144 | 145 | assert excel.range('D1').value == 24.0 146 | 147 | 148 | def test_no_arg(xll_addin_path): 149 | with set_env_vars('basic_functions'): 150 | with Excel() as excel: 151 | excel.register_xll(xll_addin_path) 152 | 153 | ( 154 | excel.new_workbook() 155 | .range('A1').set_formula('=excelbind.no_arg()') 156 | .calculate() 157 | ) 158 | 159 | assert excel.range('A1').value == 'Hello world!' 160 | 161 | 162 | def test_date_to_python(xll_addin_path): 163 | with set_env_vars('basic_functions'): 164 | with Excel() as excel: 165 | excel.register_xll(xll_addin_path) 166 | 167 | ( 168 | excel.new_workbook() 169 | .range('A1').set(29261) # 1980-02-10 170 | .range('A2').set(33090) # 1990-08-05 171 | .range('A3').set(25204) # 1969-01-01 172 | .range('A4').set(25569) # 1970-01-01 173 | .range('A5').set(36193.9549882755000000) # 1999-02-02 22:55:11.987 174 | .range('B1').set_formula('=excelbind.date_as_string(A1)') 175 | .range('B2').set_formula('=excelbind.date_as_string(A2)') 176 | .range('B3').set_formula('=excelbind.date_as_string(A3)') 177 | .range('B4').set_formula('=excelbind.date_as_string(A4)') 178 | .range('B5').set_formula('=excelbind.date_as_string(A5)') 179 | .calculate() 180 | ) 181 | 182 | assert excel.range('B1').value == '1980-02-10 00:00:00' 183 | assert excel.range('B2').value == '1990-08-05 00:00:00' 184 | assert excel.range('B3').value == '1969-01-01 00:00:00' 185 | assert excel.range('B4').value == '1970-01-01 00:00:00' 186 | assert excel.range('B5').value[:-3] == '1999-02-02 22:55:10.987' 187 | 188 | 189 | def test_date_conversion(xll_addin_path): 190 | with set_env_vars('basic_functions'): 191 | with Excel() as excel: 192 | excel.register_xll(xll_addin_path) 193 | 194 | ( 195 | excel.new_workbook() 196 | .range('A1').set(29261) # 1980-02-10 197 | .range('B1').set_formula('=excelbind.just_the_date(A1)') 198 | .calculate() 199 | ) 200 | 201 | assert excel.range('B1').value == excel.range('A1').value 202 | 203 | 204 | def test_pandas_series(xll_addin_path): 205 | with set_env_vars('basic_functions'): 206 | with Excel() as excel: 207 | excel.register_xll(xll_addin_path) 208 | 209 | ( 210 | excel.new_workbook() 211 | .range('A1').set(1) 212 | .range('B1').set(1.1) 213 | .range('A2').set(2) 214 | .range('B2').set(2.1) 215 | .range('A3').set(3) 216 | .range('B3').set(-1.1) 217 | .range('C1:D3').set_formula('=excelbind.pandas_series(A1:B3)') 218 | .calculate() 219 | ) 220 | 221 | assert excel.range('D1').value == excel.range('B1').value 222 | assert excel.range('D2').value == excel.range('B2').value 223 | assert excel.range('D3').value == -excel.range('B3').value 224 | 225 | 226 | def test_pandas_series_from_list(xll_addin_path): 227 | with set_env_vars('basic_functions'): 228 | with Excel() as excel: 229 | excel.register_xll(xll_addin_path) 230 | 231 | ( 232 | excel.new_workbook() 233 | .range('A1').set(1) 234 | .range('B1').set(1.2) 235 | .range('A2').set(2) 236 | .range('B2').set(2.1) 237 | .range('A3').set(3) 238 | .range('B3').set(-1.1) 239 | .range('C1').set_formula('=excelbind.pandas_series_sum(A1:B3)') 240 | .range('D1').set_formula('=excelbind.pandas_series_sum(B1:B3)') 241 | .calculate() 242 | ) 243 | 244 | assert excel.range('D1').value == excel.range('C1').value 245 | assert excel.range('C1').value == pytest.approx(2.2, abs=1e-10) 246 | 247 | 248 | def test_pandas_dataframe(xll_addin_path): 249 | with set_env_vars('basic_functions'): 250 | with Excel() as excel: 251 | excel.register_xll(xll_addin_path) 252 | 253 | ( 254 | excel.new_workbook() 255 | .range('B1').set("A") 256 | .range('C1').set("B") 257 | .range('A2').set(1) 258 | .range('B2').set(1.2) 259 | .range('C2').set(2.2) 260 | .range('A3').set(2) 261 | .range('B3').set(2.1) 262 | .range('C3').set(3.1) 263 | .range('A4').set(3) 264 | .range('B4').set(-1.1) 265 | .range('C4').set(-2.1) 266 | .range('D1:F4').set_formula('=excelbind.pandas_dataframe(A1:C4)') 267 | .range('G1').set_formula('=sum(B2:C4)') 268 | .range('H1').set_formula('=sum(E2:F4)') 269 | .calculate() 270 | ) 271 | 272 | assert excel.range('D1').value == "" 273 | assert excel.range('A2').value == excel.range('D2').value 274 | assert excel.range('A3').value == excel.range('D3').value 275 | assert excel.range('A4').value == excel.range('D4').value 276 | 277 | assert excel.range('B1').value == excel.range('E1').value 278 | assert excel.range('C1').value == excel.range('F1').value 279 | 280 | assert excel.range('H1').value == excel.range('G1').value + 6*2 281 | 282 | 283 | def test_volatile_function(xll_addin_path): 284 | with set_env_vars('basic_functions'): 285 | with Excel() as excel: 286 | excel.register_xll(xll_addin_path) 287 | 288 | ( 289 | excel.new_workbook() 290 | .range('A1').set_formula('=excelbind.the_time()') 291 | .calculate() 292 | ) 293 | 294 | time1 = excel.range('A1').value 295 | time.sleep(0.1) 296 | excel.calculate() 297 | time2 = excel.range('A1').value 298 | 299 | assert time1 != time2 300 | -------------------------------------------------------------------------------- /test/utilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuneLjungmann/excelbind/bde6dbfce32f1b68e8b2179c82549f34ea2eb5b4/test/utilities/__init__.py -------------------------------------------------------------------------------- /test/utilities/env_vars.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import pathlib 4 | from contextlib import contextmanager 5 | 6 | @contextmanager 7 | def set_env_vars(module_name): 8 | try: 9 | backup_env = os.environ.copy() 10 | os.environ['EXCELBIND_MODULEDIR'] = str(pathlib.Path(__file__).parent / '..' / '..' / 'examples') 11 | os.environ['EXCELBIND_MODULENAME'] = module_name 12 | os.environ['EXCELBIND_FUNCTIONPREFIX'] = 'excelbind' 13 | if hasattr(sys, 'real_prefix'): 14 | os.environ['EXCELBIND_VIRTUALENV'] = sys.prefix 15 | 16 | yield 17 | finally: 18 | os.environ = backup_env 19 | -------------------------------------------------------------------------------- /test/utilities/excel.py: -------------------------------------------------------------------------------- 1 | from subprocess import run 2 | import win32com.client as win32 3 | import win32process 4 | import time 5 | 6 | 7 | def _kill_process_if_it_doesnt_shut_down(process_id, wait_time): 8 | for i in range(wait_time): 9 | if not _is_process_running(process_id): 10 | break 11 | time.sleep(1) 12 | else: 13 | _kill_process(process_id) 14 | 15 | 16 | def _kill_process(process_id): 17 | cmd = f'taskkill /f /PID {process_id}' 18 | output = run(cmd, capture_output=True, universal_newlines=True) 19 | 20 | 21 | def _is_process_running(process_id): 22 | cmd = f'tasklist /FI "PID eq {process_id}"' 23 | output = run(cmd, capture_output=True, universal_newlines=True) 24 | return str(process_id) in output.stdout 25 | 26 | 27 | class Excel: 28 | """ Wrapper around the Excel com object. 29 | 30 | Handles clean shutdown of excel and exposes workbooks, worksheets and ranges in a 'builder' like pattern, 31 | so small test example sheets can be build and calculated easily. 32 | """ 33 | def __init__(self, visible=False): 34 | self._visible = visible 35 | self._excel = None 36 | self._active_range = None 37 | self._process_id = None 38 | 39 | def __enter__(self): 40 | self._excel = win32.Dispatch('Excel.Application') 41 | thread_id, self._process_id = win32process.GetWindowThreadProcessId(self._excel.Hwnd) 42 | self._excel.Visible = self._visible 43 | return self 44 | 45 | def __exit__(self, exc_type, exc_val, exc_tb): 46 | # Note the excessive use of 'del' is to make sure all python references to the pywin32 com objects are deleted 47 | # Otherwise a lot of errors are reported because there are references to dead com objects 48 | # which are later cleaned up by the gc 49 | del self._active_range 50 | wbs = self._excel.Workbooks 51 | for book in wbs: 52 | book.Close(False) 53 | del book 54 | del wbs 55 | 56 | # try to shut down excel nicely 57 | self._excel.Quit() 58 | del self._excel 59 | # then make sure to kill Excel if process is hanging for some reason - usually if test failed 60 | _kill_process_if_it_doesnt_shut_down(self._process_id, 5) 61 | 62 | def register_xll(self, xll_addin_path): 63 | self._excel.RegisterXLL(xll_addin_path) 64 | 65 | def calculate(self): 66 | ws = self._excel.ActiveSheet 67 | ws.Calculate() 68 | del ws 69 | return self 70 | 71 | def sheet(self, name): 72 | ws = self._excel.Worksheets(name) 73 | ws.Activate() 74 | del ws 75 | return self 76 | 77 | def book(self, name): 78 | wb = self._excel.Workbooks(name) 79 | wb.Activate() 80 | del wb 81 | return self 82 | 83 | def new_workbook(self): 84 | wb = self._excel.Workbooks.Add() 85 | wb.Activate() 86 | del wb 87 | return self 88 | 89 | def open(self, name): 90 | wb = self._excel.Workbooks.Open(name) 91 | wb.Activate() 92 | del wb 93 | return self 94 | 95 | def range(self, r): 96 | self._active_range = self._excel.Range(r) 97 | return self 98 | 99 | def cell(self, x, y): 100 | self._active_range = self._excel.Cells(x, y) 101 | return self 102 | 103 | def set(self, value): 104 | self._active_range.Value = value 105 | return self 106 | 107 | def set_formula(self, formula): 108 | if self._active_range.Count > 1: 109 | self._active_range.FormulaArray = formula 110 | else: 111 | self._active_range.Formula = formula 112 | return self 113 | 114 | @property 115 | def value(self): 116 | return self._active_range.Value 117 | --------------------------------------------------------------------------------