Skip to content

Commit aad416a

Browse files
added dict -> xml export option
- fixed recursive errors with VBA XML export - added method to populate entire browser tree
1 parent dd279c4 commit aad416a

File tree

2 files changed

+139
-36
lines changed

2 files changed

+139
-36
lines changed

pyvba/browser.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,17 @@ def _generate(self):
105105
if value not in visited[value.type]:
106106
visited[value.type].append(value)
107107
else:
108-
self._all[name] = visited[value.type].index(value)
108+
self._all[name] = visited[value.type][visited[value.type].index(value)]
109+
110+
def browse_all(self):
111+
"""Populate the browser and all descendents of the browser."""
112+
if self._all == {}:
113+
self._generate()
114+
115+
# populate child browsers if not already visited
116+
for name, value in self._all.items():
117+
if type(value) is Browser and value not in visited[value.type]:
118+
name.browse_all()
109119

110120
def search(self, name: str, exact: bool = False):
111121
"""Return a dictionary in format {path: item} matching the name.
@@ -141,6 +151,10 @@ def goto(self, path: str):
141151
"""
142152
...
143153

154+
def view_vba(self) -> str:
155+
"""Returns a string that replicates the VBA tree."""
156+
...
157+
144158
def regen(self):
145159
"""Regenerate the `all` property."""
146160
self._all = {}

pyvba/export.py

Lines changed: 124 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import os
22
import re
3+
import copy
34

4-
from pyvba.browser import Browser
5+
from win32com.universal import com_error
6+
7+
from pyvba.browser import Browser, visited
58
from pyvba.viewer import FunctionViewer
69

710

811
class ExportStr:
9-
def __init__(self, browser: Browser, skip_func: bool = False, skip_err: bool = False):
12+
def __init__(self, browser: Browser, skip_func: bool = False, skip_err: bool = False, vba_form: bool = False):
1013
"""The base class for exporting
1114
1215
Parameters
@@ -17,12 +20,15 @@ def __init__(self, browser: Browser, skip_func: bool = False, skip_err: bool = F
1720
Skips reporting any FunctionViewer instant.
1821
skip_err: bool
1922
Skips reporting any error.
23+
vba_form: bool
24+
A flag that determines if the output mimics the VBA tree structure or a more list-like view.
2025
"""
2126
self._browser = browser
2227
self._data = None
2328

2429
self._skip_func = skip_func
2530
self._skip_err = skip_err
31+
self._vba_form = vba_form
2632

2733
@property
2834
def data_str(self) -> str:
@@ -32,7 +38,7 @@ def data_str(self) -> str:
3238

3339
@property
3440
def data_min(self) -> str:
35-
"""Return the data in minimized string format.
41+
"""Return the data in a minimized string format.
3642
3743
The minimized version removes all newlines and tabs.
3844
"""
@@ -42,10 +48,14 @@ def data_min(self) -> str:
4248
def _check(self):
4349
"""Check if the string needs to be generated."""
4450
if self._data is None:
45-
self._generate()
51+
self._generate_vba() if self._vba_form else self._generate_dict()
52+
53+
def _generate_vba(self, *args, **kwargs):
54+
"""Begin generating the string based on the VBA tree."""
55+
pass
4656

47-
def _generate(self, *args, **kwargs):
48-
"""Begin generating the string."""
57+
def _generate_dict(self, *args, **kwargs):
58+
"""Begin generating the string based on the browser.visited dictionary."""
4959
pass
5060

5161
def save_as(self, name: str, ext: str, path: str = '.\\', minimize: bool = False):
@@ -60,15 +70,21 @@ def save_as(self, name: str, ext: str, path: str = '.\\', minimize: bool = False
6070
path: str
6171
The save location.
6272
minimize: bool
63-
A flag that determines the format of the final output.
73+
A flag that determines if the data is returned in a minimized string format.
6474
"""
6575
os.makedirs(path, exist_ok=True)
6676
with open(os.path.join(path, name + ext), "w") as file:
6777
file.write(self.data_str if not minimize else self.data_min)
6878
file.close()
6979

7080
def print(self, minimize: bool = False):
71-
"""Print the string in the normal or minimized version."""
81+
"""Print the string in the normal or minimized version.
82+
83+
Parameters
84+
----------
85+
minimize: bool
86+
A flag that determines if the data is returned in a minimized string format.
87+
"""
7288
self._check()
7389
print(self.data_str if not minimize else self.data_min)
7490

@@ -83,7 +99,7 @@ class XMLExport(ExportStr):
8399
}
84100

