130 (?P<ImportFromList> |
130 (?P<ImportFromList> |
131 (?: \( \s* .*? \s* \) ) |
131 (?: \( \s* .*? \s* \) ) |
132 | |
132 | |
133 (?: [^#;\\\n]* (?: \\\n )* )* ) |
133 (?: [^#;\\\n]* (?: \\\n )* )* ) |
134 )""", |
134 )""", |
135 re.VERBOSE | re.DOTALL | re.MULTILINE).search |
135 re.VERBOSE | re.DOTALL | re.MULTILINE, |
|
136 ).search |
136 |
137 |
137 _commentsub = re.compile(r"""#[^\n]*\n|#[^\n]*$""").sub |
138 _commentsub = re.compile(r"""#[^\n]*\n|#[^\n]*$""").sub |
138 |
139 |
139 _modules = {} # cache of modules we've seen |
140 _modules = {} # cache of modules we've seen |
140 |
141 |
141 |
142 |
142 class VisibilityMixin(ClbrBaseClasses.ClbrVisibilityMixinBase): |
143 class VisibilityMixin(ClbrBaseClasses.ClbrVisibilityMixinBase): |
143 """ |
144 """ |
144 Mixin class implementing the notion of visibility. |
145 Mixin class implementing the notion of visibility. |
145 """ |
146 """ |
|
147 |
146 def __init__(self): |
148 def __init__(self): |
147 """ |
149 """ |
148 Constructor |
150 Constructor |
149 """ |
151 """ |
150 if self.name.startswith('__'): |
152 if self.name.startswith("__"): |
151 self.setPrivate() |
153 self.setPrivate() |
152 elif self.name.startswith('_'): |
154 elif self.name.startswith("_"): |
153 self.setProtected() |
155 self.setProtected() |
154 else: |
156 else: |
155 self.setPublic() |
157 self.setPublic() |
156 |
158 |
157 |
159 |
158 class Class(ClbrBaseClasses.Class, VisibilityMixin): |
160 class Class(ClbrBaseClasses.Class, VisibilityMixin): |
159 """ |
161 """ |
160 Class to represent a Python class. |
162 Class to represent a Python class. |
161 """ |
163 """ |
|
164 |
162 def __init__(self, module, name, superClasses, file, lineno): |
165 def __init__(self, module, name, superClasses, file, lineno): |
163 """ |
166 """ |
164 Constructor |
167 Constructor |
165 |
168 |
166 @param module name of the module containing this class |
169 @param module name of the module containing this class |
167 @param name name of this class |
170 @param name name of this class |
168 @param superClasses list of class names this class is inherited from |
171 @param superClasses list of class names this class is inherited from |
169 @param file filename containing this class |
172 @param file filename containing this class |
170 @param lineno linenumber of the class definition |
173 @param lineno linenumber of the class definition |
171 """ |
174 """ |
172 ClbrBaseClasses.Class.__init__(self, module, name, superClasses, file, |
175 ClbrBaseClasses.Class.__init__(self, module, name, superClasses, file, lineno) |
173 lineno) |
|
174 VisibilityMixin.__init__(self) |
176 VisibilityMixin.__init__(self) |
175 |
177 |
176 |
178 |
177 class Function(ClbrBaseClasses.Function, VisibilityMixin): |
179 class Function(ClbrBaseClasses.Function, VisibilityMixin): |
178 """ |
180 """ |
179 Class to represent a Python function. |
181 Class to represent a Python function. |
180 """ |
182 """ |
181 def __init__(self, module, name, file, lineno, signature='', separator=',', |
183 |
182 modifierType=ClbrBaseClasses.Function.General, annotation=""): |
184 def __init__( |
|
185 self, |
|
186 module, |
|
187 name, |
|
188 file, |
|
189 lineno, |
|
190 signature="", |
|
191 separator=",", |
|
192 modifierType=ClbrBaseClasses.Function.General, |
|
193 annotation="", |
|
194 ): |
183 """ |
195 """ |
184 Constructor |
196 Constructor |
185 |
197 |
186 @param module name of the module containing this function |
198 @param module name of the module containing this function |
187 @param name name of this function |
199 @param name name of this function |
188 @param file filename containing this class |
200 @param file filename containing this class |
189 @param lineno linenumber of the class definition |
201 @param lineno linenumber of the class definition |
190 @param signature parameterlist of the method |
202 @param signature parameterlist of the method |
191 @param separator string separating the parameters |
203 @param separator string separating the parameters |
192 @param modifierType type of the function |
204 @param modifierType type of the function |
193 @param annotation return annotation |
205 @param annotation return annotation |
194 """ |
206 """ |
195 ClbrBaseClasses.Function.__init__(self, module, name, file, lineno, |
207 ClbrBaseClasses.Function.__init__( |
196 signature, separator, modifierType, |
208 self, |
197 annotation) |
209 module, |
|
210 name, |
|
211 file, |
|
212 lineno, |
|
213 signature, |
|
214 separator, |
|
215 modifierType, |
|
216 annotation, |
|
217 ) |
198 VisibilityMixin.__init__(self) |
218 VisibilityMixin.__init__(self) |
199 |
219 |
200 |
220 |
201 class Attribute(ClbrBaseClasses.Attribute, VisibilityMixin): |
221 class Attribute(ClbrBaseClasses.Attribute, VisibilityMixin): |
202 """ |
222 """ |
203 Class to represent a class attribute. |
223 Class to represent a class attribute. |
204 """ |
224 """ |
|
225 |
205 def __init__(self, module, name, file, lineno): |
226 def __init__(self, module, name, file, lineno): |
206 """ |
227 """ |
207 Constructor |
228 Constructor |
208 |
229 |
209 @param module name of the module containing this class |
230 @param module name of the module containing this class |
210 @param name name of this class |
231 @param name name of this class |
211 @param file filename containing this attribute |
232 @param file filename containing this attribute |
212 @param lineno linenumber of the class definition |
233 @param lineno linenumber of the class definition |
213 """ |
234 """ |
217 |
238 |
218 class Publics: |
239 class Publics: |
219 """ |
240 """ |
220 Class to represent the list of public identifiers. |
241 Class to represent the list of public identifiers. |
221 """ |
242 """ |
|
243 |
222 def __init__(self, module, file, lineno, idents): |
244 def __init__(self, module, file, lineno, idents): |
223 """ |
245 """ |
224 Constructor |
246 Constructor |
225 |
247 |
226 @param module name of the module containing this function |
248 @param module name of the module containing this function |
227 @param file filename containing this class |
249 @param file filename containing this class |
228 @param lineno linenumber of the class definition |
250 @param lineno linenumber of the class definition |
229 @param idents list of public identifiers |
251 @param idents list of public identifiers |
230 """ |
252 """ |
231 self.module = module |
253 self.module = module |
232 self.name = '__all__' |
254 self.name = "__all__" |
233 self.file = file |
255 self.file = file |
234 self.lineno = lineno |
256 self.lineno = lineno |
235 self.identifiers = [e.replace('"', '').replace("'", "").strip() |
257 self.identifiers = [ |
236 for e in idents.split(',')] |
258 e.replace('"', "").replace("'", "").strip() for e in idents.split(",") |
|
259 ] |
237 |
260 |
238 |
261 |
239 class Imports: |
262 class Imports: |
240 """ |
263 """ |
241 Class to represent the list of imported modules. |
264 Class to represent the list of imported modules. |
242 """ |
265 """ |
|
266 |
243 def __init__(self, module, file): |
267 def __init__(self, module, file): |
244 """ |
268 """ |
245 Constructor |
269 Constructor |
246 |
270 |
247 @param module name of the module containing the import (string) |
271 @param module name of the module containing the import (string) |
248 @param file file name containing the import (string) |
272 @param file file name containing the import (string) |
249 """ |
273 """ |
250 self.module = module |
274 self.module = module |
251 self.name = 'import' |
275 self.name = "import" |
252 self.file = file |
276 self.file = file |
253 self.imports = {} |
277 self.imports = {} |
254 |
278 |
255 def addImport(self, moduleName, names, lineno): |
279 def addImport(self, moduleName, names, lineno): |
256 """ |
280 """ |
257 Public method to add a list of imported names. |
281 Public method to add a list of imported names. |
258 |
282 |
259 @param moduleName name of the imported module (string) |
283 @param moduleName name of the imported module (string) |
260 @param names list of names (list of strings) |
284 @param names list of names (list of strings) |
261 @param lineno line number of the import |
285 @param lineno line number of the import |
262 """ |
286 """ |
263 if moduleName not in self.imports: |
287 if moduleName not in self.imports: |
264 module = ImportedModule(self.module, self.file, moduleName) |
288 module = ImportedModule(self.module, self.file, moduleName) |
265 self.imports[moduleName] = module |
289 self.imports[moduleName] = module |
266 else: |
290 else: |
267 module = self.imports[moduleName] |
291 module = self.imports[moduleName] |
268 module.addImport(lineno, names) |
292 module.addImport(lineno, names) |
269 |
293 |
270 def getImport(self, moduleName): |
294 def getImport(self, moduleName): |
271 """ |
295 """ |
272 Public method to get an imported module item. |
296 Public method to get an imported module item. |
273 |
297 |
274 @param moduleName name of the imported module (string) |
298 @param moduleName name of the imported module (string) |
275 @return imported module item (ImportedModule) or None |
299 @return imported module item (ImportedModule) or None |
276 """ |
300 """ |
277 if moduleName in self.imports: |
301 if moduleName in self.imports: |
278 return self.imports[moduleName] |
302 return self.imports[moduleName] |
279 else: |
303 else: |
280 return None |
304 return None |
281 |
305 |
282 def getImports(self): |
306 def getImports(self): |
283 """ |
307 """ |
284 Public method to get all imported module names. |
308 Public method to get all imported module names. |
285 |
309 |
286 @return dictionary of imported module names with name as key and list |
310 @return dictionary of imported module names with name as key and list |
287 of line numbers of imports as value |
311 of line numbers of imports as value |
288 """ |
312 """ |
289 return self.imports |
313 return self.imports |
290 |
314 |
291 |
315 |
292 class ImportedModule: |
316 class ImportedModule: |
293 """ |
317 """ |
294 Class to represent an imported module. |
318 Class to represent an imported module. |
295 """ |
319 """ |
|
320 |
296 def __init__(self, module, file, importedModule): |
321 def __init__(self, module, file, importedModule): |
297 """ |
322 """ |
298 Constructor |
323 Constructor |
299 |
324 |
300 @param module name of the module containing the import (string) |
325 @param module name of the module containing the import (string) |
301 @param file file name containing the import (string) |
326 @param file file name containing the import (string) |
302 @param importedModule name of the imported module (string) |
327 @param importedModule name of the imported module (string) |
303 """ |
328 """ |
304 self.module = module |
329 self.module = module |
305 self.name = 'import' |
330 self.name = "import" |
306 self.file = file |
331 self.file = file |
307 self.importedModuleName = importedModule |
332 self.importedModuleName = importedModule |
308 self.linenos = [] |
333 self.linenos = [] |
309 self.importedNames = {} |
334 self.importedNames = {} |
310 # dictionary of imported names with name as key and list of line |
335 # dictionary of imported names with name as key and list of line |
311 # numbers as value |
336 # numbers as value |
312 |
337 |
313 def addImport(self, lineno, importedNames): |
338 def addImport(self, lineno, importedNames): |
314 """ |
339 """ |
315 Public method to add a list of imported names. |
340 Public method to add a list of imported names. |
316 |
341 |
317 @param lineno line number of the import |
342 @param lineno line number of the import |
318 @param importedNames list of imported names (list of strings) |
343 @param importedNames list of imported names (list of strings) |
319 """ |
344 """ |
320 if lineno not in self.linenos: |
345 if lineno not in self.linenos: |
321 self.linenos.append(lineno) |
346 self.linenos.append(lineno) |
322 |
347 |
323 for name in importedNames: |
348 for name in importedNames: |
324 if name not in self.importedNames: |
349 if name not in self.importedNames: |
325 self.importedNames[name] = [lineno] |
350 self.importedNames[name] = [lineno] |
326 else: |
351 else: |
327 self.importedNames[name].append(lineno) |
352 self.importedNames[name].append(lineno) |
328 |
353 |
329 |
354 |
330 def readmodule_ex(module, path=None, inpackage=False, isPyFile=False): |
355 def readmodule_ex(module, path=None, inpackage=False, isPyFile=False): |
331 """ |
356 """ |
332 Read a module file and return a dictionary of classes. |
357 Read a module file and return a dictionary of classes. |
333 |
358 |
334 Search for MODULE in PATH and sys.path, read and parse the |
359 Search for MODULE in PATH and sys.path, read and parse the |
335 module and return a dictionary with one entry for each class |
360 module and return a dictionary with one entry for each class |
336 found in the module. |
361 found in the module. |
337 |
362 |
338 @param module name of the module file |
363 @param module name of the module file |
339 @type str |
364 @type str |
340 @param path path the module should be searched in |
365 @param path path the module should be searched in |
341 @type list of str |
366 @type list of str |
342 @param inpackage flag indicating a module inside a package is scanned |
367 @param inpackage flag indicating a module inside a package is scanned |
572 deltastack.append(thisindent - conditionalsstack[-1]) |
606 deltastack.append(thisindent - conditionalsstack[-1]) |
573 deltaindent = reduce(lambda x, y: x + y, deltastack) |
607 deltaindent = reduce(lambda x, y: x + y, deltastack) |
574 deltaindentcalculated = True |
608 deltaindentcalculated = True |
575 thisindent -= deltaindent |
609 thisindent -= deltaindent |
576 else: |
610 else: |
577 while ( |
611 while conditionalsstack and conditionalsstack[-1] >= thisindent: |
578 conditionalsstack and |
|
579 conditionalsstack[-1] >= thisindent |
|
580 ): |
|
581 del conditionalsstack[-1] |
612 del conditionalsstack[-1] |
582 if deltastack: |
613 if deltastack: |
583 del deltastack[-1] |
614 del deltastack[-1] |
584 deltaindentcalculated = False |
615 deltaindentcalculated = False |
585 # remember this class |
616 # remember this class |
586 cur_class = Class(module, class_name, inherit, |
617 cur_class = Class(module, class_name, inherit, file, lineno) |
587 file, lineno) |
|
588 endlineno = calculateEndline(lineno, srcLines, thisindent) |
618 endlineno = calculateEndline(lineno, srcLines, thisindent) |
589 cur_class.setEndLine(endlineno) |
619 cur_class.setEndLine(endlineno) |
590 if not classstack: |
620 if not classstack: |
591 if class_name in dict_counts: |
621 if class_name in dict_counts: |
592 dict_counts[class_name] += 1 |
622 dict_counts[class_name] += 1 |
593 class_name = "{0}_{1:d}".format( |
623 class_name = "{0}_{1:d}".format(class_name, dict_counts[class_name]) |
594 class_name, dict_counts[class_name]) |
|
595 else: |
624 else: |
596 dict_counts[class_name] = 0 |
625 dict_counts[class_name] = 0 |
597 dictionary[class_name] = cur_class |
626 dictionary[class_name] = cur_class |
598 else: |
627 else: |
599 classstack[-1][0]._addclass(class_name, cur_class) |
628 classstack[-1][0]._addclass(class_name, cur_class) |
600 classstack.append((cur_class, thisindent)) |
629 classstack.append((cur_class, thisindent)) |
601 |
630 |
602 elif m.start("Attribute") >= 0: |
631 elif m.start("Attribute") >= 0: |
603 lineno += src.count('\n', last_lineno_pos, start) |
632 lineno += src.count("\n", last_lineno_pos, start) |
604 last_lineno_pos = start |
633 last_lineno_pos = start |
605 index = -1 |
634 index = -1 |
606 while index >= -len(classstack): |
635 while index >= -len(classstack): |
607 if ( |
636 if classstack[index][0] is not None and not isinstance( |
608 classstack[index][0] is not None and |
637 classstack[index][0], Function |
609 not isinstance(classstack[index][0], Function) |
|
610 ): |
638 ): |
611 attr = Attribute( |
639 attr = Attribute(module, m.group("AttributeName"), file, lineno) |
612 module, m.group("AttributeName"), file, lineno) |
|
613 classstack[index][0]._addattribute(attr) |
640 classstack[index][0]._addattribute(attr) |
614 break |
641 break |
615 else: |
642 else: |
616 index -= 1 |
643 index -= 1 |
617 |
644 |
618 elif m.start("Main") >= 0: |
645 elif m.start("Main") >= 0: |
619 # 'main' part of the script, reset class stack |
646 # 'main' part of the script, reset class stack |
620 lineno += src.count('\n', last_lineno_pos, start) |
647 lineno += src.count("\n", last_lineno_pos, start) |
621 last_lineno_pos = start |
648 last_lineno_pos = start |
622 classstack = [] |
649 classstack = [] |
623 |
650 |
624 elif m.start("Variable") >= 0: |
651 elif m.start("Variable") >= 0: |
625 thisindent = _indent(m.group("VariableIndent")) |
652 thisindent = _indent(m.group("VariableIndent")) |
626 variable_name = m.group("VariableName") |
653 variable_name = m.group("VariableName") |
627 lineno += src.count('\n', last_lineno_pos, start) |
654 lineno += src.count("\n", last_lineno_pos, start) |
628 last_lineno_pos = start |
655 last_lineno_pos = start |
629 if thisindent == 0 or not classstack: |
656 if thisindent == 0 or not classstack: |
630 # global variable, reset class stack first |
657 # global variable, reset class stack first |
631 classstack = [] |
658 classstack = [] |
632 |
659 |
633 if "@@Globals@@" not in dictionary: |
660 if "@@Globals@@" not in dictionary: |
634 dictionary["@@Globals@@"] = ClbrBaseClasses.ClbrBase( |
661 dictionary["@@Globals@@"] = ClbrBaseClasses.ClbrBase( |
635 module, "Globals", file, lineno) |
662 module, "Globals", file, lineno |
|
663 ) |
636 dictionary["@@Globals@@"]._addglobal( |
664 dictionary["@@Globals@@"]._addglobal( |
637 Attribute(module, variable_name, file, lineno)) |
665 Attribute(module, variable_name, file, lineno) |
|
666 ) |
638 else: |
667 else: |
639 index = -1 |
668 index = -1 |
640 while index >= -len(classstack): |
669 while index >= -len(classstack): |
641 if classstack[index][1] >= thisindent: |
670 if classstack[index][1] >= thisindent: |
642 index -= 1 |
671 index -= 1 |
643 else: |
672 else: |
644 if isinstance(classstack[index][0], Class): |
673 if isinstance(classstack[index][0], Class): |
645 classstack[index][0]._addglobal( |
674 classstack[index][0]._addglobal( |
646 Attribute(module, variable_name, file, lineno)) |
675 Attribute(module, variable_name, file, lineno) |
|
676 ) |
647 break |
677 break |
648 |
678 |
649 elif m.start("Publics") >= 0: |
679 elif m.start("Publics") >= 0: |
650 idents = m.group("Identifiers") |
680 idents = m.group("Identifiers") |
651 lineno += src.count('\n', last_lineno_pos, start) |
681 lineno += src.count("\n", last_lineno_pos, start) |
652 last_lineno_pos = start |
682 last_lineno_pos = start |
653 pubs = Publics(module, file, lineno, idents) |
683 pubs = Publics(module, file, lineno, idents) |
654 dictionary['__all__'] = pubs |
684 dictionary["__all__"] = pubs |
655 |
685 |
656 elif m.start("Import") >= 0: |
686 elif m.start("Import") >= 0: |
657 #- import module |
687 # - import module |
658 names = [n.strip() for n in |
688 names = [ |
659 "".join(m.group("ImportList").splitlines()) |
689 n.strip() |
660 .replace("\\", "").split(',')] |
690 for n in "".join(m.group("ImportList").splitlines()) |
661 lineno += src.count('\n', last_lineno_pos, start) |
691 .replace("\\", "") |
|
692 .split(",") |
|
693 ] |
|
694 lineno += src.count("\n", last_lineno_pos, start) |
662 last_lineno_pos = start |
695 last_lineno_pos = start |
663 if "@@Import@@" not in dictionary: |
696 if "@@Import@@" not in dictionary: |
664 dictionary["@@Import@@"] = Imports(module, file) |
697 dictionary["@@Import@@"] = Imports(module, file) |
665 for name in names: |
698 for name in names: |
666 dictionary["@@Import@@"].addImport(name, [], lineno) |
699 dictionary["@@Import@@"].addImport(name, [], lineno) |
667 |
700 |
668 elif m.start("ImportFrom") >= 0: |
701 elif m.start("ImportFrom") >= 0: |
669 #- from module import stuff |
702 # - from module import stuff |
670 mod = m.group("ImportFromPath") |
703 mod = m.group("ImportFromPath") |
671 namesLines = (m.group("ImportFromList") |
704 namesLines = ( |
672 .replace("(", "").replace(")", "") |
705 m.group("ImportFromList") |
673 .replace("\\", "") |
706 .replace("(", "") |
674 .strip().splitlines()) |
707 .replace(")", "") |
675 namesLines = [line.split("#")[0].strip() |
708 .replace("\\", "") |
676 for line in namesLines] |
709 .strip() |
677 names = [n.strip() for n in |
710 .splitlines() |
678 "".join(namesLines) |
711 ) |
679 .split(',')] |
712 namesLines = [line.split("#")[0].strip() for line in namesLines] |
680 lineno += src.count('\n', last_lineno_pos, start) |
713 names = [n.strip() for n in "".join(namesLines).split(",")] |
|
714 lineno += src.count("\n", last_lineno_pos, start) |
681 last_lineno_pos = start |
715 last_lineno_pos = start |
682 if "@@Import@@" not in dictionary: |
716 if "@@Import@@" not in dictionary: |
683 dictionary["@@Import@@"] = Imports(module, file) |
717 dictionary["@@Import@@"] = Imports(module, file) |
684 dictionary["@@Import@@"].addImport(mod, names, lineno) |
718 dictionary["@@Import@@"].addImport(mod, names, lineno) |
685 |
719 |
686 elif m.start("ConditionalDefine") >= 0: |
720 elif m.start("ConditionalDefine") >= 0: |
687 # a conditional function/method definition |
721 # a conditional function/method definition |
688 thisindent = _indent(m.group("ConditionalDefineIndent")) |
722 thisindent = _indent(m.group("ConditionalDefineIndent")) |
689 while conditionalsstack and conditionalsstack[-1] >= thisindent: |
723 while conditionalsstack and conditionalsstack[-1] >= thisindent: |
690 del conditionalsstack[-1] |
724 del conditionalsstack[-1] |
691 if deltastack: |
725 if deltastack: |
692 del deltastack[-1] |
726 del deltastack[-1] |
693 conditionalsstack.append(thisindent) |
727 conditionalsstack.append(thisindent) |
694 deltaindentcalculated = False |
728 deltaindentcalculated = False |
695 |
729 |
696 elif m.start("CodingLine") >= 0: |
730 elif m.start("CodingLine") >= 0: |
697 # a coding statement |
731 # a coding statement |
698 coding = m.group("Coding") |
732 coding = m.group("Coding") |
699 lineno += src.count('\n', last_lineno_pos, start) |
733 lineno += src.count("\n", last_lineno_pos, start) |
700 last_lineno_pos = start |
734 last_lineno_pos = start |
701 if "@@Coding@@" not in dictionary: |
735 if "@@Coding@@" not in dictionary: |
702 dictionary["@@Coding@@"] = ClbrBaseClasses.Coding( |
736 dictionary["@@Coding@@"] = ClbrBaseClasses.Coding( |
703 module, file, lineno, coding) |
737 module, file, lineno, coding |
704 |
738 ) |
705 if '__all__' in dictionary: |
739 |
|
740 if "__all__" in dictionary: |
706 # set visibility of all top level elements |
741 # set visibility of all top level elements |
707 pubs = dictionary['__all__'] |
742 pubs = dictionary["__all__"] |
708 for key in dictionary: |
743 for key in dictionary: |
709 if key == '__all__' or key.startswith("@@"): |
744 if key == "__all__" or key.startswith("@@"): |
710 continue |
745 continue |
711 if key in pubs.identifiers: |
746 if key in pubs.identifiers: |
712 dictionary[key].setPublic() |
747 dictionary[key].setPublic() |
713 else: |
748 else: |
714 dictionary[key].setPrivate() |
749 dictionary[key].setPrivate() |
715 del dictionary['__all__'] |
750 del dictionary["__all__"] |
716 |
751 |
717 return dictionary |
752 return dictionary |
718 |
753 |
719 |
754 |
720 def _indent(ws): |
755 def _indent(ws): |
721 """ |
756 """ |
722 Module function to return the indentation depth. |
757 Module function to return the indentation depth. |
723 |
758 |
724 @param ws the whitespace to be checked (string) |
759 @param ws the whitespace to be checked (string) |
725 @return length of the whitespace string (integer) |
760 @return length of the whitespace string (integer) |
726 """ |
761 """ |
727 return len(ws.expandtabs(TABWIDTH)) |
762 return len(ws.expandtabs(TABWIDTH)) |