Skip to content

Commit 0029275

Browse files
committed
refactoring
1 parent ad831e7 commit 0029275

31 files changed

+531
-433
lines changed

pyaddin/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
from . import src

pyaddin/addin.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import os
2+
import logging
3+
import xml.etree.ElementTree as ET
4+
import win32com.client
5+
from .xlam.ui import UI
6+
from .xlam.vba import VBA
7+
from .share import (AddInException, copytree)
8+
9+
10+
# logging
11+
logging.basicConfig(
12+
level=logging.INFO,
13+
format="[%(levelname)s] %(message)s")
14+
15+
16+
# configuration path
17+
SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__))
18+
RESOURCE_PATH = os.path.join(SCRIPT_PATH, 'resources')
19+
20+
RESOURCE_ADDIN = 'xlam'
21+
RESOURCE_PYTHON = 'python'
22+
RESOURCE_VBA = 'vba'
23+
24+
VBA_GENERAL = 'general'
25+
VBA_MENU = 'menu'
26+
VBA_WORKBOOK = 'ThisWorkbook'
27+
28+
CUSTOM_UI = 'CustomUI.xml'
29+
30+
31+
class Addin:
32+
33+
def __init__(self, xlam_file:str, visible:bool=False) -> None:
34+
'''The Excel add-in object, including ribbon UI and VBA modules.
35+
36+
Args:
37+
xlam_file (str): Add-in file path.
38+
visible (bool): Process the add-in with Excel application running in the background if False.
39+
'''
40+
# work path
41+
self.xlam_file = xlam_file
42+
self.path = os.path.dirname(xlam_file)
43+
44+
# Add-in VBA modules
45+
self.excel_app = win32com.client.Dispatch('Excel.Application') # win32 COM object
46+
self.excel_app.Visible = visible
47+
self.excel_app.DisplayAlerts = False
48+
49+
50+
def close(self):
51+
'''Close add-in and exit Excel.'''
52+
self.excel_app.Application.Quit()
53+
54+
55+
def create(self, vba_only:bool=False):
56+
'''Create addin file.
57+
- customize ribbon tab and associated VBA callback according to ui file
58+
- include VBA modules, e.g., general VBA subroutines for data transferring.
59+
60+
Args:
61+
vba_only (bool, optional): Whether simple VBA addin (without Python related modules).
62+
Defaults to False.
63+
'''
64+
N = 2 if vba_only else 3
65+
66+
# create addin file
67+
logging.info('(1/%d) Creating add-in structure...', N)
68+
ui = UI(self.xlam_file)
69+
template = os.path.join(RESOURCE_PATH, RESOURCE_ADDIN)
70+
custom_ui = os.path.join(self.path, CUSTOM_UI)
71+
ui.create(template, custom_ui)
72+
73+
if not os.path.exists(self.xlam_file):
74+
raise AddInException('Create add-in structures failed.')
75+
76+
# update VBA module
77+
vba = VBA(xlam_file=self.xlam_file, excel_app=self.excel_app)
78+
base_menu = os.path.join(RESOURCE_PATH, RESOURCE_VBA, f'{VBA_MENU}.bas')
79+
80+
# 1. import menu module
81+
logging.info('(2/%d) Creating menu callback subroutines...', N)
82+
83+
# create callback function module for customized menu button
84+
callbacks = self.__get_callbacks_from_custom_ui()
85+
vba.add_callbacks(VBA_MENU, callbacks, base_menu)
86+
87+
# extra steps for VBA-Python combined addin
88+
if not vba_only:
89+
logging.info('(3/%d) Creating Python-VBA interaction modules...', N)
90+
91+
# 2. import workbook module
92+
workbook_module = os.path.join(RESOURCE_PATH, RESOURCE_VBA, f'{VBA_WORKBOOK}.cls')
93+
vba.import_named_module(VBA_WORKBOOK, workbook_module)
94+
95+
# 3. import general module
96+
general_module = os.path.join(RESOURCE_PATH, RESOURCE_VBA, f'{VBA_GENERAL}.bas')
97+
vba.import_module(general_module)
98+
99+
# 4. copy main python scripts
100+
python_module = os.path.join(RESOURCE_PATH, RESOURCE_PYTHON)
101+
copytree(python_module, self.path)
102+
103+
# save vba modules
104+
vba.save()
105+
106+
107+
def update(self):
108+
'''Update Ribbon and associated callback functions.
109+
110+
NOTE: only update newly added callbacks. Editing existing callback names will be ignored.
111+
'''
112+
# update addin with customized ui file
113+
logging.info('(1/2) Updating ribbon structures...')
114+
custom_ui = os.path.join(self.path, CUSTOM_UI)
115+
ui = UI(self.xlam_file)
116+
ui.update(custom_ui)
117+
118+
# update VBA menu module
119+
logging.info('(2/2) Updating menu callback subroutines...')
120+
vba = VBA(xlam_file=self.xlam_file, excel_app=self.excel_app)
121+
callbacks = self.__get_callbacks_from_custom_ui() # get new callback functions
122+
vba.update_callbacks(VBA_MENU, callbacks)
123+
124+
# save vba modules
125+
vba.save()
126+
127+
128+
def __get_callbacks_from_custom_ui(self) -> list:
129+
'''parse CustomUI.xml to collect all callback function names, e.g.,
130+
- attribute=onAction for button, toggleButton and checkBox.
131+
- attribute=onChange for editBox and comboBox.
132+
'''
133+
ui_file = os.path.join(self.path, CUSTOM_UI)
134+
if not os.path.exists(ui_file):
135+
raise AddInException(f'Can not find ribbon file: {CUSTOM_UI}.')
136+
137+
try:
138+
tree = ET.parse(ui_file)
139+
except ET.ParseError as e:
140+
raise AddInException(f'Error format in {CUSTOM_UI}: {str(e)}')
141+
else:
142+
root = tree.getroot()
143+
144+
# get root and check all nodes by iteration
145+
callbacks = []
146+
for attr_name in ('onAction', 'onChange'):
147+
callbacks.extend([node.attrib.get(attr_name) \
148+
for node in root.iter() if attr_name in node.attrib])
149+
150+
if not callbacks:
151+
raise AddInException(f'No actions defined: {CUSTOM_UI}')
152+
153+
return callbacks