85101
def __init__(self, browser: Browser, version=1.0, encoding: str = "UTF-8", skip_func: bool = False,
86-
skip_err: bool = False):
102+
skip_err: bool = False, vba_form: bool = False):
87103
"""Create a well-formed XML string for export.
88104
89105
Parameters
@@ -95,7 +111,7 @@ def __init__(self, browser: Browser, version=1.0, encoding: str = "UTF-8", skip_
95111
encoding: str
96112
The encoding type (default is UTF-8).
97113
"""
98-
super().__init__(browser, skip_func, skip_err)
114+
super().__init__(browser, skip_func, skip_err, vba_form)
99115

100116
self._xml_head = f'<?xml version="{str(version)}" encoding="{encoding}"?>\n'
101117

@@ -104,8 +120,8 @@ def xml_encode(text: str) -> str:
104120
"""Map special XML characters to their encoded form in a given string."""
105121
return "".join(XMLExport.XML_ESCAPE_CHARS.get(c, c) for c in str(text))
106122

107-
def _generate(self):
108-
"""Begin generating the XML string."""
123+
def _generate_vba(self):
124+
"""Begin generating the XML string based on the VBA tree."""
109125
self._data = self._xml_head + self._generate_tag(self._browser)
110126

111127
# convert empty elements to a single tag
@@ -128,12 +144,19 @@ def _generate_tag(self, elem, tabs: int = 0, **kwargs) -> str:
128144
"""
129145

130146
xml = ''
147+
stack = kwargs.get('stack', [])
148+
131149
if isinstance(elem, Browser):
132-
# display the browser and its children
150+
tag = XMLExport.Tag(elem.name)
133151

134-
# setup the tag and attributes
152+
# check if in stack already
153+
if elem in stack:
154+
return tag.enclose('BrowserObject: See ancestors', tabs)
155+
else:
156+
stack.append(elem)
157+
158+
# setup the tag attributes
135159
attrs = ["Name", "Count"]
136-
tag = XMLExport.Tag(elem.name)
137160
[
138161
tag.add_attr(attr, value)
139162
for attr, value in elem.all.items()
@@ -143,45 +166,110 @@ def _generate_tag(self, elem, tabs: int = 0, **kwargs) -> str:
143166
# add the element and start adding the sub-elements
144167
xml += '\t' * tabs + tag.open_tag + '\n'
145168
for item, value in elem.all.items():
146-
if type(value) is list:
169+
if isinstance(value, list):
147170
item_tag = XMLExport.Tag("Item")
148171

149172
xml += '\t' * (tabs + 1) + item_tag.open_tag + '\n'
150173
for i in value:
151-
xml += self._generate_tag(i, tabs + 2)
174+
xml += self._generate_tag(i, tabs + 2, stack=stack)
152175
xml += '\t' * (tabs + 1) + item_tag.close_tag + '\n'
153176

154177
elif item not in attrs:
155-
# overlook objects that call itself
178+
# overlook objects that point to themselves
156179
if item == elem.name:
157180
continue
158181
else:
159-
xml += self._generate_tag(value, tabs + 1, name=item)
182+
xml += self._generate_tag(value, tabs + 1, name=item, stack=stack)
160183

161184
xml += '\t' * tabs + tag.close_tag + '\n'
185+
162186
elif isinstance(elem, FunctionViewer):
163187
if not self._skip_func:
164188
# display the function and its properties
165189
tag = XMLExport.Tag("Function", name=elem.name, args=len(elem.args))
166190
xml += tag.enclose(str(elem)[26:], tabs)
167-
elif isinstance(elem, BaseException):
191+
192+
elif isinstance(elem, com_error):
168193
# display the error location and method
169194
if not self._skip_err:
170-
try:
171-
tag = XMLExport.Tag("Error", on=str(elem.args[2][1]))
172-
xml += tag.enclose(self.xml_encode(str(elem.args[2][2])), tabs)
173-
except TypeError:
174-
tag = XMLExport.Tag("Error")
175-
xml += tag.enclose(self.xml_encode(str(elem.args[2])), tabs)
176-
except IndexError:
177-
tag = XMLExport.Tag("Error")
178-
xml += tag.enclose(self.xml_encode(str(elem)), tabs)
195+
tag = XMLExport.Tag("Error")
196+
xml += tag.enclose(self.xml_encode(str(elem)), tabs)
197+
179198
else:
180199
# display the variable and value
181200
tag = XMLExport.Tag(kwargs.get('name', 'Unknown'))
182201
xml += tag.enclose(self.xml_encode(str(elem)), tabs)
183202
return xml
184203

204+
def _generate_dict(self):
205+
"""Begin generating the XML string based on the visited dictionary."""
206+
# populate browser and copy visited
207+
self._browser.browse_all()
208+
visited2 = copy.copy(visited)
209+
210+
tag = XMLExport.Tag(self._browser.name, count=len(visited2))
211+
xml = self._xml_head + tag.open_tag + "\n"
212+
213+
# iterate through dictionary items
214+
for var, value in visited2.items():
215+
tag1 = XMLExport.Tag(var, count=len(value))
216+
xml += "\t" + tag1.open_tag + "\n"
217+
218+
# iterate through each list
219+
for item in value:
220+
tag2 = XMLExport.Tag(item.name)
221+
xml += "\t" * 2 + tag2.open_tag + "\n"
222+
223+
# add name attribute
224+
if 'Name' in item.all:
225+
tag2.add_attr('Name', item.Name)
226+
227+
# iterate through each browser in the list
228+
for var2, value2 in item.all.items():
229+
tag3 = XMLExport.Tag(var2)
230+
231+
# add name attribute
232+
if isinstance(value2, Browser) and 'Name' in value2.all:
233+
tag3.add_attr('Name', value2.Name)
234+
235+
# check for a collection object
236+
if isinstance(value2, list):
237+
tag3.add_attr('count', len(value2))
238+
xml += "\t" * 3 + tag3.open_tag + "\n"
239+
240+
# iterate through the browser's collection
241+
for item2 in value2:
242+
tag4 = XMLExport.Tag(item2.name if isinstance(item2, Browser) else item2)
243+
244+
# add name attribute
245+
if isinstance(item2, Browser) and 'Name' in item2.all:
246+
tag4.add_attr('Name', item2.Name)
247+
248+
xml += tag4.enclose(item2.name if isinstance(item2, Browser) else item2, 4)
249+
250+
xml += "\t" * 3 + tag3.close_tag + "\n"
251+
else:
252+
if isinstance(value2, Browser):
253+
output = 'BrowserObject'
254+
elif isinstance(value2, com_error):
255+
if self._skip_err:
256+
continue
257+
output = self.xml_encode(repr(value2))
258+
elif isinstance(value2, FunctionViewer):
259+
if self._skip_func:
260+
continue
261+
tag3 = XMLExport.Tag("Function", name=value2.name, args=len(value2.args))
262+
output = str(value2)[26:]
263+
else:
264+
output = self.xml_encode(value2)
265+
xml += tag3.enclose(output, 3)
266+
267+
xml += "\t" * 2 + tag1.close_tag + "\n"
268+
269+
xml += "\t" + tag1.close_tag + "\n"
270+
271+
self._data = xml + tag.close_tag
272+
185273
def save(self, name: str, path: str = '.\\', minimize: bool = False):
186274
"""Save to a file."""
187275
super().save_as(name, '.xml', path, minimize)
@@ -224,6 +312,7 @@ def open_tag(self) -> str:
224312
tag += " " + " ".join(
225313
f'{key}="{XMLExport.xml_encode(value)}"'
226314
for key, value in self._attrs.items()
315+
if not isinstance(value, com_error)
227316
)
228317
return tag + ">"
229318

@@ -256,8 +345,8 @@ def rm_attr(self, attr):
256345
class JSONExport(ExportStr):
257346
JSON_ESCAPE_CHARS = ["\b", "\f", "\n", "\r", "\t", "\"", "\\"]
258347

259-
def __init__(self, browser: Browser, skip_func: bool = False, skip_err: bool = False):
260-
super(JSONExport, self).__init__(browser, skip_func, skip_err)
348+
def __init__(self, browser: Browser, skip_func: bool = False, skip_err: bool = False, vba_form: bool = False):
349+
super(JSONExport, self).__init__(browser, skip_func, skip_err, vba_form)
261350

262351
@staticmethod
263352
def json_encode(text: str) -> str:
@@ -271,10 +360,10 @@ def json_encode(text: str) -> str:
271360
def _check(self):
272361
"""Check if the JSON string needs to be generated."""
273362
if self._data is None:
274-
self._data = self._generate(self._browser)
363+
self._data = self._generate_vba(self._browser)
275364
self._data = re.sub(r',(?!\s*?[{\[\"\'\w])', '', self._data)
276365

277-
def _generate(self, elem, tabs: int = 0, **kwargs) -> str:
366+
def _generate_vba(self, elem, tabs: int = 0, **kwargs) -> str:
278367
"""Recursively generate each element into a string.
279368
280369
Parameters
@@ -299,10 +388,10 @@ def _generate(self, elem, tabs: int = 0, **kwargs) -> str:
299388
if type(value) is list and len(value) > 0:
300389
json += "\t" * (tabs + 1) + "{ \"Item\": [\n"
301390
for i in value:
302-
json += self._generate(i, tabs + 2)
391+
json += self._generate_vba(i, tabs + 2)
303392
json += "\t" * (tabs + 1) + "]},\n"
304393
else:
305-
json += self._generate(value, tabs + 1, name=item)
394+
json += self._generate_vba(value, tabs + 1, name=item)
306395

307396
json += "\t" * tabs + "]},\n"
308397
elif isinstance(elem, FunctionViewer):

0 commit comments

Comments
 (0)