Skip to content
This repository has been archived by the owner on Nov 3, 2020. It is now read-only.

Commit

Permalink
Merge pull request #7 from enthought/enh/ribbon-contribution
Browse files Browse the repository at this point in the history
Extension based ribbon contribution
  • Loading branch information
dpinte committed May 19, 2016
2 parents 50729e4 + 1aa13d8 commit 49f7573
Show file tree
Hide file tree
Showing 11 changed files with 332 additions and 31 deletions.
Empty file.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions doc/data_analytics_example/data_analytics/pyxll_extension.py
@@ -0,0 +1,16 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-


def load(submit_ribbon_tab=None, **kw):
""" Loads the PyXLL modules to be exposed to Excel. """

# Try to submit our ribbon piece to PyXLL

if submit_ribbon_tab:
import os
import pkgutil
tab_template = pkgutil.get_data('data_analytics', 'ribbon_tab.xml')
root_path = os.path.join(os.path.dirname(__file__), '')
tab = tab_template.format(ROOT=root_path)
submit_ribbon_tab('data_analytics', tab)
9 changes: 9 additions & 0 deletions doc/data_analytics_example/data_analytics/ribbon_tab.xml
@@ -0,0 +1,9 @@
<tab id="data_analytics_tab" label="DataAnalytics">
<group id="data_analytics_group" label="Analyze">
<button id="data_analytics_button"
size="large"
label="Do Some Analytics!"
onAction="pyxll.about"
image="{ROOT}data_analytics_button.png"/>
</group>
</tab>
16 changes: 16 additions & 0 deletions doc/data_analytics_example/setup.py
@@ -0,0 +1,16 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup, find_packages

setup(
name='data_analytics',
version='1.0',
packages=find_packages(),
package_data={'data_analytics': ['ribbon_tab.xml', '*.png']},
entry_points={
'pyxll.extensions': [
'data_analytics = data_analytics.pyxll_extension:load'
]
}
)
131 changes: 108 additions & 23 deletions doc/source/extension_loader.rst
Expand Up @@ -9,7 +9,7 @@ always practical and does not scale in the context of corporate deployment.

The extension loader for PyXLL allows developer to contribute functionalities
to PyXLL without the need to modify the configuration file. By exposing an
entry point, any Python package is allowed to contribute PyxLL function, macros
entry point, any Python package is allowed to contribute PyXLL function, macros
and menus in a very practical way. The extension loader relies on standard
Python deployment process which makes it a good choice for deployment.

Expand All @@ -27,16 +27,30 @@ the list of modules::
modules =
...
pyxll_utils.extension_loader

The next time PyXLL will be started all the Python packages contributing to the
`pyxll.extensions` extension point will be loaded by PyXLL.

2. Adjust the `ribbon` configuration value.

As of this writing, PyXLL reads the configuration setting for the ribbon file
prior to loading extensions. The solution is to rename the `ribbon`
configuration key to `default_ribbon`, and to create a new `ribbon` with a
value that is safe to modify. This file doesn't need to exist yet and will be
overwritten. We suggest setting this new `ribbon` file to be right next to the
`default_ribbon` like so::

[PYXLL]
...
default_ribbon = ./examples/ribbon/ribbon.xml
ribbon = ./examples/ribbon/ribbon_extended.xml


Developers
----------

The PyXLL utils extension loader uses the `stevedore` package that leverages
the `setuptools` entry point machinerie to discover extensions. To build a
the `setuptools` entry point machinery to discover extensions. To build a
Python package that contributes to PyXLL, one just need to add the following
entry point to the `setup.py` of the package::

Expand All @@ -50,15 +64,25 @@ entry point to the `setup.py` of the package::
)

In this code, the PyXLL extension loader will automatically call
`mypackage.myodule.callable()` when PyXLL will start. If the callable activates
some `@xl_func`, or `@xl_macro` or `@xl_menu` decorators, they will be exposed to
Excel.

Some advanced checkes can be added in the callable (dependencies verification,
version checks, access rights, etc.).

Example
-------
`mypackage.myodule.callable(**kw)` when PyXLL will start. The callable must be
prepared to accept arbitrary keyword arguments in order to avoid forward
compatibility issues as this extension mechanism grows new features. If the
callable activates some `@xl_func`, or `@xl_macro` or `@xl_menu` decorators,
they will be exposed to Excel.

Some advanced checks can be added in the callable (dependencies verification,
version checks, access rights, etc.).

The ribbon workaround is due to PyXLL reading the config file prior to loading
any extensions. To work around this, the extension mechanism must
write directly over the file marked as `ribbon` so that PyXLL will load it. Our
solution is specify a second "default" ribbon file, which will serve as the
base to which other extensions can contribute. PyXLL will combine the
`default_ribbon` with any additional ribbon features contributed by extensions
before writing out a finalized ribbon to `ribbon`.

Basic Example
-------------

A developer wants to expose the `ewma` functions from the Pandas library to
Excel. The function will belong to a package `data_analytics`.
Expand All @@ -68,23 +92,23 @@ The package will contain the following file:
1. `data_analytics/stats.py`::

from pandas.stats.moments import ewma as pandas_ewma

from pyxll import xl_func

@xl_func('dataframe df, float span: dataframe')
def ewma(df, span):
""" Exponentially-weighted moving average. """
return pandas_ewma(df, span)

2. `data_analytics/pyxll_extension.py`::

def load():
def load(**kw):
""" Loads the PyXLL modules to be exposed to Excel. """

# load PyXLL utilities dependencies
# required to support the dataframe type converters
import pyxll_utils.pandastypes

# required to support the dataframe type converters
import pyxll_utils.pandastypes