pyaddin/main.py

Lines changed: 60 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,65 @@
11
import os
2-
import sys
3-
import argparse
2+
import shutil
3+
import logging
4+
from .addin import (Addin, RESOURCE_PATH, CUSTOM_UI)
45

5-
from .src.pyaddin import init_project, create_addin, update_addin
6+
7+
class PyAddin:
8+
'''Command line interface for ``PyAddin``.'''
9+
10+
@staticmethod
11+
def init():
12+
'''Initialize project and set current path as working path.'''
13+
ui_file = os.path.join(RESOURCE_PATH, CUSTOM_UI)
14+
work_path = os.getcwd()
15+
shutil.copy(ui_file, work_path)
16+
17+
18+
@staticmethod
19+
def create(name:str='addin', vba:bool=False, quiet:bool=True):
20+
'''Create add-in file (name.xlam) based on ribbon UI file (CustomUI.xml) under working path.
21+
22+
Args:
23+
name (str) : the name of add-in to create (without the suffix ``.xlam``).
24+
vba (bool): create VBA add-in only if True, otherwise VBA-Python addin by default.
25+
quiet (bool): perform the process in the background if True.
26+
'''
27+
filename = os.path.join(os.getcwd(), f'{name}.xlam')
28+
addin = Addin(xlam_file=filename, visible=not quiet)
29+
30+
try:
31+
addin.create(vba_only=vba)
32+
except Exception as e:
33+
logging.error(e)
34+
addin.close()
35+
else:
36+
if quiet: addin.close()
37+
38+
39+
@staticmethod
40+
def update(name:str='addin', quiet:bool=True):
41+
'''Update add-in file (name.xlam) based on ribbon UI file (CustomUI.xml) under working path.
42+
43+
Args:
44+
name (str) : the name of add-in to update (without the suffix ``.xlam``).
45+
quiet (bool): perform the process in the background if True.
46+
'''
47+
filename = os.path.join(os.getcwd(), f'{name}.xlam')
48+
addin = Addin(xlam_file=filename, visible=not quiet)
49+
50+
try:
51+
addin.update()
52+
except Exception as e:
53+
logging.error(e)
54+
addin.close()
55+
else:
56+
if quiet: addin.close()
657

758

859
def main():
9-
'''commands:
10-
pyaddin init
11-
pyaddin create --name --vba
12-
pyaddin update --name
13-
'''
14-
try:
15-
if len(sys.argv) == 1:
16-
sys.argv.append('--help')
17-
18-
# parse arguments
19-
parser = argparse.ArgumentParser()
20-
parser.add_argument('operation', choices=['init', 'create', 'update'], help='init, create, update')
21-
parser.add_argument('-n','--name', default='addin', help='addin file name to be created/updated: [name].xlam')
22-
parser.add_argument('-v','--vba', action='store_true', help='create VBA addin only, otherwise VBA-Python addin by default')
23-
args = parser.parse_args()
24-
25-
# do what you need
26-
current_path = os.getcwd()
27-
if args.operation == 'init':
28-
init_project(current_path)
29-
elif args.operation == 'create':
30-
create_addin(current_path, args.name, args.vba)
31-
elif args.operation == 'update':
32-
update_addin(current_path, args.name)
33-
except Exception as e:
34-
print(e)
60+
import fire
61+
fire.Fire(PyAddin)
62+
63+
64+
if __name__ == '__main__':
65+
main()
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)