# load all the local modules that we want to expose to Excel
import data_analytics.stats
Expand All @@ -99,18 +123,79 @@ The package will contain the following file:
packages=find_packages(),
entry_points = {
'pyxll.extensions' : [
'data_analytics_extension = data_analytics.pyxll_extension:load'
'data_analytics = data_analytics.pyxll_extension:load'
]
}
)

Then build and install your egg, start Excel and the ewna function will be
Then build and install your egg, start Excel and the ewma function will be
available. The PyXLL log file will list your extension::

2015-02-03 16:15:34,510 - INFO : Extensions loaded:
data_analytics_extension
data_analytics
...


Ribbon Extension Example
------------------------

A developer has written an extension which should contribute a tab to the excel
ribbon (requires PyXLL >= 3.0). If the `tab` element's `id` attribute matches
an existing tab, the two tabs will be combined by appended the contributed
tab's groups to the original.

The package will contain the following files:

1. `data_analytics/pyxll_extension.py`::

def load(submit_ribbon_tab=None, **kw):
""" Loads the PyXLL modules to be exposed to Excel. """

# Try to submit our ribbon piece to PyXLL
if submit_ribbon_tab:
import os
import pkgutil
tab_template = pkgutil.get_data('data_analytics', 'ribbon_tab.xml')
root_path = os.path.dirname(__file__)
tab = tab_template.format(ROOT=root_path)
submit_ribbon_tab('data_analytics', tab)

2. `setup.py`::

from setuptools import setup, find_packages

setup(
name='data_analytics',
version='1.0',
packages=find_packages(),
package_data={'data_analytics': ['ribbon_tab.xml', '*.png']},
entry_points={
'pyxll.extensions': [
'data_analytics = data_analytics.pyxll_extension:load'
]
}
)

3. `data_analytics/ribbon_tab.xml`::

<tab id="data_analytics_tab" label="DataAnalytics">
<group id="data_analytics_group" label="Analyze">
<button id="data_analytics_button"
size="large"
label="Do Some Analytics!"
onAction="pyxll.about"
image="{ROOT}\data_analytics_button.png"/>
</group>
</tab>

4. `data_analytics/data_analytics_button.png` - An image file


Then build and install your egg, start Excel and the new ribbon button will be
available. The PyXLL log file will announce your extension's ribbon fragment::

2016-03-05 12:22:48,867 - INFO : Adding ribbon fragment: data_analytics
2016-03-05 12:22:48,867 - INFO : Wrote extended ribbon to C:\Users\EnUser\AppData\Local\Enthught\Canopy32\User\lib\site-packages\pyxll\examples\ribbon\ribbon_extended.xml
2016-03-05 12:22:48,867 - INFO : Extensions loaded:
data_analytics
...
1 change: 1 addition & 0 deletions endist.dat
Expand Up @@ -4,4 +4,5 @@ packages = [
'pandas',
'pytz',
'stevedore',
'lxml',
]
16 changes: 16 additions & 0 deletions pyxll_utils/data/ribbon.xml
@@ -0,0 +1,16 @@
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui"
loadImage="pyxll.load_image"> <!-- pyxll.load_image is a built-in image loader -->
<ribbon>
<tabs>
<tab id="pyxll_example_tab" label="Sample">
<group id="Tools" label="Tools">
<button id="About"
size="large"
label="About PyXLL"
onAction="pyxll.about"
image="about.png"/>
</group>
</tab>
</tabs>
</ribbon>
</customUI>
44 changes: 40 additions & 4 deletions pyxll_utils/extension_loader.py
Expand Up @@ -5,22 +5,58 @@
the installed packages that contributes to PyXLL through setuptools.
"""
import errno
import logging
import os

from stevedore import extension

from pyxll import get_config
from .ribbon_synthesizer import RibbonSynthesizer

logger = logging.getLogger(__name__)

# Keyword args to pass to plugin initializers
invoke_kwds = {}

config = dict(get_config().items('PYXLL'))
default_ribbon_path = config.get('default_ribbon', '')
ribbon_path = config.get('ribbon', '')

should_make_ribbon = (default_ribbon_path and ribbon_path and
default_ribbon_path != ribbon_path)
ribbon_synthesizer = RibbonSynthesizer.from_file(default_ribbon_path)

if should_make_ribbon:
invoke_kwds['submit_ribbon_tab'] = ribbon_synthesizer.submit_ribbon_tab
else:
logger.info("The ribbon will not be modified because your config does"
" not define both PYXLL::ribbon and PYXLL::default_ribbon")

extension_manager = extension.ExtensionManager(
namespace='pyxll.extensions',
invoke_on_load=True,
namespace='pyxll.extensions',
invoke_on_load=True,
invoke_kwds=invoke_kwds,
)

extension_names = extension_manager.names()

if len(extension_manager.names()) > 0:
if len(extension_names) > 0:
logger.info(
'Extensions loaded:\n{}'.format('\n'.join(extension_manager.names()))
'Extensions loaded:\n{}'.format('\n'.join(extension_names))
)
else:
logger.info('No extension loaded')

if should_make_ribbon:
target_dir = os.path.dirname(ribbon_path)
try:
# If the ribbon dirs don't exist, make them.
os.makedirs(target_dir)
logger.info("Created ribbon directory: {}".format(target_dir))
except Exception as e:
if not e.errno == errno.EEXIST:
raise
with open(ribbon_path, 'w') as f:
f.write(ribbon_synthesizer.to_bytes())
logger.info("Wrote extended ribbon to {}".format(ribbon_path))

0 comments on commit 49f7573

Please sign in to comment.