Repository: edreamleo/make-stub-files (language: python)

Issue:
"None" is not handled properly
Traces do not show None properly, nor do stubs.


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index d8a4adb..48404ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,6 @@
 test/*.pyc
 __pycache__/
 .mypy_cache/
-.cache/
\ No newline at end of file
+.cache/
+.coverage
+htmlcov/
\ No newline at end of file
diff --git a/bug2.py b/bug2.py
deleted file mode 100644
index 61f8dbb..0000000
--- a/bug2.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# Issue #2: https://github.com/edreamleo/make-stub-files/issues/2
-class InvalidTag(Exception):
-    pass
diff --git a/bug2a.py b/bug2a.py
deleted file mode 100644
index 8341717..0000000
--- a/bug2a.py
+++ /dev/null
@@ -1,6 +0,0 @@
-# Issue #2: https://github.com/edreamleo/make-stub-files/issues/2
-
-class NonEmptyClass:
-    
-    def spam():
-        pass
diff --git a/bug3.py b/bug3.py
deleted file mode 100644
index f0869d5..0000000
--- a/bug3.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# Test issue #3: https://github.com/edreamleo/make-stub-files/issues/3
-
-class UnsupportedAlgorithm(Exception):
-    def __init__(self, message: Any, reason: Optional[str]=None) -> None:
-        pass
diff --git a/make_stub_files.leo b/make_stub_files.leo
index 7c13968..517f24c 100644
--- a/make_stub_files.leo
+++ b/make_stub_files.leo
@@ -11,23 +11,7 @@
 <v t="ekr.20180706073424.1"><vh>@bool run-pyflakes-on-write = False</vh></v>
 <v t="ekr.20160207182535.1"><vh>@bool preload-find-pattern = False</vh></v>
 </v>
-<v t="ekr.20160207105647.1"><vh>Scripts</vh>
-<v t="ekr.20160206095102.1"><vh>@button write-unit-tests</vh>
-<v t="ekr.20160206095102.2"><vh>&lt;&lt; docstring &gt;&gt; (write-unit-tests)</vh></v>
-<v t="ekr.20160206101802.3"><vh>class TestWriter</vh>
-<v t="ekr.20160206101802.4"><vh>&lt;&lt; define file_template &gt;&gt;</vh></v>
-<v t="ekr.20160206101802.5"><vh>&lt;&lt; define test_template &gt;&gt;</vh></v>
-<v t="ekr.20160206101802.6"><vh> ctor</vh></v>
-<v t="ekr.20160206101802.7"><vh>clean</vh></v>
-<v t="ekr.20160206101802.8"><vh>get_body</vh></v>
-<v t="ekr.20160206101802.9"><vh>run</vh></v>
-<v t="ekr.20160206103417.1"><vh>plural</vh></v>
-<v t="ekr.20160206101802.10"><vh>test</vh></v>
-<v t="ekr.20160206101802.11"><vh>write_file</vh></v>
-</v>
-</v>
-<v t="ekr.20160207105710.1"><vh>@button check-leading-lines</vh></v>
-</v>
+<v t="ekr.20160207105647.1"><vh>Scripts</vh></v>
 <v t="ekr.20160126153016.3"><vh>Unused</vh>
 <v t="ekr.20160206104033.1"><vh>@@test run all msf unit tests</vh></v>
 <v t="ekr.20160208162138.1"><vh>from st.Call (failed experiment)</vh></v>
@@ -41,15 +25,11 @@
 <v t="ekr.20160214043351.1"><vh>rt.make_optional</vh></v>
 </v>
 </v>
-<v t="ekr.20210803060314.1"><vh>--- others</vh>
-<v t="ekr.20160128104714.1"><vh>Config files</vh>
+<v t="ekr.20160128104714.1"><vh>Files</vh>
 <v t="ekr.20160128225533.1"><vh>@clean .gitignore</vh></v>
 <v t="ekr.20160128102557.1"><vh>@clean example.cfg</vh></v>
-<v t="ekr.20160128104639.1"><vh>@clean msf.cfg</vh></v>
 <v t="ekr.20160126153220.1"><vh>@clean make_stub_files.cfg</vh></v>
-<v t="ekr.20160206165507.1"><vh>@clean test.cfg</vh></v>
-</v>
-<v t="ekr.20160128105006.1"><vh>Documentation</vh>
+<v t="ekr.20160330201030.1"><vh>@clean PKG-INFO.TXT</vh></v>
 <v t="ekr.20160211110739.1"><vh>@clean README.md</vh>
 <v t="ekr.20160211110807.1"><vh>Overview</vh></v>
 <v t="ekr.20160211113019.1"><vh>Quick start</vh></v>
@@ -64,14 +44,13 @@
 <v t="ekr.20160211110811.1"><vh>Summary</vh></v>
 </v>
 <v t="ekr.20160207101607.1"><vh>@clean theory.md</vh></v>
-<v t="ekr.20160330201030.1"><vh>@clean PKG-INFO.TXT</vh></v>
+<v t="ekr.20160318141204.1" descendentVnodeUnknownAttributes="7d71005805000000302e362e3571017d71025808000000616e6e6f7461746571037d710473732e"><vh>@file make_stub_files.py</vh></v>
 </v>
-<v t="ekr.20180706073205.1"><vh>Recent Code</vh>
+<v t="ekr.20180706073205.1"><vh>To do</vh>
 <v t="ekr.20180706071540.1"><vh>#2: Fails to generate valid stubs for empty classes</vh>
 <v t="ekr.20180901050914.1"><vh>Changed...</vh>
 <v t="ekr.20160318141204.186"><vh>st.visit_ClassDef</vh></v>
 <v t="ekr.20160318141204.20"><vh>f.ClassDef (make_stub_files)</vh></v>
-<v t="ekr.20160318141204.131"><vh>msf.scan_command_line</vh></v>
 </v>
 </v>
 <v t="ekr.20180831102536.1"><vh>#3: Creates duplicate annotations for function arguments that are already annotated</vh>
@@ -83,90 +62,22 @@
 <v t="ekr.20160318141204.171"><vh>st.add_stub</vh></v>
 <v t="ekr.20160318141204.110"><vh>pattern.match_balanced</vh></v>
 </v>
-</v>
-<v t="ekr.20160131174909.1"><vh>To do</vh></v>
-</v>
-<v t="ekr.20210803060400.1"><vh>--- old unit tests</vh>
-<v t="ekr.20160318141204.195"><vh>class TestClass</vh>
-<v t="ekr.20160318141204.196"><vh>parse_group (Guido)</vh></v>
-<v t="ekr.20160318141204.197"><vh>return_all</vh></v>
-<v t="ekr.20160318141204.198"><vh>return_array</vh></v>
-<v t="ekr.20160318141204.199"><vh>return_list</vh></v>
-<v t="ekr.20160318141204.200"><vh>return_two_lists (fails)</vh></v>
-</v>
-<v t="ekr.20160206171945.1"><vh>OLDUnit tests</vh>
-<v t="ekr.20160207051429.1"><vh>@test merge_types</vh>
+<v t="ekr.20210805054028.1"><vh>=== #19: revise readme.md</vh></v>
+<v t="ekr.20210805053830.1"><vh>=== #18: full coverage</vh></v>
+<v t="ekr.20210803131439.1"><vh>=== #10, #11, #13: bug and unit tests</vh>
+<v t="ekr.20210804153200.1"><vh>script convert-at-test</vh></v>
+<v t="ekr.20210804104214.1"><vh>--- no longer used</vh>
+<v t="ekr.20160318141204.130"><vh>msf.run_all_unit_tests</vh></v>
+<v t="ekr.20160318141204.129"><vh>msf.run</vh></v>
+<v t="ekr.20160207051429.1"><vh>@@@test merge_types</vh></v>
 <v t="ekr.20160318141204.5"><vh>merge_types (not used)</vh></v>
+<v t="ekr.20160318141204.150"><vh>sf.visit</vh></v>
+<v t="ekr.20210804025215.1"><vh>@@@button backup</vh></v>
 </v>
-<v t="ekr.20160206110004.1"><vh>@test Pattern class</vh>
-<v t="ekr.20160206104634.1"><vh>  utility functions</vh>
-<v t="ekr.20160210083825.1"><vh>dump</vh></v>
-<v t="ekr.20160210141342.1"><vh>dump_dict</vh></v>
-<v t="ekr.20160210141651.1"><vh>dump_list</vh></v>
-<v t="ekr.20160126153212.100"><vh>main</vh></v>
-<v t="ekr.20160206120522.1"><vh>pdb</vh></v>
-<v t="ekr.20160207115604.1"><vh>truncate</vh></v>
-</v>
-<v t="ekr.20160318141204.92"><vh>class LeoGlobals</vh>
-<v t="ekr.20160318141204.93"><vh>class NullObject (Python Cookbook)</vh></v>
-<v t="ekr.20160318141204.94"><vh>g._callerName</vh></v>
-<v t="ekr.20180902035806.1"><vh>g.caller</vh></v>
-<v t="ekr.20160318141204.95"><vh>g.callers</vh></v>
-<v t="ekr.20160318141204.96"><vh>g.cls</vh></v>
-<v t="ekr.20180902073131.1"><vh>g.execute_shell_commands</vh></v>
-<v t="ekr.20160318141204.97"><vh>g.isString &amp; isUnicode (make_stub_files.py)</vh></v>
-<v t="ekr.20180902034437.1"><vh>g.objToSTring &amp; helpers</vh>
-<v t="ekr.20180902041247.1"><vh>g.dictToString</vh></v>
-<v t="ekr.20180902041311.1"><vh>g.listToString</vh></v>
-<v t="ekr.20180902041320.1"><vh>g.tupleToString</vh></v>
-</v>
-<v t="ekr.20160318141204.98"><vh>g.pdb</vh></v>
-<v t="ekr.20180902034446.1"><vh>g.printObj</vh></v>
-<v t="ekr.20160318141204.99"><vh>g.shortFileName</vh></v>
-<v t="ekr.20160318141204.100"><vh>g.splitLines</vh></v>
-<v t="ekr.20160318141204.101"><vh>g.trace</vh></v>
-</v>
-<v t="ekr.20160318141204.102"><vh>class Pattern</vh>
-<v t="ekr.20160318141204.103"><vh>pattern.ctor</vh></v>
-<v t="ekr.20160318141204.104"><vh>pattern.__eq__, __ne__, __hash__</vh></v>
-<v t="ekr.20160318141204.105"><vh>pattern.str &amp; repr</vh></v>
-<v t="ekr.20160318141204.106"><vh>pattern.is_balanced</vh></v>
-<v t="ekr.20160318141204.107"><vh>pattern.is_regex</vh></v>
-<v t="ekr.20160318141204.108"><vh>pattern.all_matches &amp; helpers</vh>
-<v t="ekr.20160318141204.109"><vh>pattern.full_balanced_match</vh></v>
-<v t="ekr.20160318141204.110"></v>
-</v>
-<v t="ekr.20160318141204.111"><vh>pattern.match (trace-matches)</vh></v>
-<v t="ekr.20160318141204.112"><vh>pattern.match_entire_string</vh></v>
-<v t="ekr.20160318141204.113"><vh>pattern.replace &amp; helpers</vh>
-<v t="ekr.20160318141204.114"><vh>pattern.replace_balanced</vh></v>
-<v t="ekr.20160318141204.115"><vh>pattern.replace_regex</vh></v>
-</v>
-</v>
-</v>
-<v t="ekr.20160206170515.1"><vh>@test reduce_numbers</vh>
-<v t="ekr.20160318141204.116"><vh>class ReduceTypes</vh>
-<v t="ekr.20160318141204.117"><vh>rt.ctor</vh></v>
-<v t="ekr.20160318141204.118"><vh>rt.is_known_type</vh></v>
-<v t="ekr.20160318141204.119"><vh>rt.reduce_collection</vh></v>
-<v t="ekr.20160318141204.120"><vh>rt.reduce_numbers</vh></v>
-<v t="ekr.20160318141204.121"><vh>rt.reduce_types</vh></v>
-<v t="ekr.20160318141204.122"><vh>rt.reduce_unknowns</vh></v>
-<v t="ekr.20160318141204.123"><vh>rt.show</vh></v>
-<v t="ekr.20160318141204.124"><vh>rt.split_types</vh></v>
-</v>
-</v>
-<v t="ekr.20160206173343.1"><vh>@test reduce_types</vh>
-<v t="ekr.20160318141204.102"></v>
-<v t="ekr.20160318141204.116"></v>
-</v>
-<v t="ekr.20160207162818.1"><vh>@test split_types</vh>
-<v t="ekr.20160318141204.116"></v>
-</v>
-<v t="ekr.20160210145128.1"><vh>@test st.find_stub &amp; find_parent_stub</vh>
-<v t="ekr.20160203075612.1"><vh> &lt;&lt; imports &gt;&gt; (@test st.find/parent_stub)</vh></v>
-<v t="ekr.20160210164250.1"><vh>&lt;&lt; define s &gt;&gt; (@test st.find/parent_stub)</vh></v>
-<v t="ekr.20160211050346.4"><vh>base classes</vh>
+<v t="ekr.20210804020706.1"><vh>@button coverage</vh></v>
+<v t="ekr.20210804060105.1"><vh>@button unitttest</vh></v>
+<v t="ekr.20210804021331.1"><vh>@button msf</vh></v>
+<v t="ekr.20210804215428.1"><vh>--- Classes</vh>
 <v t="ekr.20160318141204.14"><vh> class AstFormatter</vh>
 <v t="ekr.20160318141204.15"><vh> f.Entries</vh>
 <v t="ekr.20160318141204.17"><vh>f.format</vh></v>
@@ -193,6 +104,7 @@
 <v t="ekr.20160318141204.35"><vh>f.Call &amp; f.keyword</vh>
 <v t="ekr.20160318141204.36"><vh>f.keyword</vh></v>
 </v>
+<v t="ekr.20210804214511.1"><vh>f.Constant</vh></v>
 <v t="ekr.20160318141204.37"><vh>f.comprehension</vh></v>
 <v t="ekr.20160318141204.38"><vh>f.Dict</vh></v>
 <v t="ekr.20160318141204.39"><vh>f.Ellipsis</vh></v>
@@ -251,35 +163,20 @@
 <v t="ekr.20160318141204.89"><vh>f.op_name</vh></v>
 </v>
 </v>
-<v t="ekr.20160318141204.90"><vh>class AstArgFormatter (AstFormatter)</vh>
-<v t="ekr.20160318141204.91"><vh>sf.Constants &amp; Name</vh></v>
-</v>
-<v t="ekr.20160318141204.92"></v>
-<v t="ekr.20160318141204.147"><vh>class StubFormatter (AstFormatter)</vh>
-<v t="ekr.20160318141204.148"><vh>sf.ctor</vh></v>
-<v t="ekr.20160318141204.149"><vh>sf.match_all</vh></v>
-<v t="ekr.20160318141204.150"><vh>sf.visit</vh></v>
-<v t="ekr.20160318141204.151"><vh>sf.trace_visitor</vh></v>
-<v t="ekr.20160318141204.152"><vh>sf.Operands</vh>
-<v t="ekr.20160318141204.153"><vh>sf.Attribute</vh></v>
-<v t="ekr.20160318141204.154"><vh>sf.Constants: Bytes, Num, Str</vh></v>
-<v t="ekr.20160318141204.155"><vh>sf.Dict</vh></v>
-<v t="ekr.20160318141204.156"><vh>sf.List</vh></v>
-<v t="ekr.20160318141204.157"><vh>sf.Name</vh></v>
-<v t="ekr.20160318141204.158"><vh>sf.Tuple</vh></v>
-</v>
-<v t="ekr.20160318141204.159"><vh>sf.Operators</vh>
-<v t="ekr.20160318141204.160"><vh>sf.BinOp</vh></v>
-<v t="ekr.20160318141204.161"><vh>sf.BoolOp</vh></v>
-<v t="ekr.20160318141204.162"><vh>sf.Call &amp; sf.keyword</vh>
-<v t="ekr.20160318141204.163"><vh>sf.keyword</vh></v>
-</v>
-<v t="ekr.20160318141204.164"><vh>sf.Compare</vh></v>
-<v t="ekr.20160318141204.165"><vh>sf.IfExp</vh></v>
-<v t="ekr.20160318141204.166"><vh>sf.Subscript</vh></v>
-<v t="ekr.20160318141204.167"><vh>sf.UnaryOp</vh></v>
-</v>
-<v t="ekr.20160318141204.168"><vh>sf.Return</vh></v>
+<v t="ekr.20160318141204.125"><vh>class Controller</vh>
+<v t="ekr.20160318141204.126"><vh>msf.ctor</vh></v>
+<v t="ekr.20160318141204.127"><vh>msf.finalize</vh></v>
+<v t="ekr.20160318141204.128"><vh>msf.make_stub_file</vh></v>
+<v t="ekr.20160318141204.131"><vh>msf.scan_command_line</vh></v>
+<v t="ekr.20160318141204.132"><vh>msf.scan_options &amp; helpers</vh>
+<v t="ekr.20160318141204.133"><vh>msf.make_op_name_dict</vh></v>
+<v t="ekr.20160318141204.134"><vh>msf.create_parser</vh></v>
+<v t="ekr.20160318141204.135"><vh>msf.find_pattern_ops</vh></v>
+<v t="ekr.20160318141204.136"><vh>msf.get_config_string</vh></v>
+<v t="ekr.20160318141204.137"><vh>msf.init_parser</vh></v>
+<v t="ekr.20160318141204.138"><vh>msf.is_section_name</vh></v>
+<v t="ekr.20160318141204.139"><vh>msf.make_patterns_dict</vh></v>
+<v t="ekr.20160318141204.140"><vh>msf.scan_patterns</vh></v>
 </v>
 </v>
 <v t="ekr.20160318141204.141"><vh>class Stub</vh>
@@ -320,35 +217,54 @@
 </v>
 <v t="ekr.20160318141204.194"><vh>st.visit_Return</vh></v>
 </v>
+<v t="ekr.20210803055042.1"><vh>class TestMakeStubFiles(unittest.TestCase)</vh>
+<v t="ekr.20180901040718.1"><vh>test_bug2_empty</vh></v>
+<v t="ekr.20180901044640.1"><vh>test_bug2_non_empty</vh></v>
+<v t="ekr.20180901051603.1"><vh>test_bug3</vh></v>
+<v t="ekr.20210804103146.1"><vh>test_pattern_class</vh></v>
+<v t="ekr.20210804105256.1"><vh>test_reduce_numbers</vh></v>
+<v t="ekr.20210804111613.1"><vh>test_reduce_types</vh></v>
+<v t="ekr.20210804111803.1"><vh>test_split_types</vh></v>
+<v t="ekr.20210804111915.1"><vh>test_st_find</vh></v>
+<v t="ekr.20210804112211.1"><vh>test_st_flatten_stubs</vh></v>
+<v t="ekr.20210804112405.1"><vh>test_st_merge_stubs</vh>
+<v t="ekr.20210804112405.3"><vh>&lt;&lt; old_stubs &gt;&gt;</vh></v>
+<v t="ekr.20210804112405.4"><vh>&lt;&lt; new_stubs &gt;&gt;</vh></v>
+</v>
+<v t="ekr.20210804112556.1"><vh>test_stub_class</vh></v>
+<v t="ekr.20160207115947.1"><vh>test_truncate</vh></v>
 </v>
-<v t="ekr.20160211050346.1"><vh>@test st.flatten_stubs</vh>
-<v t="ekr.20160211050346.2"><vh> &lt;&lt; imports &gt;&gt; (@test st.flatten_stubs)</vh></v>
-<v t="ekr.20160211050346.3"><vh>&lt;&lt; define s &gt;&gt; (@test st.flatten_stubs)</vh></v>
-<v t="ekr.20160211050346.4"></v>
-<v t="ekr.20160318141204.141"></v>
-<v t="ekr.20160318141204.169"></v>
+<v t="ekr.20160318141204.147"><vh>class StubFormatter (AstFormatter)</vh>
+<v t="ekr.20160318141204.148"><vh>sf.ctor</vh></v>
+<v t="ekr.20160318141204.149"><vh>sf.match_all</vh></v>
+<v t="ekr.20160318141204.151"><vh>sf.trace_visitor</vh></v>
+<v t="ekr.20160318141204.152"><vh>sf.Operands</vh>
+<v t="ekr.20160318141204.153"><vh>sf.Attribute</vh></v>
+<v t="ekr.20160318141204.154"><vh>sf.Constants: Bytes, Num, Str</vh></v>
+<v t="ekr.20160318141204.155"><vh>sf.Dict</vh></v>
+<v t="ekr.20160318141204.156"><vh>sf.List</vh></v>
+<v t="ekr.20160318141204.157"><vh>sf.Name</vh></v>
+<v t="ekr.20160318141204.158"><vh>sf.Tuple</vh></v>
 </v>
-<v t="ekr.20160210080720.1"><vh>@test st.merge_stubs</vh>
-<v t="ekr.20160203075612.1"></v>
-<v t="ekr.20160210081628.1"><vh>&lt;&lt; old_stubs &gt;&gt;</vh></v>
-<v t="ekr.20160210144515.1"><vh>&lt;&lt; new_stubs &gt;&gt;</vh></v>
-<v t="ekr.20160206104634.1"></v>
-<v t="ekr.20160211050346.4"></v>
-<v t="ekr.20160318141204.141"></v>
-<v t="ekr.20160318141204.169"></v>
+<v t="ekr.20160318141204.159"><vh>sf.Operators</vh>
+<v t="ekr.20160318141204.160"><vh>sf.BinOp</vh></v>
+<v t="ekr.20160318141204.161"><vh>sf.BoolOp</vh></v>
+<v t="ekr.20160318141204.162"><vh>sf.Call &amp; sf.keyword</vh>
+<v t="ekr.20160318141204.163"><vh>sf.keyword</vh></v>
 </v>
-<v t="ekr.20160210112945.1"><vh>@test Stub class</vh>
-<v t="ekr.20160203075612.1"></v>
-<v t="ekr.20160318141204.92"></v>
-<v t="ekr.20160318141204.141"></v>
+<v t="ekr.20160318141204.164"><vh>sf.Compare</vh></v>
+<v t="ekr.20160318141204.165"><vh>sf.IfExp</vh></v>
+<v t="ekr.20160318141204.166"><vh>sf.Subscript</vh></v>
+<v t="ekr.20160318141204.167"><vh>sf.UnaryOp</vh></v>
 </v>
-<v t="ekr.20160207115947.1"><vh>@test truncate</vh>
-<v t="ekr.20160207115604.1"></v>
+<v t="ekr.20160318141204.168"><vh>sf.Return</vh></v>
 </v>
-<v t="ekr.20180901040718.1"><vh>@test bug2 empty</vh></v>
-<v t="ekr.20180901044640.1"><vh>@test bug2 non-empty</vh></v>
-<v t="ekr.20180901051603.1"><vh>@test bug3</vh></v>
 </v>
+<v t="ekr.20210804070157.1"><vh>--- command-line</vh>
+<v t="ekr.20160318141204.131"></v>
+<v t="ekr.20160318141204.132"></v>
+</v>
+<v t="ekr.20210803060400.1"><vh>--- testing</vh>
 <v t="ekr.20160630100214.1"><vh>@@@clean test.py</vh></v>
 <v t="ekr.20160128154715.1"><vh>@@@clean test/test_msf.py</vh>
 <v t="ekr.20160128190252.1"><vh>setUp</vh></v>
@@ -356,50 +272,28 @@
 <v t="ekr.20160128190224.1"><vh>test_is_known_type</vh></v>
 </v>
 <v t="ekr.20160206165656.1"><vh>@@@clean test/test.py</vh></v>
+<v t="ekr.20160318141204.195"><vh>class TestClass (not used)</vh>
+<v t="ekr.20160318141204.196"><vh>parse_group (Guido)</vh></v>
+<v t="ekr.20160318141204.197"><vh>return_all</vh></v>
+<v t="ekr.20160318141204.198"><vh>return_array</vh></v>
+<v t="ekr.20160318141204.199"><vh>return_list</vh></v>
+<v t="ekr.20160318141204.200"><vh>return_two_lists (fails)</vh></v>
+</v>
+</v>
+<v t="ekr.20210804161223.1"><vh>--- revised tests</vh>
+<v t="ekr.20180901040718.1"></v>
+<v t="ekr.20180901044640.1"></v>
+<v t="ekr.20180901051603.1"></v>
+</v>
+<v t="ekr.20210805051331.1"><vh>--- the bug fix</vh>
+<v t="ekr.20160318141204.18"></v>
+<v t="ekr.20210804214511.1"></v>
 </v>
-<v t="ekr.20210803060804.1"><vh>To do</vh></v>
-<v t="ekr.20160318141204.30"></v>
-<v t="ekr.20160318141204.147"></v>
-<v t="ekr.20160318141204.14"></v>
-<v t="ekr.20160318141204.21"></v>
-<v t="ekr.20160318141204.11"><vh>main</vh></v>
-<v t="ekr.20210803061104.1"><vh>COPY: leoAst.py:main</vh></v>
-<v t="ekr.20160318141204.125"><vh>class Controller</vh>
-<v t="ekr.20160318141204.126"><vh>msf.ctor</vh></v>
-<v t="ekr.20160318141204.127"><vh>msf.finalize</vh></v>
-<v t="ekr.20160318141204.128"><vh>msf.make_stub_file</vh></v>
-<v t="ekr.20160318141204.129"><vh>msf.run</vh></v>
-<v t="ekr.20160318141204.130"><vh>msf.run_all_unit_tests</vh></v>
-<v t="ekr.20160318141204.131"></v>
-<v t="ekr.20160318141204.132"><vh>msf.scan_options &amp; helpers</vh>
-<v t="ekr.20160318141204.133"><vh>msf.make_op_name_dict</vh></v>
-<v t="ekr.20160318141204.134"><vh>msf.create_parser</vh></v>
-<v t="ekr.20160318141204.135"><vh>msf.find_pattern_ops</vh></v>
-<v t="ekr.20160318141204.136"><vh>msf.get_config_string</vh></v>
-<v t="ekr.20160318141204.137"><vh>msf.init_parser</vh></v>
-<v t="ekr.20160318141204.138"><vh>msf.is_section_name</vh></v>
-<v t="ekr.20160318141204.139"><vh>msf.make_patterns_dict</vh></v>
-<v t="ekr.20160318141204.140"><vh>msf.scan_patterns</vh></v>
 </v>
 </v>
-<v t="ekr.20160318141204.1" descendentVnodeUnknownAttributes="7d71005805000000302e362e3571017d71025808000000616e6e6f7461746571037d710473732e"><vh>@file make_stub_files.py</vh></v>
 </vnodes>
 <tnodes>
 <t tx="ekr.20160126153016.3"></t>
-<t tx="ekr.20160126153212.100">
-def main():
-    '''
-    The driver for the stand-alone version of make-stub-files.
-    All options come from ~/stubs/make_stub_files.cfg.
-    '''
-    # g.cls()
-    controller = StandAloneMakeStubFile()
-    controller.scan_command_line()
-    controller.scan_options()
-    controller.run()
-    if not controller.silent:
-        print('done')
-</t>
 <t tx="ekr.20160126153220.1">@language config
 
 # A configuration file to make stubs for make_stub_files.py itself.
@@ -584,110 +478,8 @@ str%str: str
 # Regex patterns...
 
 .*__name__: str
-</t>
-<t tx="ekr.20160128104639.1">@language config
-
-# A configuration file to make stubs for make_stub_files.py itself.
-
-[Global]
-
-files: make_stub_files.py
-    
-output_directory: .
-    
-prefix_lines:
-    from typing import Any, Dict, Optional, Sequence, Tuple, Union
-        # At present, I don't understand how to tell mypy about ast.Node
-        # import ast
-        # Node = ast.Node
-    Node = Any
-
-[Def Name Patterns]
-
-# The returns are inherent in the design of make_stub_files.py:
-AstFormatter.do_*: str
-StubFormatter.do_*: str
-StubTraverser.do_*: str
-
-# Test of regex pattern.
-.*__hash__$: int
-
-
-# Pattern.all_matches: Sequence
-# Pattern.full_balanced_match: Optional[int]
-# Pattern.match_balanced: int
-# Pattern.match_entire_string: bool
-# StandAloneMakeStubFile.scan_types: Dict[str, str]
-
-# StubTraverser.format_returns: str
-# StubTraverser.match_return_patterns: Tuple[bool,str]
-# StubTraverser.match_return_pattern: Optional[str]
-# StubTraverser.match_balanced: int
-
-[General Patterns]
-
-# Test of regex patterns
-.*\.endswith\(.*\)$: bool
-
-# Declarations of names...
-NotImplemented: bool
-a: str
-aList: List[Any]
-comments: str
-controller: StandAloneMakeStubFile
-find_s: str
-fn: str
-found: str
-general_patterns: List[Pattern]
-group: str
-i1: int
-i2: int
-i: int
-j1: int
-j2: int
-j: int
-n1: int
-n2: int
-n: int
-name: str
-ndots: int
-node: Node
-parser: optparse.OptionParser
-patterns: List
-repl_s: str
-s: str
-s1: str
-s2: str
-# s[1-2]?$: str
-strict: bool
-trace: bool
-
-# Known functions...
-all(*): bool
-any(*): bool
-int(*): int
-hash(*): int
-len(*): int
-os.path.basename(*): str
-os.sep.join(*): str
-repr(*): str
-sep.join(*): str
-self.__eq__(*): bool
-self.__ne__(*): bool
-self.__gt__(*): bool
-self.__lt__(*): bool
-sorted(*): str
-str%(*): str
-str.endswith(*): bool 
-str.join(*): str
-str.startswith(*): bool 
-r[*]: str
-# Put this in the code.
-str[*]: str
-
 </t>
 <t tx="ekr.20160128104714.1"></t>
-<t tx="ekr.20160128105006.1"></t>
 <t tx="ekr.20160128154715.1">import pdb
 import unittest
 import make_stub_files as msf
@@ -770,25 +562,17 @@ test/*.pyc
 __pycache__/
 .mypy_cache/
 .cache/
-</t>
-<t tx="ekr.20160131174909.1">@language rest
-@wrap
-
-None of the following are very important because the user can always add
-more patterns.
-
-- Add more hard-coded patterns, especially binop patterns.
-- Rough edges re number types. Is reduce_numbers correct?
-- How are non-comment lines between subs updated?
+.coverage
+htmlcov/
 </t>
 <t tx="ekr.20160202061118.1">
 seen_patterns = []
 
 def visit(self, node):
-    '''
+    """
     Return the formatted version of an Ast node after
     applying all general patterns.
-    '''
+    """
     # This is the heart of this script.
     trace = False
     s = AstFormatter.visit(self, node)
@@ -814,7 +598,7 @@ def visit(self, node):
 </t>
 <t tx="ekr.20160202064553.1">
 def match(self, patterns, s):
-    '''Return s with at most one pattern matched.'''
+    """Return s with at most one pattern matched."""
     assert False, g.callers()
     for pattern in patterns:
         found, s = pattern.match(s)
@@ -822,305 +606,15 @@ def match(self, patterns, s):
             break
     return s
 </t>
-<t tx="ekr.20160203075612.1">import ast
-from collections import OrderedDict
-    # Requires Python 2.7 or above. Without OrderedDict
-    # the configparser will give random order for patterns.
-try:
-    import ConfigParser as configparser # Python 2
-except ImportError:
-    import configparser # Python 3
-import glob
-import optparse
-import os
-import re
-import sys
-import time
-try:
-    import StringIO as io # Python 2
-except ImportError:
-    import io # Python 3
-
-</t>
-<t tx="ekr.20160206095102.1">&lt;&lt; docstring &gt;&gt;
-# **Note**: this is a Leo script.
-# It **does** have access to c, g and p.
-import os
-import re
-@others
-test_dir = os.path.dirname(c.fileName())+os.sep+'test'
-assert os.path.exists(test_dir), test_dir
-assert os.path.isdir(test_dir), test_dir
-
-if 1:
-    # Writes each test to a separate file in the test directory.
-    TestWriter(c,path=test_dir).run(fn=None)    
-if 0:
-    # Writes all tests to a single file: test/unit_tests.py
-    TestWriter(c,path=test_dir).run(fn='unit_tests.py')
-</t>
-<t tx="ekr.20160206095102.2">@
-@language rest
-'''
-This script transliterates @test nodes into .py file. The two main ways of
-using this script are as follows::
-
-    TestWriter(c,path='test').run(fn='unit_tests.py') # writes one file
-    TestWriter(c,path='test').run(fn=None)            # writes separate files.
-     
-The first writes all tests to test/unit_tests.py; the second writes each
-unit test to a separate .py file in the test directory.
-
-The script imports each written file and reports any syntax errors.
-
-This is a straightforward script; it should be easy to modify it to suit
-individual needs.
-
-The &lt;\&lt; file_template &gt;&gt; and &lt;\&lt; test_template &gt;&gt; sections in the TestWriter
-class determines exactly what this script writes.
-'''
-</t>
-<t tx="ekr.20160206101802.10">
-def test(self,fn):
-    '''Test the newly created file.'''
-    import imp
-    import sys
-
-    if self.path not in sys.path:
-        sys.path.append(self.path)
-    assert fn.endswith('.py')
-    name = fn[:-3]
-    try:
-        f,path,desc = imp.find_module(name,[self.path])
-        imp.load_module(name,f,path,desc)
-        # print('imported %s' % (name))
-    except Exception:
-        print('can not import: %s' % (name))
-        g.es_print_exception()
-</t>
-<t tx="ekr.20160206101802.11">
-def write_file(self,fn):
-
-    assert g.os_path_exists(self.path),self.path
-    fn = g.os_path_finalize_join(self.path,fn)
-    f = open(fn,'w')
-    f.write(self.file_template)
-    # g.trace(''.join([z.h for z in self.nodes]))
-    for p in self.nodes:
-        f.write(self.test_template % (self.clean(p.h),self.get_body(p)))
-    f.close()
-    g.trace('wrote', fn)
-</t>
-<t tx="ekr.20160206101802.3">
-
-class TestWriter:
-    
-    &lt;&lt; define file_template &gt;&gt;
-    &lt;&lt; define test_template &gt;&gt;
-
-    @others
-</t>
-<t tx="ekr.20160206101802.4"># Add any other common imports here.
-
-file_template = '''\
-import unittest
-from make_stub_files import *
-'''
-
-file_template = g.adjustTripleString(file_template,c.tab_width)
-</t>
-<t tx="ekr.20160206101802.5">test_template = '''
-class %s (unittest.TestCase):
-    def runTest(self):
-%s
-'''
-
-test_template = g.adjustTripleString(test_template,c.tab_width)
-</t>
-<t tx="ekr.20160206101802.6">
-def __init__(self,c,path=''):
-    '''TestWriter ctor.'''
-    self.c = c
-    load_dir = g.os_path_dirname(c.fileName())
-    self.path = g.os_path_finalize_join(load_dir,path)
-    self.nodes = []
-    assert g.os_path_exists(self.path),self.path
-</t>
-<t tx="ekr.20160206101802.7">
-def clean(self,s):
-    '''Munge s so that it can be used as a file name.'''
-    result,tag = [],'@test'
-    if s.startswith(tag):
-        s = s[len(tag):]
-    for ch in s.strip():
-        if ch.isalnum():
-            result.append(ch)
-        else:
-            result.append('_')
-        # elif ch.isspace():
-            # result.append('_')
-    s = ''.join(result)
-    if s.endswith('.py'):
-        s = s[:-3]
-    if not s.startswith('test'):
-        s = 'test_' + s
-    return s.replace('__','_').strip()
-</t>
-<t tx="ekr.20160206101802.8">
-def get_body(self, p):
-    '''Convert p.b to a valid script.'''
-    s_old = p.b
-    # Suppress @others but not section references.
-    p.b = p.b.replace('@others', '')
-    assert p.b.find('@others') == -1
-    s = g.getScript(c, p,
-                    useSelectedText=False,
-                    forcePythonSentinels=True,
-                    useSentinels=False)
-    p.b = s_old
-    s = ''.join([' '*8+z for z in g.splitLines(s) if z.strip()])
-            # Add leading indentation needed by test_template.
-    return s.rstrip()+'\n'
-</t>
-<t tx="ekr.20160206101802.9">
-def run(self,fn=None):
-    
-    n, p = 0, c.rootPosition()
-    while p:
-        if p.h.startswith('@ignore '):
-            p.moveToNodeAfterTree()
-        elif p.h.startswith('@test '):
-            self.nodes.append(p.copy())
-            if not fn:
-                fn2 = self.clean(p.h)+'.py'
-                self.write_file(fn2)
-                self.test(fn2)
-                self.nodes=[]
-            n += 1
-            p.moveToThreadNext()
-        else:
-            p.moveToThreadNext()
-    if n == 0:
-        print('no @file or @suite nodes found.')
-    else:
-        if fn:
-            self.write_file(fn)
-            self.test(fn)
-        dest = g.os_path_join(self.path, fn) if fn else self.path
-        print('wrote %s test%s to %s' % (n,self.plural(n), dest))
-</t>
-<t tx="ekr.20160206103417.1">
-def plural(self, n):
-    return 's' if n &gt; 1 else ''
-</t>
 <t tx="ekr.20160206104033.1"># pylint: disable=relative-import
-g.cls()
-import test_msf
-import unittest
-loader = unittest.TestLoader()
-suite = loader.loadTestsFromTestCase(test_msf.TestMakeStubFiles)
-unittest.TextTestRunner(verbosity=0).run(suite)
-</t>
-<t tx="ekr.20160206104634.1">
-# Top-level functions</t>
-<t tx="ekr.20160206110004.1">@others
-import re
-g = LeoGlobals() # Use the g available to the script.
-table = (
-    # s,  Pattern.find_s, Pattern.repl_s, expected
-    # Passed...
-    ('aabbcc', '(a+)(b+)(c+)$', r'\3\2\1', 'ccbbaa'),
-    ('[str]', r'\[str\]$', 'xxx', 'xxx'), # Guido bug.
-    ('s3', r's[1-3]?\b$', 'str', 'str'), # lengthening bug.
-    ('s', 's', 'str', 'str'),
-    ('abc', 'abc', 'ABC', 'ABC'),
-    ('str(str)', 'str(*)', 'str', 'str'),
-    ('[whatever]', '[*]', 'List[*]', 'List[whatever]'), # * on the RHS.
-    ('(int,str)', '(*)', 'Tuple[*]', 'Tuple[int,str]'), # Guido bug 2.
-    ('abcxyz', 'abc*', 'xxx', 'xxx'), # New test for trailing *.
-    ('list(self.regex.finditer(str))','list(*)','List[*]',
-     'List[self.regex.finditer(str)]'),
-)
-for s, find, repl, expected in table:
-    pattern = Pattern(find, repl)
-    result = pattern.match_entire_string(s)
-    assert result, (result, s, find, repl, expected)
-    aList = pattern.all_matches(s)
-    assert len(aList) == 1, aList
-    found, s2 = pattern.match(s)
-    assert found, 'after pattern.match(s)'
-    assert s2 == expected, (s, pattern, 'expected', expected, 'got', s2)
-p1 = Pattern('abc','xyz')
-p2 = Pattern('abc','xyz')
-p3 = Pattern('abc','pdq')
-assert p1 == p2
-assert p1 != p3
-assert p2 != p3
-aSet = set()
-aSet.add(p1)
-assert p1 in aSet
-assert p2 in aSet
-assert p3 not in aSet
-assert list(aSet) == [p1] == [p2]
-aSet.add(p3)
-assert p1.match_entire_string('abc')
-assert not p1.match_entire_string('abcx')
-</t>
-<t tx="ekr.20160206120522.1">
-def pdb(self):
-    '''Invoke a debugger during unit testing.'''
-    try:
-        import leo.core.leoGlobals as leo_g
-        leo_g.pdb()
-    except ImportError:
-        import pdb
-        pdb.set_trace()
-</t>
-<t tx="ekr.20160206165507.1">@language config
-
-# A configuration file to make stubs for make_stub_files.py itself.
-
-[Global]
-
-files: test/test.py
-output_directory: ./test
-# prefix_lines:
-
-[Def Name Patterns]
-
-[General Patterns]
-
-# Declarations of names..
-a: Any
-s: str
-aList: List[Any]
-is_known_type(*): bool
-
-# Known functions...
-all(*): bool
-any(*): bool
-endswith(*): bool
-g.match_word(*): bool
-hash(*): int
-hasattr(*): bool
-int(*): int
-len(*): int
-os.path.basename(*): str
-os.sep.join(*): str
-repr(*): str
-sep.join(*): str
-self.__eq__(*): bool
-self.__ne__(*): bool
-self.__gt__(*): bool
-self.__lt__(*): bool
-sorted(*): str
-str%(*): str
-str.join(*): str
-str.isspace(*): str
-str[*]: str
-sum(*): number
+g.cls()
+import test_msf
+import unittest
+loader = unittest.TestLoader()
+suite = loader.loadTestsFromTestCase(test_msf.TestMakeStubFiles)
+unittest.TextTestRunner(verbosity=0).run(suite)
 </t>
-<t tx="ekr.20160206165656.1">'''A test file containing code that caused mypy errors.'''
+<t tx="ekr.20160206165656.1">"""A test file containing code that caused mypy errors."""
 
 def is_known_type(s):
     '''
@@ -1251,76 +745,6 @@ def optional_any2(a):
     else:
         return
 </t>
-<t tx="ekr.20160206170515.1">a, c, f, i, l, n = ('Any', 'complex', 'float', 'int', 'long', 'number')
-table = (
-    ([i,i],     [i]),
-    ([i],       [i]),
-    ([f, i],    [f]),
-    ([c, i],    [c]),
-    ([l, a],    [a, l]),
-)
-@others
-for aList, expected in table:
-    got = ReduceTypes().reduce_numbers(aList)
-    assert expected == got,  (aList, 'expected:', expected, 'got', got)
-</t>
-<t tx="ekr.20160206171945.1">@language rest
-
-To run unit tests from top-level directory:
-    
-1. Make sure to run the write-unit-tests script after making any changes.
-
-2. python -m unittest discover test
-
-http://leoeditor.com/unitTesting.html discusses unit testing in Leo.
-
-http://leoeditor.com/tutorial-scripting.html discusses basic Leo programming,
-including the the predefined vars c, g and p.
-
-*How to run unit tests from this file*:
-
-    Alt-6 runs *all* @test nodes.
-    Alt-5 runs all *marked* @test nodes.
-    Alt-4 runs the selected @test node.
-
-I typically use Alt-5 when working a long time on a particular test.
-Marking just that @test node allows me to run the test from any node.
-
-@language python
-</t>
-<t tx="ekr.20160206173343.1">a, c, f, i, l, n = ('Any', 'complex', 'float', 'int', 'long', 'number')
-none = 'None'
-x = 'xyzzy'
-y = 'pdq'
-table = (
-    ([i,i],         i),
-    ([i],           i),
-    ([f, i],        f),
-    ([c, i],        c),
-    ([l, a],        'Union[Any, long]'),
-    # Handle None
-    ([None],        none),
-    ([None, None],  none),
-    ([None, a, c],  'Optional[Union[Any, complex]]'),
-    # Handle unknown types, and special cases
-    ([i, x],        'Union[Any, int]'),
-    ([None, x],     'Optional[Any]'),
-    ([none, x],     'Optional[Any]'),
-    (['', x],       'Optional[Any]'),
-    ([none, x, c],  'Optional[Union[Any, complex]]'),
-    ([x, y],        'Any'),
-    # Collection merging.  More could be done...
-    (['Dict[int, str]', 'Dict[Any, str]'],          'Union[Dict[Any, str], Dict[int, str]]'),
-    (['List[int, str]', 'List[Any, str]'],          'Union[List[Any, str], List[int, str]]'),
-    (['Union[int, str]', 'Union[Any, str]'],        'Union[Union[Any, str], Union[int, str]]'),
-    (['Union[int, str]', 'int', 'Union[Any, str]'], 'Union[Union[Any, str], Union[int, str], int]'),
-    (['Tuple[xyz, pdq]'],                           'Tuple[Any, Any]'),
-)
-@others
-for aList, expected in table:
-    got = ReduceTypes(aList).reduce_types()
-    assert expected == got, '\naList:    %s\nexpected: %s\ngot:      %s' % (aList, expected, got)
-</t>
 <t tx="ekr.20160207051429.1">a, c, f, i, l, n = ('Any', 'complex', 'float', 'int', 'long', 'number')
 none = 'None'
 La, Lc = ['Any'], ['complex']
@@ -1394,7 +818,7 @@ The previous section is really you should need to know about this program.  Howe
 Given the file truncate.py:
 
     def truncate(s, n):
-        '''Return s truncated to n characters.'''
+        """Return s truncated to n characters."""
         return s if len(s) &lt;= n else s[:n-3] + '...'
         
 The script produces this output with the --verbose option in effect:
@@ -1492,41 +916,17 @@ edreamleo@gmail.com
 (608) 886-5730
 </t>
 <t tx="ekr.20160207105647.1"></t>
-<t tx="ekr.20160207105710.1">g.cls()
-for p1 in c.all_unique_positions():
-    if p1.h.startswith('@clean'):
-        for p in p1.subtree():
-            if (not p.h.strip().startswith('&lt;&lt;') and
-                p.b.strip() and not p.b.startswith('\n')
-            ):
-                print(repr(p.b[:3]), p.h)
-print('done')
-</t>
-<t tx="ekr.20160207115604.1">
-def truncate(s, n):
-    '''Return s truncated to n characters.'''
-    return s if len(s) &lt;= n else s[:n-3] + '...'
-</t>
-<t tx="ekr.20160207115947.1">table = (
-    ('abc',     'abc'),
-    ('abcd',    'abcd'),
-    ('abcde',   'abcde'),
-    ('abcdef',  'ab...'),
-    ('abcdefg', 'ab...'),
-)
-@others  
-for s1, s2 in table:
-    got = truncate(s1, 5)
-    assert s2 == got, (s1, 'expected', s2, 'got', got)</t>
-<t tx="ekr.20160207162818.1">table = (
-    ('list',                    ['list']),
-    ('List[a,b]',               ['List[a,b]']),
-    ('List[a,b], List[c,d]',    ['List[a,b]', 'List[c,d]']),
-)
-@others
-for s, expected in table:
-    got = ReduceTypes().split_types(s)
-    assert expected == got, (s, 'expected', expected, 'got', got)
+<t tx="ekr.20160207115947.1">def test_truncate(self):
+    table = (
+        ('abc',     'abc'),
+        ('abcd',    'abcd'),
+        ('abcde',   'abcde'),
+        ('abcdef',  'ab...'),
+        ('abcdefg', 'ab...'),
+    )
+    for s1, s2 in table:
+        got = truncate(s1, 5)
+        self.assertEqual(s2, got, msg=f"s1: {s1!r}")
 </t>
 <t tx="ekr.20160207181637.1"></t>
 <t tx="ekr.20160207181648.1"></t>
@@ -1544,222 +944,6 @@ for s, expected in table:
             if found:
                 if trace: g.trace('%s: %s -&gt; %s' % (pattern.find_s, name, s))
                 return s</t>
-<t tx="ekr.20160210080720.1">'''Test framework for st.update and helpers.'''
-# To do:
-# - Test between-stub lines and leading lines.
-# - Round-trip tests!
-&lt;&lt; imports &gt;&gt;
-&lt;&lt; old_stubs &gt;&gt;
-&lt;&lt; new_stubs &gt;&gt;
-@others
-g = LeoGlobals() # Use the g available to the script.
-# g.cls()
-st = StubTraverser(controller=g.NullObject())
-# dump('old_s', old_s)
-# dump('new_s', new_s)
-old_d, old_root = st.parse_stub_file(old_s, root_name='&lt;old-root&gt;')
-new_d, new_root = st.parse_stub_file(new_s, root_name='&lt;new-root&gt;')
-if 0:
-    dump_dict('old_d', old_d)
-    dump_dict('new_d', new_d)
-    print(st.trace_stubs(old_root, header='trace_stubs(old_root)'))
-    print(st.trace_stubs(new_root, header='trace_stubs(new_root)'))
-if 0: # separate unit test. Passed.
-    aList = st.sort_stubs_by_hierarchy(new_root)
-    dump_list(aList, 'after sort_stubs_by_hierarcy')
-new_stubs = new_d.values()
-st.merge_stubs(new_stubs, old_root, new_root, trace=False)
-if 0:
-    print(st.trace_stubs(old_root, header='trace_stubs(old_root)'))
-</t>
-<t tx="ekr.20160210081628.1"># To be INSERTED (They exist in new stubs, but not here.)
-# def is_known_type(s: str) -&gt; Union[Any,bool]: ...
-# def reduce_numbers(aList: List[Any]) -&gt; List[Any]: ...
-# class AstFormatter:
-    # def format(self, node: Node) -&gt; Union[Any,str]: ...
-    # def visit(self, node: Node) -&gt; str: ...
-    # def do_ClassDef(self, node: Node) -&gt; str: ...
-    # def do_FunctionDef(self, node: Node) -&gt; str: ...
-old_s = '''\
-def main() -&gt; None: ...
-def merge_types(a1: Any, a2: Any) -&gt; str: ...
-def pdb(self) -&gt; None: ...
-def reduce_types(aList: List[Any], name: str=None, trace: bool=False) -&gt; Any: ...
-class Pattern(object):
-    def __init__(self, find_s: str, repl_s: str='') -&gt; None: ...
-    def __eq__(self, obj: Any) -&gt; bool: ...
-    def __ne__(self, obj: Any) -&gt; bool: ...
-    def __hash__(self) -&gt; int: ...
-    def __repr__(self) -&gt; str: ...
-    def is_balanced(self) -&gt; bool: ...
-    def is_regex(self) -&gt; Any: ...
-        #   0: return self.find_s.endswith('$')
-        # ? 0: return self.find_s.endswith(str)
-'''
-</t>
-<t tx="ekr.20160210083825.1">
-def dump(title, s=None):
-    if s:
-        print('===== %s...\n%s\n' % (title, s.rstrip()))
-    else:
-        print('===== %s...\n' % title)
-</t>
-<t tx="ekr.20160210112945.1">&lt;&lt; imports &gt;&gt;
-@others
-g = LeoGlobals() # Use the g available to the script.
-# g.cls()
-# Test equality...
-stub1 = Stub(kind='def', name='foo')
-stub2 = Stub(kind='class', name='foo')
-stub3 = Stub(kind='def', name='bar')
-stub4 = Stub(kind='def', name='foo')
-stub4.out_list = ['xyzzy']
-    # Contents of out_list must not affect equality!
-aList = [stub1, stub3]
-assert stub1 != stub2
-assert stub1 != stub3
-assert stub1 == stub4
-assert stub1 in aList
-assert stub2 not in aList
-assert stub3 in aList
-# Test __hash__
-d = {stub1: 'stub1'}
-assert stub1 in d
-assert stub2 not in d
-# Test parents and level.
-stub_1 = Stub(kind='def', name='stub_1')
-stub_2 = Stub(kind='def', name='stub_2', parent=stub_1, stack=['stub_1'])
-stub_3 = Stub(kind='def', name='stub_3', parent=stub_2, stack=['stub_1', 'stub_2'])
-assert stub_1.parents() == [], stub_1.parents()
-assert stub_2.parents() == ['stub_1'], stub_2.parents()
-assert stub_3.parents() == ['stub_1', 'stub_2'], stub_3.parents()
-assert stub_1.level() == 0
-assert stub_2.level() == 1
-assert stub_3.level() == 2
-</t>
-<t tx="ekr.20160210141342.1">
-def dump_dict(title, d):
-    '''Dump a dictionary with a header.'''
-    dump(title)
-    for z in sorted(d):
-        print('%30s %s' % (z, d.get(z)))
-    print('')</t>
-<t tx="ekr.20160210141651.1">
-def dump_list(title, aList):
-    '''Dump a list with a header.'''
-    dump(title)
-    for z in aList:
-        print(z)
-    print('')
-</t>
-<t tx="ekr.20160210144515.1"># To be DELETED (They exist in old_stubs, but not here)
-# class Pattern(object):
-    # def __init__(self, find_s: str, repl_s: str='') -&gt; None: ...
-    # def __eq__(self, obj: Any) -&gt; bool: ...
-    # def __ne__(self, obj: Any) -&gt; bool: ...
-    # def __hash__(self) -&gt; int: ...
-    # def __repr__(self) -&gt; str: ...
-    # def is_balanced(self) -&gt; bool: ...
-    # def is_regex(self) -&gt; Any: ...
-        # #   0: return self.find_s.endswith('$')
-        # # ? 0: return self.find_s.endswith(str)
-new_s = '''\
-def is_known_type(s: str) -&gt; Union[Any,bool]: ...
-def main() -&gt; None: ...
-def merge_types(a1: Any, a2: Any) -&gt; str: ...
-def pdb(self) -&gt; None: ...
-def reduce_numbers(aList: List[Any]) -&gt; List[Any]: ...
-def reduce_types(aList: List[Any], name: str=None, trace: bool=False) -&gt; Any: ...
-
-class AstFormatter:
-    def format(self, node: Node) -&gt; Union[Any,str]: ...
-    def visit(self, node: Node) -&gt; str: ...
-    def do_ClassDef(self, node: Node) -&gt; str: ...
-    def do_FunctionDef(self, node: Node) -&gt; str: ...
-'''</t>
-<t tx="ekr.20160210145128.1">&lt;&lt; imports &gt;&gt;
-&lt;&lt; define s &gt;&gt;
-@others
-g = LeoGlobals() # Use the g available to the script.
-st = StubTraverser(controller=g.NullObject())
-d, root = st.parse_stub_file(s, root_name='&lt;root&gt;')
-    # Root *is* used below.
-if 0:
-    print(st.trace_stubs(root, header='root'))
-stub1 = Stub(kind='class', name='AstFormatter')
-stub2 = Stub(kind='def', name='format', parent=stub1, stack=['AstFormatter'])
-stub3 = Stub(kind='def', name='helper', parent = stub2, stack=['AstFormatter', 'format'])
-# stub4 = Stub(kind='def', name='main')
-for stub in (stub1, stub2, stub3,): # (stub1, stub2, stub3):
-    found = st.find_stub(stub, root)
-    id_found = found and id(found) or None
-    if 0:
-        print('found  %s =&gt; %9s %35s ==&gt; %s' % (id(stub), id_found, stub, found))
-    found = st.find_parent_stub(stub, root)
-    id_found = found and id(found) or None
-    if 0:
-        print('parent %s =&gt; %9s %35s ==&gt; %s' % (id(stub), id_found, stub, found))</t>
-<t tx="ekr.20160210164250.1">s = '''\
-def is_known_type(s: str) -&gt; Union[Any,bool]: ...
-def main() -&gt; None: ...
-def merge_types(a1: Any, a2: Any) -&gt; str: ...
-
-class AstFormatter:
-    def format(self, node: Node) -&gt; Union[Any,str]: ...
-        def helper(self): -&gt; None
-    def visit(self, node: Node) -&gt; str: ...
-    def do_ClassDef(self, node: Node) -&gt; str: ...
-    def do_FunctionDef(self, node: Node) -&gt; str: ...
-'''</t>
-<t tx="ekr.20160211050346.1">&lt;&lt; imports &gt;&gt;
-&lt;&lt; define s &gt;&gt;
-@others
-g = LeoGlobals() # Use the g available to the script.
-st = StubTraverser(controller=g.NullObject())
-d, root = st.parse_stub_file(s, root_name='&lt;root&gt;')
-if 0:
-    print(st.trace_stubs(root, header='root'))
-aList = st.flatten_stubs(root)
-assert aList
-if 0:
-    for i, stub in enumerate(aList):
-        print('%2s %s' % (i, stub))
-for stub in aList:
-    found = st.find_stub(stub, root)
-    assert found, stub
-</t>
-<t tx="ekr.20160211050346.2">import ast
-from collections import OrderedDict
-    # Requires Python 2.7 or above. Without OrderedDict
-    # the configparser will give random order for patterns.
-try:
-    import ConfigParser as configparser # Python 2
-except ImportError:
-    import configparser # Python 3
-import glob
-import optparse
-import os
-import re
-import sys
-import time
-try:
-    import StringIO as io # Python 2
-except ImportError:
-    import io # Python 3
-</t>
-<t tx="ekr.20160211050346.3">s = '''\
-def is_known_type(s: str) -&gt; Union[Any,bool]: ...
-def main() -&gt; None: ...
-def merge_types(a1: Any, a2: Any) -&gt; str: ...
-
-class AstFormatter:
-    def format(self, node: Node) -&gt; Union[Any,str]: ...
-        def helper(self): -&gt; None
-    def visit(self, node: Node) -&gt; str: ...
-    def do_ClassDef(self, node: Node) -&gt; str: ...
-    def do_FunctionDef(self, node: Node) -&gt; str: ...
-'''</t>
-<t tx="ekr.20160211050346.4"></t>
 <t tx="ekr.20160211110739.1">@language rest
 @wrap
 
@@ -2098,148 +1282,11 @@ def make_optional(self,r):
     g.trace(aList, result)
     return result
 </t>
-<t tx="ekr.20160318141204.100">def splitLines(self, s):
-    '''Split s into lines, preserving trailing newlines.'''
-    return s.splitlines(True) if s else []
-</t>
-<t tx="ekr.20160318141204.101">def trace(self, *args, **keys):
-
-    # Compute the caller name.
-    try:
-        f1 = sys._getframe(1)
-        code1 = f1.f_code
-        name = code1.co_name
-    except Exception:
-        name = ''
-    print('%s: %s' % (name, ' '.join(str(z) for z in args)))
-
-    
-</t>
-<t tx="ekr.20160318141204.102">
-
-class Pattern:
-    '''
-    A class representing regex or balanced patterns.
-    
-    Sample matching code, for either kind of pattern:
-        
-        for m in reversed(pattern.all_matches(s)):
-            s = pattern.replace(m, s)
-    '''
-    @others
-</t>
-<t tx="ekr.20160318141204.103">def __init__(self, find_s, repl_s=''):
-    '''Ctor for the Pattern class.'''
-    self.find_s = find_s
-    self.repl_s = repl_s
-    if self.is_regex():
-        self.regex = re.compile(find_s)
-    elif self.is_balanced():
-        self.regex = None
-    else:
-        # Escape all dangerous characters.
-        result = []
-        for ch in find_s:
-            if ch == '_' or ch.isalnum():
-                result.append(ch)
-            else:
-                result.append('\\' + ch)
-        self.regex = re.compile(''.join(result))
-</t>
-<t tx="ekr.20160318141204.104">def __eq__(self, obj):
-    """Return True if two Patterns are equivalent."""
-    if isinstance(obj, Pattern):
-        return self.find_s == obj.find_s and self.repl_s == obj.repl_s
-    return NotImplemented
-
-def __ne__(self, obj):
-    """Return True if two Patterns are not equivalent."""
-    return not self.__eq__(obj)
-
-def __hash__(self):
-    '''Pattern.__hash__'''
-    return len(self.find_s) + len(self.repl_s)
-</t>
-<t tx="ekr.20160318141204.105">def __repr__(self):
-    '''Pattern.__repr__'''
-    return '%s: %s' % (self.find_s, self.repl_s)
-
-__str__ = __repr__
-</t>
-<t tx="ekr.20160318141204.106">def is_balanced(self):
-    '''Return True if self.find_s is a balanced pattern.'''
-    s = self.find_s
-    if s.endswith('*'):
-        return True
-    for pattern in ('(*)', '[*]', '{*}'):
-        if s.find(pattern) &gt; -1:
-            return True
-    return False
-</t>
-<t tx="ekr.20160318141204.107">def is_regex(self):
-    '''
-    Return True if self.find_s is a regular pattern.
-    For now a kludgy convention suffices.
-    '''
-    return self.find_s.endswith('$')
-        # A dollar sign is not valid in any Python expression.
-</t>
-<t tx="ekr.20160318141204.108">def all_matches(self, s):
-    '''
-    Return a list of match objects for all matches in s.
-    These are regex match objects or (start, end) for balanced searches.
-    '''
-    if self.is_balanced():
-        aList, i = [], 0
-        while i &lt; len(s):
-            progress = i
-            j = self.full_balanced_match(s, i)
-            if j is None:
-                i += 1
-            else:
-                aList.append((i, j),)
-                i = j
-            assert progress &lt; i
-        return aList
-    return list(self.regex.finditer(s))
-</t>
-<t tx="ekr.20160318141204.109">def full_balanced_match(self, s, i):
-    '''Return the index of the end of the match found at s[i:] or None.'''
-    pattern = self.find_s
-    j = 0  # index into pattern
-    while i &lt; len(s) and j &lt; len(pattern) and pattern[j] in ('*', s[i]):
-        progress = i
-        if pattern[j : j + 3] in ('(*)', '[*]', '{*}'):
-            delim = pattern[j]
-            i = self.match_balanced(delim, s, i)
-            j += 3
-        elif j == len(pattern) - 1 and pattern[j] == '*':
-            # A trailing * matches the rest of the string.
-            j += 1
-            i = len(s)
-            break
-        else:
-            i += 1
-            j += 1
-        assert progress &lt; i
-    found = i &lt;= len(s) and j == len(pattern)
-    return i if found else None
-</t>
-<t tx="ekr.20160318141204.11">def main():
-    '''
-    The driver for the stand-alone version of make-stub-files.
-    All options come from ~/stubs/make_stub_files.cfg.
-    '''
-    controller = Controller()
-    controller.scan_command_line()
-    controller.scan_options()
-    controller.run()
-</t>
 <t tx="ekr.20160318141204.110">def match_balanced(self, delim, s, i):
-    '''
+    """
     delim == s[i] and delim is in '([{'
     Return the index of the end of the balanced parenthesized string, or len(s)+1.
-    '''
+    """
     global g_input_file_name
     assert s[i] == delim, s[i]
     assert delim in '([{'
@@ -2262,281 +1309,19 @@ __str__ = __repr__
     # print('called from:', g.callers(4))
     return len(s) + 1
 </t>
-<t tx="ekr.20160318141204.111">def match(self, s, trace=False):
-    '''
-    Perform the match on the entire string if possible.
-    Return (found, new s)
-    '''
-    # caller = g.callers(2).split(',')[0].strip()
-    # s1 = truncate(s,40)
-    if self.is_balanced():
-        j = self.full_balanced_match(s, 0)
-        if j is None:
-            return False, s
-        start, end = 0, len(s)
-        s = self.replace_balanced(s, start, end)
-        return True, s
-    m = self.regex.match(s)
-    if m and m.group(0) == s:
-        s = self.replace_regex(m, s)
-        return True, s
-    return False, s
-</t>
-<t tx="ekr.20160318141204.112">def match_entire_string(self, s):
-    '''Return True if s matches self.find_s'''
-    if self.is_balanced():
-        j = self.full_balanced_match(s, 0)
-        return j == len(s)
-    m = self.regex.match(s)
-    return m and m.group(0) == s
-</t>
-<t tx="ekr.20160318141204.113">def replace(self, m, s):
-    '''Perform any kind of replacement.'''
-    if self.is_balanced():
-        start, end = m
-        return self.replace_balanced(s, start, end)
-    return self.replace_regex(m, s)
-</t>
-<t tx="ekr.20160318141204.114">def replace_balanced(self, s1, start, end):
-    '''
-    Use m (returned by all_matches) to replace s by the string implied by repr_s.
-    Within repr_s, * star matches corresponding * in find_s
-    '''
-    s = s1[start:end]
-    f, r = self.find_s, self.repl_s
-    i1 = f.find('(*)')
-    i2 = f.find('[*]')
-    i3 = f.find('{*}')
-    if -1 == i1 == i2 == i3:
-        return s1[:start] + r + s1[end:]
-    j = r.find('*')
-    if j == -1:
-        return s1[:start] + r + s1[end:]
-    i = min([z for z in [i1, i2, i3] if z &gt; -1])
-    assert i &gt; -1  # i is an index into f AND s
-    delim = f[i]
-    assert s[:i] == f[:i], (s[:i], f[:i])
-    k = self.match_balanced(delim, s, i)
-    s_star = s[i + 1 : k - 1]
-    repl = r[:j] + s_star + r[j + 1 :]
-    return s1[:start] + repl + s1[end:]
-</t>
-<t tx="ekr.20160318141204.115">def replace_regex(self, m, s):
-    '''Do the replacement in s specified by m.'''
-    s = self.repl_s
-    for i in range(9):
-        group = '\\%s' % i
-        if s.find(group) &gt; -1:
-            s = s.replace(group, m.group(i))
-    return s
-</t>
-<t tx="ekr.20160318141204.116">class ReduceTypes:
-    '''
-    A helper class for the top-level reduce_types function.
-    
-    This class reduces a list of type hints to a string containing the
-    reduction of all types in the list.
-    '''
-    @others
-</t>
-<t tx="ekr.20160318141204.117">def __init__(self, aList=None, name=None, trace=False):
-    '''Ctor for ReduceTypes class.'''
-    self.aList = aList
-    self.name = name
-    self.optional = False
-    self.trace = trace
-</t>
-<t tx="ekr.20160318141204.118">def is_known_type(self, s):
-    '''
-    Return True if s is nothing but a single known type.
-
-    It suits the other methods of this class *not* to test inside inner
-    brackets. This prevents unwanted Any types.
-    '''
-    s = s.strip()
-    table = (
-        '', 'None',  # Tricky.
-        'complex', 'float', 'int', 'long', 'number',
-        'dict', 'list', 'tuple',
-        'bool', 'bytes', 'str', 'unicode',
-    )
-    for s2 in table:
-        if s2 == s:
-            return True
-        if Pattern(s2 + '(*)', s).match_entire_string(s):
-            return True
-    if s.startswith('[') and s.endswith(']'):
-        inner = s[1:-1]
-        return self.is_known_type(inner) if inner else True
-    if s.startswith('(') and s.endswith(')'):
-        inner = s[1:-1]
-        return self.is_known_type(inner) if inner else True
-    if s.startswith('{') and s.endswith('}'):
-        return True
-    table = (
-        # Pep 484: https://www.python.org/dev/peps/pep-0484/
-        # typing module: https://docs.python.org/3/library/typing.html
-        # Test the most common types first.
-        'Any', 'Dict', 'List', 'Optional', 'Tuple', 'Union',
-        # Not generated by this program, but could arise from patterns.
-        'AbstractSet', 'AnyMeta', 'AnyStr',
-        'BinaryIO', 'ByteString',
-        'Callable', 'CallableMeta', 'Container',
-        'Final', 'Generic', 'GenericMeta', 'Hashable',
-        'IO', 'ItemsView', 'Iterable', 'Iterator',
-        'KT', 'KeysView',
-        'Mapping', 'MappingView', 'Match',
-        'MutableMapping', 'MutableSequence', 'MutableSet',
-        'NamedTuple', 'OptionalMeta',
-        # 'POSIX', 'PY2', 'PY3',
-        'Pattern', 'Reversible',
-        'Sequence', 'Set', 'Sized',
-        'SupportsAbs', 'SupportsFloat', 'SupportsInt', 'SupportsRound',
-        'T', 'TextIO', 'TupleMeta', 'TypeVar', 'TypingMeta',
-        'Undefined', 'UnionMeta',
-        'VT', 'ValuesView', 'VarBinding',
-    )
-    for s2 in table:
-        if s2 == s:
-            return True
-        # Don't look inside bracketss.
-        pattern = Pattern(s2 + '[*]', s)
-        if pattern.match_entire_string(s):
-            return True
-    return False
-</t>
-<t tx="ekr.20160318141204.119">def reduce_collection(self, aList, kind):
-    '''
-    Reduce the inner parts of a collection for the given kind.
-    Return a list with only collections of the given kind reduced.
-    '''
-    assert isinstance(aList, list)
-    assert None not in aList, aList
-    pattern = Pattern('%s[*]' % kind)
-    others, r1, r2 = [], [], []
-    for s in sorted(set(aList)):
-        if pattern.match_entire_string(s):
-            r1.append(s)
-        else:
-            others.append(s)
-    for s in sorted(set(r1)):
-        parts = []
-        s2 = s[len(kind) + 1 : -1]
-        for s3 in s2.split(','):
-            s3 = s3.strip()
-            parts.append(s3 if self.is_known_type(s3) else 'Any')
-        r2.append('%s[%s]' % (kind, ', '.join(parts)))
-    result = others
-    result.extend(r2)
-    return sorted(set(result))
-</t>
-<t tx="ekr.20160318141204.120">def reduce_numbers(self, aList):
-    '''
-    Return aList with all number types in aList replaced by the most
-    general numeric type in aList.
-    '''
-    found = None
-    numbers = ('number', 'complex', 'float', 'long', 'int')
-    for kind in numbers:
-        for z in aList:
-            if z == kind:
-                found = kind
-                break
-        if found:
-            break
-    if found:
-        assert found in numbers, found
-        aList = [z for z in aList if z not in numbers]
-        aList.append(found)
-    return aList
-</t>
-<t tx="ekr.20160318141204.121">def reduce_types(self):
-    '''
-    self.aList consists of arbitrarily many types because this method is
-    called from format_return_expressions.
-    
-    Return a *string* containing the reduction of all types in this list.
-    Returning a string means that all traversers always return strings,
-    never lists.
-    '''
-    r = [('None' if z in ('', None) else z) for z in self.aList]
-    assert None not in r
-    self.optional = 'None' in r
-        # self.show adds Optional if this flag is set.
-    r = [z for z in r if z != 'None']
-    if not r:
-        self.optional = False
-        return self.show('None')
-    r = sorted(set(r))
-    assert r
-    assert None not in r
-    r = self.reduce_numbers(r)
-    for kind in ('Dict', 'List', 'Tuple',):
-        r = self.reduce_collection(r, kind)
-    r = self.reduce_unknowns(r)
-    r = sorted(set(r))
-    assert r
-    assert 'None' not in r
-    if len(r) == 1:
-        return self.show(r[0])
-    return self.show('Union[%s]' % (', '.join(sorted(r))))
-</t>
-<t tx="ekr.20160318141204.122">def reduce_unknowns(self, aList):
-    '''Replace all unknown types in aList with Any.'''
-    return [z if self.is_known_type(z) else 'Any' for z in aList]
-</t>
-<t tx="ekr.20160318141204.123">def show(self, s, known=True):
-    '''Show the result of reduce_types.'''
-    aList, name = self.aList, self.name
-    trace = self.trace
-    s = s.strip()
-    if self.optional:
-        s = 'Optional[%s]' % s
-    if trace and (not known or len(aList) &gt; 1):
-        if name:
-            if name.find('.') &gt; -1:
-                context = ''.join(name.split('.')[1:])
-            else:
-                context = name
-        else:
-            context = g.callers(3).split(',')[0].strip()
-        context = truncate(context, 26)
-        known = '' if known else '? '
-        pattern = sorted(set([z.replace('\n', ' ') for z in aList]))
-        pattern = '[%s]' % truncate(', '.join(pattern), 53 - 2)
-        print('reduce_types: %-26s %53s ==&gt; %s%s' % (context, pattern, known, s))
-            # widths above match the corresponding indents in match_all and match.
-    return s
-</t>
-<t tx="ekr.20160318141204.124">def split_types(self, s):
-    '''Split types on *outer level* commas.'''
-    aList, i1, level = [], 0, 0
-    for i, ch in enumerate(s):
-        if ch == '[':
-            level += 1
-        elif ch == ']':
-            level -= 1
-        elif ch == ',' and level == 0:
-            aList.append(s[i1:i])
-            i1 = i + 1
-    aList.append(s[i1:].strip())
-    return aList
-</t>
-<t tx="ekr.20160318141204.125">
-
-class Controller:
-    '''
-    A class to make Python stub (.pyi) files in the ~/stubs directory for
-    every file mentioned in the [Source Files] section of
-    ~/stubs/make_stub_files.cfg.
-    '''
+<t tx="ekr.20160318141204.125">class Controller:
+    """
+    Make Python stub (.pyi) files in the ~/stubs directory for every file
+    in the [Source Files] section of the configuration file.
+    """
     @others
 </t>
 <t tx="ekr.20160318141204.126">def __init__(self):
-    '''Ctor for Controller class.'''
+    """Ctor for Controller class."""
     self.options = {}
     # Ivars set on the command line...
     self.config_fn = None
+    self.enable_coverage_tests = False
     self.enable_unit_tests = False
     self.files = []
     # Ivars set in the config file...
@@ -2562,19 +1347,16 @@ class Controller:
     self.regex_patterns = []
 </t>
 <t tx="ekr.20160318141204.127">def finalize(self, fn):
-    '''Finalize and regularize a filename.'''
-    fn = os.path.expanduser(fn)
-    fn = os.path.abspath(fn)
-    fn = os.path.normpath(fn)
-    return fn
+    """Finalize and regularize a filename."""
+    return os.path.normpath(os.path.abspath(os.path.expanduser(fn)))
 </t>
 <t tx="ekr.20160318141204.128">directory_warning_given = False
 
 def make_stub_file(self, fn):
-    '''
+    """
     Make a stub file in ~/stubs for all source files mentioned in the
-    [Source Files] section of ~/stubs/make_stub_files.cfg
-    '''
+    [Source Files] section of the configuration file.
+    """
     global g_input_file_name
     extension = fn[fn.rfind('.'):]
     if not extension == '.py' and not (self.force_pyx and extension == '.pyx'):
@@ -2613,16 +1395,7 @@ def make_stub_file(self, fn):
     StubTraverser(controller=self).run(node)
 </t>
 <t tx="ekr.20160318141204.129">def run(self):
-    '''
-    Make stub files for all files.
-    Do nothing if the output directory does not exist.
-    '''
-    if self.enable_unit_tests:
-        self.run_all_unit_tests()
-    if not self.files:
-        if not self.enable_unit_tests:
-            print('no input files')
-        return
+    """Make stubs for all files."""
     for fn in self.files:
         self.make_stub_file(fn)
 </t>
@@ -2636,21 +1409,24 @@ def make_stub_file(self, fn):
     unittest.TextTestRunner(verbosity=1).run(suite)
 </t>
 <t tx="ekr.20160318141204.131">def scan_command_line(self):
-    '''Set ivars from command-line arguments.'''
-    # This automatically implements the --help option.
-    usage = "usage: make_stub_files.py [options] file1, file2, ..."
-    parser = optparse.OptionParser(usage=usage)
-    add = parser.add_option
-    add('-c', '--config', dest='fn',
+    """Set ivars from command-line arguments."""
+    # The parser implements the --help option.
+    description='make_stub_file: Create stub (.pyi) files from python files'
+    usage = 'make_stub_files.py [options] file1, file2, ...'
+    parser = argparse.ArgumentParser(description=description, usage=usage)
+    add = parser.add_argument
+    add('files', metavar='FILE', type=str, nargs='+',
+        help='input files')
+    add('-c', '--config', dest='fn', metavar='FILE',
         help='full path to configuration file')
     add('-d', '--dir', dest='dir',
         help='full path to the output directory')
+    add('-f', '--force-pyx', action='store_true', default=False,
+        help='force the parsing of .pyx files')
     add('-o', '--overwrite', action='store_true', default=False,
         help='overwrite existing stub (.pyi) files')
     add('-s', '--silent', action='store_true', default=False,
         help='run without messages')
-    add('-t', '--test', action='store_true', default=False,
-        help='run unit tests on startup')
     add('--trace-matches', action='store_true', default=False,
         help='trace Pattern.matches')
     add('--trace-patterns', action='store_true', default=False,
@@ -2665,25 +1441,23 @@ def make_stub_file(self, fn):
         help='verbose output in .pyi file')
     add('-w', '--warn', action='store_true', default=False,
         help='warn about unannotated args')
-    add('--force-pyx', action='store_true', default=False, help='force the parsing of .pyx files')
-    # Parse the options
-    options, args = parser.parse_args()
-    # Handle the options...
-    self.enable_unit_tests = options.test
-    self.overwrite = options.overwrite
-    self.silent = options.silent
-    self.trace_matches = options.trace_matches
-    self.trace_patterns = options.trace_patterns
-    self.trace_reduce = options.trace_reduce
-    self.trace_visitors = options.trace_visitors
-    self.update_flag = options.update
-    self.verbose = options.verbose
-    self.warn = options.warn
-    self.force_pyx = options.force_pyx
-    if options.fn:
-        self.config_fn = options.fn
-    if options.dir:
-        dir_ = options.dir and options.dir.strip()
+    # Parse.
+    args = parser.parse_args()
+    # Handle the args...
+    self.overwrite = args.overwrite
+    self.silent = args.silent
+    self.trace_matches = args.trace_matches
+    self.trace_patterns = args.trace_patterns
+    self.trace_reduce = args.trace_reduce
+    self.trace_visitors = args.trace_visitors
+    self.update_flag = args.update
+    self.verbose = args.verbose
+    self.warn = args.warn
+    self.force_pyx = args.force_pyx
+    if args.fn:
+        self.config_fn = args.fn
+    if args.dir:
+        dir_ = args.dir and args.dir.strip()
         dir_ = self.finalize(dir_)
         g.trace('dir', dir_)
         if os.path.exists(dir_):
@@ -2692,18 +1466,15 @@ def make_stub_file(self, fn):
             print('--dir: directory does not exist: %s' % dir_)
             print('exiting')
             sys.exit(1)
-    if options.force_pyx:
+    if args.force_pyx:
         print('--force-pyx: .pyx files will be parsed as regular python, cython syntax is not supported')
-    # If any files remain, set self.files.
-    if args:
-        args = [self.finalize(z) for z in args]
-        if args:
-            self.files = args
+    self.files = args.files
 </t>
 <t tx="ekr.20160318141204.132">def scan_options(self):
-    '''Set all configuration-related ivars.'''
+    """Set all configuration-related ivars."""
     if self.verbose:
-        g.trace('config file: %s' % self.config_fn)
+        print('')
+        print(f"configuration file: {self.config_fn}")
     if not self.config_fn:
         return
     self.parser = parser = self.create_parser()
@@ -2712,50 +1483,65 @@ def make_stub_file(self, fn):
     if self.files:
         files_source = 'command-line'
         files = self.files
+        if isinstance(files, str):
+            files = [files]
     elif parser.has_section('Global'):
         files_source = 'config file'
         files = parser.get('Global', 'files')
         files = [z.strip() for z in files.split('\n') if z.strip()]
     else:
         return
+    if self.verbose:
+        print(f"Files (from {files_source})...")
     files2 = []
+    not_found = []
     for z in files:
-        files2.extend(glob.glob(self.finalize(z)))
-    self.files = [z for z in files2 if z and os.path.exists(z)]
-    if self.verbose:
-        print('Files (from %s)...\n' % files_source)
-        for z in self.files:
-            print(z)
-        print('')
+        # 2021/08/04: Warn if z does not exist.
+        files3 = glob.glob(self.finalize(z))
+        if files3:
+            if self.verbose:
+                for z in files3:
+                    print(f"  {z}")
+            files2.extend(files3)
+        else:
+            not_found.append(z)
+    if not_found:
+        print('Not found...')
+        for z in not_found:
+            print(f"  {z}")
+    self.files = files2
     if 'output_directory' in parser.options('Global'):
         s = parser.get('Global', 'output_directory').strip()
         output_dir = self.finalize(s)
         if os.path.exists(output_dir):
             self.output_directory = output_dir
             if self.verbose:
-                print('output directory: %s\n' % output_dir)
+                print(f"output directory: {output_dir}")
         else:
-            print('output directory not found: %s\n' % output_dir)
+            print(f"output directory not found: {output_dir}")
             self.output_directory = None  # inhibit run().
     if 'prefix_lines' in parser.options('Global'):
         prefix = parser.get('Global', 'prefix_lines')
         prefix_lines = prefix.split('\n')
             # The parser does not preserve leading whitespace.
         self.prefix_lines = [z for z in prefix_lines if z.strip()]
-        if self.verbose:
-            print('Prefix lines...\n')
-            for z in self.prefix_lines:
-                print('  %s' % z)
-            print('')
+        # Annoying
+        # if self.verbose:
+            # print('Prefix lines...\n')
+            # for z in self.prefix_lines:
+                # print('  %s' % z)
+            # print('')
+    if self.verbose:
+        print('')
     self.def_patterns = self.scan_patterns('Def Name Patterns')
     self.general_patterns = self.scan_patterns('General Patterns')
     self.make_patterns_dict()
 </t>
 <t tx="ekr.20160318141204.133">def make_op_name_dict(self):
-    '''
+    """
     Make a dict whose keys are operators ('+', '+=', etc),
     and whose values are lists of values of ast.Node.__class__.__name__.
-    '''
+    """
     d = {
         '.': ['Attr',],
         '(*)': ['Call', 'Tuple',],
@@ -2777,14 +1563,14 @@ def make_stub_file(self, fn):
     return d
 </t>
 <t tx="ekr.20160318141204.134">def create_parser(self):
-    '''Create a RawConfigParser and return it.'''
+    """Create a RawConfigParser and return it."""
     parser = configparser.RawConfigParser(dict_type=OrderedDict)
         # Requires Python 2.7
     parser.optionxform = str
     return parser
 </t>
 <t tx="ekr.20160318141204.135">def find_pattern_ops(self, pattern):
-    '''Return a list of operators in pattern.find_s.'''
+    """Return a list of operators in pattern.find_s."""
     trace = False or self.trace_patterns
     if pattern.is_regex():
         # Add the pattern to the regex patterns list.
@@ -2824,21 +1610,17 @@ def make_stub_file(self, fn):
     return ops
 </t>
 <t tx="ekr.20160318141204.136">def get_config_string(self):
-
+    """Read the configuration file."""
     fn = self.finalize(self.config_fn)
     if os.path.exists(fn):
-        if self.verbose:
-            print('\nconfiguration file: %s\n' % fn)
-        f = open(fn, 'r')
-        s = f.read()
-        f.close()
-        return s
-    print('\nconfiguration file not found: %s' % fn)
+        with open(fn, 'r') as f:
+            return f.read()
+    print(f"\nconfiguration file not found: {fn}")
     return ''
 
 </t>
 <t tx="ekr.20160318141204.137">def init_parser(self, s):
-    '''Add double back-slashes to all patterns starting with '['.'''
+    """Add double back-slashes to all patterns starting with '['."""
     if not s:
         return
     aList = []
@@ -2867,7 +1649,7 @@ def make_stub_file(self, fn):
     return False
 </t>
 <t tx="ekr.20160318141204.139">def make_patterns_dict(self):
-    '''Assign all patterns to the appropriate ast.Node.'''
+    """Assign all patterns to the appropriate ast.Node."""
     for pattern in self.general_patterns:
         ops = self.find_pattern_ops(pattern)
         if ops:
@@ -2903,19 +1685,17 @@ def make_stub_file(self, fn):
                 print('  ' + repr(pattern))
     # Note: retain self.general_patterns for use in argument lists.
 </t>
-<t tx="ekr.20160318141204.14">
-
-class AstFormatter:
-    '''
+<t tx="ekr.20160318141204.14">class AstFormatter:
+    """
     A class to recreate source code from an AST.
     
     This does not have to be perfect, but it should be close.
-    '''
+    """
     # pylint: disable=consider-using-enumerate
     @others
 </t>
 <t tx="ekr.20160318141204.140">def scan_patterns(self, section_name):
-    '''Parse the config section into a list of patterns, preserving order.'''
+    """Parse the config section into a list of patterns, preserving order."""
     trace = False or self.trace_patterns
     parser = self.parser
     aList = []
@@ -2939,14 +1719,14 @@ class AstFormatter:
     return aList
 </t>
 <t tx="ekr.20160318141204.141">class Stub:
-    '''
+    """
     A class representing all the generated stub for a class or def.
     stub.full_name should represent the complete context of a def.
-    '''
+    """
     @others
 </t>
 <t tx="ekr.20160318141204.142">def __init__(self, kind, name, parent=None, stack=None):
-    '''Stub ctor. Equality depends only on full_name and kind.'''
+    """Stub ctor. Equality depends only on full_name and kind."""
     self.children = []
     self.full_name = '%s.%s' % ('.'.join(stack), name) if stack else name
     self.kind = kind
@@ -2961,10 +1741,10 @@ class AstFormatter:
         parent.children.append(self)
 </t>
 <t tx="ekr.20160318141204.143">def __eq__(self, obj):
-    '''
+    """
     Stub.__eq__. Return whether two stubs refer to the same method.
     Do *not* test parent links. That would interfere with --update logic.
-    '''
+    """
     if isinstance(obj, Stub):
         return self.full_name == obj.full_name and self.kind == obj.kind
     return NotImplemented
@@ -2974,33 +1754,33 @@ def __ne__(self, obj):
     return not self.__eq__(obj)
 </t>
 <t tx="ekr.20160318141204.144">def __hash__(self):
-    '''Stub.__hash__. Equality depends *only* on full_name and kind.'''
+    """Stub.__hash__. Equality depends *only* on full_name and kind."""
     return len(self.kind) + sum([ord(z) for z in self.full_name])
 </t>
 <t tx="ekr.20160318141204.145">def __repr__(self):
-    '''Stub.__repr__.'''
+    """Stub.__repr__."""
     # return 'Stub: %s %s' % (id(self), self.full_name)
     return 'Stub: %s\n%s' % (self.full_name, g.objToString(self.out_list))
 
 __str__ = __repr__
 </t>
 <t tx="ekr.20160318141204.146">def level(self):
-    '''Return the number of parents.'''
+    """Return the number of parents."""
     return len(self.parents())
 
 def parents(self):
-    '''Return a list of this stub's parents.'''
+    """Return a list of this stub's parents."""
     return self.full_name.split('.')[:-1]
 </t>
 <t tx="ekr.20160318141204.147">class StubFormatter(AstFormatter):
-    '''
+    """
     Formats an ast.Node and its descendants,
     making pattern substitutions in Name and operator nodes.
-    '''
+    """
     @others
 </t>
 <t tx="ekr.20160318141204.148">def __init__(self, controller, traverser):
-    '''Ctor for StubFormatter class.'''
+    """Ctor for StubFormatter class."""
     self.controller = x = controller
     self.traverser = traverser
         # 2016/02/07: to give the formatter access to the class_stack.
@@ -3021,7 +1801,7 @@ def parents(self):
 <t tx="ekr.20160318141204.149">matched_d = {}
 
 def match_all(self, node, s, trace=False):
-    '''Match all the patterns for the given node.'''
+    """Match all the patterns for the given node."""
     trace = trace or self.trace_matches
     # verbose = True
     d = self.matched_d
@@ -3051,7 +1831,7 @@ def match_all(self, node, s, trace=False):
     return s
 </t>
 <t tx="ekr.20160318141204.151">def trace_visitor(self, node, op, s):
-    '''Trace node's visitor.'''
+    """Trace node's visitor."""
     if self.trace_visitors:
         caller = g.callers(2).split(',')[1]
         s1 = AstFormatter().format(node).strip()
@@ -3064,7 +1844,7 @@ def match_all(self, node, s, trace=False):
 attrs_seen = []
 
 def do_Attribute(self, node):
-    '''StubFormatter.do_Attribute.'''
+    """StubFormatter.do_Attribute."""
     s = '%s.%s' % (
         self.visit(node.value),
         node.attr)  # Don't visit node.attr: it is always a string.
@@ -3085,7 +1865,7 @@ def do_Num(self, node):
     return 'number'  # return repr(node.n)
 
 def do_Str(self, node):
-    '''This represents a string constant.'''
+    """This represents a string constant."""
     return 'str'  # return repr(node.s)
 </t>
 <t tx="ekr.20160318141204.155">def do_Dict(self, node):
@@ -3106,7 +1886,7 @@ def do_Str(self, node):
     return 'Dict[%s]' % ''.join(result) if result else 'Dict'
 </t>
 <t tx="ekr.20160318141204.156">def do_List(self, node):
-    '''StubFormatter.List.'''
+    """StubFormatter.List."""
     elts = [self.visit(z) for z in node.elts]
     elts = [z for z in elts if z]  # Defensive.
     return 'List[%s]' % ', '.join(elts) if elts else 'List'
@@ -3114,7 +1894,7 @@ def do_Str(self, node):
 <t tx="ekr.20160318141204.157"># seen_names = [] # t--ype: List[str]
 
 def do_Name(self, node):
-    '''StubFormatter ast.Name visitor.'''
+    """StubFormatter ast.Name visitor."""
     d = self.names_dict
     name = d.get(node.id, node.id)
     s = 'bool' if name in ('True', 'False') else name
@@ -3127,7 +1907,7 @@ def do_Name(self, node):
     return s
 </t>
 <t tx="ekr.20160318141204.158">def do_Tuple(self, node):
-    '''StubFormatter.Tuple.'''
+    """StubFormatter.Tuple."""
     elts = [self.visit(z) for z in node.elts]
     return 'Tuple[%s]' % ', '.join(elts)
 </t>
@@ -3136,7 +1916,7 @@ def do_Name(self, node):
 <t tx="ekr.20160318141204.160"># BinOp(expr left, operator op, expr right)
 
 def do_BinOp(self, node):
-    '''StubFormatter.BinOp visitor.'''
+    """StubFormatter.BinOp visitor."""
     trace = self.trace_reduce
     numbers = ['number', 'complex', 'float', 'long', 'int',]
     op = self.op_name(node.op)
@@ -3166,8 +1946,8 @@ def do_BinOp(self, node):
 </t>
 <t tx="ekr.20160318141204.161"># BoolOp(boolop op, expr* values)
 
-def do_BoolOp(self, node):  # Python 2.x only.
-    '''StubFormatter.BoolOp visitor for 'and' and 'or'.'''
+def do_BoolOp(self, node):  # pragma: no cover (Python 2.x only)
+    """StubFormatter.BoolOp visitor for 'and' and 'or'."""
     trace = self.trace_reduce
     op = self.op_name(node.op)
     values = [self.visit(z).strip() for z in node.values]
@@ -3179,7 +1959,7 @@ def do_BoolOp(self, node):  # Python 2.x only.
 <t tx="ekr.20160318141204.162"># Call(expr func, expr* args, keyword* keywords, expr? starargs, expr? kwargs)
 
 def do_Call(self, node):
-    '''StubFormatter.Call visitor.'''
+    """StubFormatter.Call visitor."""
     func = self.visit(node.func)
     args = [self.visit(z) for z in node.args]
     for z in node.keywords:
@@ -3213,10 +1993,10 @@ def do_keyword(self, node):
 <t tx="ekr.20160318141204.164"># Compare(expr left, cmpop* ops, expr* comparators)
 
 def do_Compare(self, node):
-    '''
+    """
     StubFormatter ast.Compare visitor for these ops:
     '==', '!=', '&lt;', '&lt;=', '&gt;', '&gt;=', 'is', 'is not', 'in', 'not in',
-    '''
+    """
     s = 'bool'  # Correct regardless of arguments.
     ops = ','.join([self.op_name(z) for z in node.ops])
     self.trace_visitor(node, ops, s)
@@ -3225,7 +2005,7 @@ def do_Compare(self, node):
 <t tx="ekr.20160318141204.165"># If(expr test, stmt* body, stmt* orelse)
 
 def do_IfExp(self, node):
-    '''StubFormatterIfExp (ternary operator).'''
+    """StubFormatterIfExp (ternary operator)."""
     trace = self.trace_reduce
     aList = [
         self.match_all(node, self.visit(node.body)),
@@ -3239,7 +2019,7 @@ def do_IfExp(self, node):
 <t tx="ekr.20160318141204.166"># Subscript(expr value, slice slice, expr_context ctx)
 
 def do_Subscript(self, node):
-    '''StubFormatter.Subscript.'''
+    """StubFormatter.Subscript."""
     s = '%s[%s]' % (
         self.visit(node.value),
         self.visit(node.slice))
@@ -3250,7 +2030,7 @@ def do_Subscript(self, node):
 <t tx="ekr.20160318141204.167"># UnaryOp(unaryop op, expr operand)
 
 def do_UnaryOp(self, node):
-    '''StubFormatter.UnaryOp for unary +, -, ~ and 'not' operators.'''
+    """StubFormatter.UnaryOp for unary +, -, ~ and 'not' operators."""
     op = self.op_name(node.op)
     if op.strip() == 'not':
         return 'bool'
@@ -3260,31 +2040,30 @@ def do_UnaryOp(self, node):
     return s
 </t>
 <t tx="ekr.20160318141204.168">def do_Return(self, node):
-    '''
+    """
     StubFormatter ast.Return vsitor.
     Return only the return expression itself.
-    '''
+    """
     s = AstFormatter.do_Return(self, node)
     assert s.startswith('return'), repr(s)
     return s[len('return') :].strip()
 </t>
 <t tx="ekr.20160318141204.169">class StubTraverser(ast.NodeVisitor):
-    '''
+    """
     An ast.Node traverser class that outputs a stub for each class or def.
     Names of visitors must start with visit_. The order of traversal does
     not matter, because so few visitors do anything.
-    '''
+    """
     @others
 </t>
 <t tx="ekr.20160318141204.17">def format(self, node):
-    '''Format the node (or list of nodes) and its descendants.'''
+    """Format the node (or list of nodes) and its descendants."""
     self.level = 0
     val = self.visit(node)
-    # pylint: disable=consider-using-ternary
-    return val and val.strip() or ''
+    return val.strip()  # val is a string.
 </t>
 <t tx="ekr.20160318141204.170">def __init__(self, controller):
-    '''Ctor for StubTraverser class.'''
+    """Ctor for StubTraverser class."""
     self.controller = x = controller  # A Controller instance.
     # Internal state ivars...
     self.class_name_stack = []
@@ -3322,7 +2101,7 @@ def do_UnaryOp(self, node):
     self.patterns_dict = x.patterns_dict
 </t>
 <t tx="ekr.20160318141204.171">def add_stub(self, d, stub):
-    '''Add the stub to d, checking that it does not exist.'''
+    """Add the stub to d, checking that it does not exist."""
     global g_input_file_name
     key = stub.full_name
     assert key
@@ -3333,12 +2112,12 @@ def do_UnaryOp(self, node):
         d[key] = stub
 </t>
 <t tx="ekr.20160318141204.172">def indent(self, s):
-    '''Return s, properly indented.'''
+    """Return s, properly indented."""
     # This version of indent *is* used.
     return '%s%s' % (' ' * 4 * self.level, s)
 
 def out(self, s):
-    '''Output the string to the console or the file.'''
+    """Output the string to the console or the file."""
     s = self.indent(s)
     if self.parent_stub:
         self.parent_stub.out_list.append(s)
@@ -3348,7 +2127,7 @@ def out(self, s):
         print(s)
 </t>
 <t tx="ekr.20160318141204.173">def run(self, node):
-    '''StubTraverser.run: write the stubs in node's tree to self.output_fn.'''
+    """StubTraverser.run: write the stubs in node's tree to self.output_fn."""
     fn = self.output_fn
     dir_ = os.path.dirname(fn)
     if os.path.exists(fn) and not self.overwrite:
@@ -3375,7 +2154,7 @@ def out(self, s):
         print('wrote: %s' % fn)
 </t>
 <t tx="ekr.20160318141204.174">def output_stubs(self, stub):
-    '''Output this stub and all its descendants.'''
+    """Output this stub and all its descendants."""
     for s in stub.out_list or []:
         # Indentation must be present when an item is added to stub.out_list.
         if self.output_file:
@@ -3387,20 +2166,20 @@ def out(self, s):
         self.output_stubs(child)
 </t>
 <t tx="ekr.20160318141204.175">def output_time_stamp(self):
-    '''Put a time-stamp in the output file.'''
+    """Put a time-stamp in the output file."""
     if self.output_file:
         self.output_file.write('# make_stub_files: %s\n' %
             time.strftime("%a %d %b %Y at %H:%M:%S"))
 </t>
 <t tx="ekr.20160318141204.176">def update(self, fn, new_root):
-    '''
+    """
     Merge the new_root tree with the old_root tree in fn (a .pyi file).
 
     new_root is the root of the stub tree from the .py file.
     old_root (read below) is the root of stub tree from the .pyi file.
     
     Return old_root, or new_root if there are any errors.
-    '''
+    """
     s = self.get_stub_file(fn)
     if not s or not s.strip():
         return new_root
@@ -3422,7 +2201,7 @@ def out(self, s):
     return new_root
 </t>
 <t tx="ekr.20160318141204.177">def get_stub_file(self, fn):
-    '''Read the stub file into s.'''
+    """Read the stub file into s."""
     if os.path.exists(fn):
         try:
             s = open(fn, 'r').read()
@@ -3434,11 +2213,11 @@ def out(self, s):
     return None
 </t>
 <t tx="ekr.20160318141204.178">def parse_stub_file(self, s, root_name):
-    '''
+    """
     Parse s, the contents of a stub file, into a tree of Stubs.
     
     Parse by hand, so that --update can be run with Python 2.
-    '''
+    """
     assert '\t' not in s
     d = {}
     root = Stub(kind='root', name=root_name)
@@ -3486,12 +2265,12 @@ def out(self, s):
     return d, root
 </t>
 <t tx="ekr.20160318141204.179">def merge_stubs(self, new_stubs, old_root, new_root, trace=False):
-    '''
+    """
     Merge the new_stubs *list* into the old_root *tree*.
     - new_stubs is a list of Stubs from the .py file.
     - old_root is the root of the stubs from the .pyi file.
     - new_root is the root of the stubs from the .py file.
-    '''
+    """
     # Part 1: Delete old stubs do *not* exist in the *new* tree.
     aList = self.check_delete(new_stubs, old_root, new_root, trace)
         # Checks that all ancestors of deleted nodes will be deleted.
@@ -3513,25 +2292,28 @@ def out(self, s):
         assert self.find_stub(stub, old_root), stub
 </t>
 <t tx="ekr.20160318141204.18">def visit(self, node):
-    '''Return the formatted version of an Ast node, or list of Ast nodes.'''
+    """Return the formatted version of an Ast node, or list of Ast nodes."""
+    tag = 'AstFormatter.visit'
+    name = node.__class__.__name__
     if isinstance(node, (list, tuple)):
         return ','.join([self.visit(z) for z in node])
     if node is None:
         return 'None'
-    assert isinstance(node, ast.AST), node.__class__.__name__
+    if not isinstance(node, ast.AST):
+        # #13: Insert an error comment directly into the output.
+        return f"\n#{tag}: not an AST node: {name}\n"
     method_name = 'do_' + node.__class__.__name__
-    try:
-        method = getattr(self, method_name)
-    except AttributeError:
-        return ''
-
-    s = method(node)
-    # assert type(s) == type('abc'), (node, type(s))
-    assert g.isString(s), type(s)
-    return s
+    # #13: *Never* ignore missing visitors!
+    method = getattr(self, method_name, None)
+    if method:
+        s = method(node)
+        assert g.isString(s), s.__class__.__name__
+        return s
+    # #13: Insert an error comment directly into the output.
+    return f"\n#{tag}: no visitor: do_{name}\n"
 </t>
 <t tx="ekr.20160318141204.180">def check_delete(self, new_stubs, old_root, new_root, trace):
-    '''Return a list of nodes that can be deleted.'''
+    """Return a list of nodes that can be deleted."""
     old_stubs = self.flatten_stubs(old_root)
     old_stubs.remove(old_root)
     aList = [z for z in old_stubs if z not in new_stubs]
@@ -3561,24 +2343,24 @@ def out(self, s):
     return delete_list
 </t>
 <t tx="ekr.20160318141204.181">def flatten_stubs(self, root):
-    '''Return a flattened list of all stubs in root's tree.'''
+    """Return a flattened list of all stubs in root's tree."""
     aList = [root]
     for child in root.children:
         self.flatten_stubs_helper(child, aList)
     return aList
 
 def flatten_stubs_helper(self, root, aList):
-    '''Append all stubs in root's tree to aList.'''
+    """Append all stubs in root's tree to aList."""
     aList.append(root)
     for child in root.children:
         self.flatten_stubs_helper(child, aList)
 </t>
 <t tx="ekr.20160318141204.182">def find_parent_stub(self, stub, root):
-    '''Return stub's parent **in root's tree**.'''
+    """Return stub's parent **in root's tree**."""
     return self.find_stub(stub.parent, root) if stub.parent else None
 </t>
 <t tx="ekr.20160318141204.183">def find_stub(self, stub, root):
-    '''Return the stub **in root's tree** that matches stub.'''
+    """Return the stub **in root's tree** that matches stub."""
     if stub == root:  # Must use Stub.__eq__!
         return root  # not stub!
     for child in root.children:
@@ -3587,10 +2369,10 @@ def flatten_stubs_helper(self, root, aList):
     return None
 </t>
 <t tx="ekr.20160318141204.184">def sort_stubs_by_hierarchy(self, stubs1):
-    '''
+    """
     Sort the list of Stubs so that parents appear before all their
     descendants.
-    '''
+    """
     stubs, result = stubs1[:], []
     for i in range(50):
         if stubs:
@@ -3605,7 +2387,7 @@ def flatten_stubs_helper(self, root, aList):
     return []  # Abort the merge.
 </t>
 <t tx="ekr.20160318141204.185">def trace_stubs(self, stub, aList=None, header=None, level=-1):
-    '''Return a trace of the given stub and all its descendants.'''
+    """Return a trace of the given stub and all its descendants."""
     indent = ' ' * 4 * max(0, level)
     if level == -1:
         aList = ['===== %s...\n' % (header) if header else '']
@@ -3699,10 +2481,10 @@ def visit_FunctionDef(self, node):
 <t tx="ekr.20160318141204.188"># arguments = (expr* args, identifier? vararg, identifier? kwarg, expr* defaults)
 
 def format_arguments(self, node):
-    '''
+    """
     Format the arguments node.
     Similar to AstFormat.do_arguments, but it is not a visitor!
-    '''
+    """
     assert isinstance(node, ast.arguments), node
     args = [self.raw_format(z) for z in node.args]
     defaults = [self.raw_format(z) for z in node.defaults]
@@ -3731,7 +2513,7 @@ def format_arguments(self, node):
 <t tx="ekr.20160318141204.189">type_pattern = re.compile(r'.*:.*')
 
 def munge_arg(self, s):
-    '''Add an annotation for s if possible.'''
+    """Add an annotation for s if possible."""
     if s == 'self':
         return s
     for pattern in self.general_patterns:
@@ -3749,16 +2531,17 @@ def munge_arg(self, s):
 # Contexts...
 </t>
 <t tx="ekr.20160318141204.190">def format_returns(self, node):
-    '''
+    """
     Calculate the return type:
     - Return None if there are no return statements.
     - Patterns in [Def Name Patterns] override all other patterns.
     - Otherwise, return a list of return values.
-    '''
+    """
     name = self.get_def_name(node)
     raw = [self.raw_format(z) for z in self.returns]
     # Allow StubFormatter.do_Return to do the hack.
     r = [self.format(z) for z in self.returns]
+    ### g.trace('1', raw, r)
     # Step 1: Return None if there are no return statements.
     if not [z for z in self.returns if z.value is not None]:
         empty = not any(isinstance(z, ast.FunctionDef) for z in node.body)
@@ -3767,25 +2550,27 @@ def munge_arg(self, s):
     # Step 2: [Def Name Patterns] override all other patterns.
     for pattern in self.def_patterns:
         found, s = pattern.match(name)
+        ### g.trace('2', found, name, pattern, s)
         if found:
             return s + ': ...'
     # Step 3: remove recursive calls.
     raw, r = self.remove_recursive_calls(name, raw, r)
+    ### g.trace('3', raw, r)
     # Step 4: Calculate return types.
     return self.format_return_expressions(node, name, raw, r)
 </t>
 <t tx="ekr.20160318141204.191">def format_return_expressions(self, node, name, raw_returns, reduced_returns):
-    '''
+    """
     aList is a list of maximally reduced return expressions.
     For each expression e in Alist:
     - If e is a single known type, add e to the result.
     - Otherwise, add Any # e to the result.
     Return the properly indented result.
-    '''
+    """
     assert len(raw_returns) == len(reduced_returns)
     lws = '\n' + ' ' * 4
     n = len(raw_returns)
-    known = all([is_known_type(e) for e in reduced_returns])
+    known = all(is_known_type(e) for e in reduced_returns)
     empty = not any(isinstance(z, ast.FunctionDef) for z in node.body)
     tail = ': ...' if empty else ':'
     if not known or self.verbose:
@@ -3806,7 +2591,7 @@ def munge_arg(self, s):
     return s + tail
 </t>
 <t tx="ekr.20160318141204.192">def get_def_name(self, node):
-    '''Return the representaion of a function or method name.'''
+    """Return the representaion of a function or method name."""
     if self.class_name_stack:
         name = '%s.%s' % (self.class_name_stack[-1], node.name)
         # All ctors should return None
@@ -3817,7 +2602,7 @@ def munge_arg(self, s):
     return name
 </t>
 <t tx="ekr.20160318141204.193">def remove_recursive_calls(self, name, raw, reduced):
-    '''Remove any recursive calls to name from both lists.'''
+    """Remove any recursive calls to name from both lists."""
     # At present, this works *only* if the return is nothing but the recursive call.
     assert len(raw) == len(reduced)
     pattern = Pattern('%s(*)' % name)
@@ -3928,7 +2713,7 @@ def return_two_lists(s):
 #                expr? returns)
 
 def do_FunctionDef(self, node):
-    '''Format a FunctionDef node.'''
+    """Format a FunctionDef node."""
     result = []
     if node.decorator_list:
         for z in node.decorator_list:
@@ -3968,12 +2753,12 @@ def do_FunctionDef(self, node):
 
 </t>
 <t tx="ekr.20160318141204.26">def do_Expr(self, node):
-    '''An outer expression: must be indented.'''
+    """An outer expression: must be indented."""
     return self.indent('%s\n' % self.visit(node.value))
 
 </t>
 <t tx="ekr.20160318141204.27">def do_Expression(self, node):
-    '''An inner expression: do not indent.'''
+    """An inner expression: do not indent."""
     return '%s\n' % self.visit(node.body)
 
 </t>
@@ -4009,7 +2794,7 @@ def do_Store(self, node):
 #                arg? kwarg, expr* defaults)
 
 def do_arguments(self, node):
-    '''Format the arguments node.'''
+    """Format the arguments node."""
     kind = self.kind(node)
     assert kind == 'arguments', kind
     args = [self.visit(z) for z in node.args]
@@ -4158,7 +2943,7 @@ def do_NameConstant(self, node):  # Python 3 only.
     return repr(node.n)
 
 </t>
-<t tx="ekr.20160318141204.46">def do_Repr(self, node):  # Python 2.x only
+<t tx="ekr.20160318141204.46">def do_Repr(self, node):  # pragma: no cover (Python 2.x only)
     return 'repr(%s)' % self.visit(node.value)
 
 </t>
@@ -4176,7 +2961,7 @@ def do_NameConstant(self, node):  # Python 3 only.
 
 </t>
 <t tx="ekr.20160318141204.48">def do_Str(self, node):
-    '''This represents a string constant.'''
+    """This represents a string constant."""
     return repr(node.s)
 
 </t>
@@ -4304,7 +3089,7 @@ def do_Subscript(self, node):
     return ''.join(result)
 
 </t>
-<t tx="ekr.20160318141204.65">def do_Exec(self, node):  # Python 2.x only
+<t tx="ekr.20160318141204.65">def do_Exec(self, node):  # pragma: no cover (Python 2.x only)
     body = self.visit(node.body)
     args = []  # Globals before locals.
     if getattr(node, 'globals', None):
@@ -4369,7 +3154,7 @@ def do_Subscript(self, node):
 
 </t>
 <t tx="ekr.20160318141204.70">def get_import_names(self, node):
-    '''Return a list of the the full file names in the import statement.'''
+    """Return a list of the the full file names in the import statement."""
     result = []
     for ast2 in node.names:
         if self.kind(ast2) == 'alias':
@@ -4401,7 +3186,7 @@ def do_Nonlocal(self, node):
     return self.indent('pass\n')
 
 </t>
-<t tx="ekr.20160318141204.74">def do_Print(self, node):  # Python 2.x only
+<t tx="ekr.20160318141204.74">def do_Print(self, node):  # pragma: no cover (Python 2.x only)
     vals = []
     for z in node.values:
         vals.append(self.visit(z))
@@ -4569,7 +3354,7 @@ def do_YieldFrom(self, node):
 
 </t>
 <t tx="ekr.20160318141204.87">def kind(self, node):
-    '''Return the name of node's class.'''
+    """Return the name of node's class."""
     return node.__class__.__name__
 </t>
 <t tx="ekr.20160318141204.88">def indent(self, s):
@@ -4578,7 +3363,7 @@ def do_YieldFrom(self, node):
 <t tx="ekr.20160318141204.89">@nobeautify
 
 def op_name(self, node, strict=True):
-    '''Return the print name of an operator node.'''
+    """Return the print name of an operator node."""
     d = {
         # Binary operators.
         'Add': '+',
@@ -4624,131 +3409,6 @@ def op_name(self, node, strict=True):
     if strict: assert name, self.kind(node)
     return name
 </t>
-<t tx="ekr.20160318141204.90">
-
-class AstArgFormatter(AstFormatter):
-    '''
-    Just like the AstFormatter class, except it prints the class
-    names of constants instead of actual values.
-    '''
-    @others
-</t>
-<t tx="ekr.20160318141204.91"># Return generic markers to allow better pattern matches.
-
-def do_BoolOp(self, node):  # Python 2.x only.
-    return 'bool'
-
-def do_Bytes(self, node):  # Python 3.x only.
-    return 'bytes'  # return str(node.s)
-
-def do_Name(self, node):
-    return 'bool' if node.id in ('True', 'False') else node.id
-
-def do_Num(self, node):
-    return 'number'  # return repr(node.n)
-
-def do_Str(self, node):
-    '''This represents a string constant.'''
-    return 'str'  # return repr(node.s)
-</t>
-<t tx="ekr.20160318141204.92">
-
-class LeoGlobals:
-    '''A class supporting g.pdb and g.trace for compatibility with Leo.'''
-    @others
-</t>
-<t tx="ekr.20160318141204.93">
-class NullObject:
-    """
-    An object that does nothing, and does it very well.
-    From the Python cookbook, recipe 5.23
-    """
-    def __init__(self, *args, **keys): pass
-    def __call__(self, *args, **keys): return self
-    def __repr__(self): return "NullObject"
-    def __str__(self): return "NullObject"
-    def __bool__(self): return False
-    def __nonzero__(self): return 0
-    def __delattr__(self, attr): return self
-    def __getattr__(self, attr): return self
-    def __setattr__(self, attr, val): return self
-</t>
-<t tx="ekr.20160318141204.94">def _callerName(self, n=1, files=False):
-    # print('_callerName: %s %s' % (n,files))
-    try:  # get the function name from the call stack.
-        f1 = sys._getframe(n)  # The stack frame, n levels up.
-        code1 = f1.f_code  # The code object
-        name = code1.co_name
-        if name == '__init__':
-            name = '__init__(%s,line %s)' % (
-                self.shortFileName(code1.co_filename), code1.co_firstlineno)
-        if files:
-            return '%s:%s' % (self.shortFileName(code1.co_filename), name)
-        return name  # The code name
-    except ValueError:
-        # print('g._callerName: ValueError',n)
-        return ''  # The stack is not deep enough.
-    except Exception:
-        # es_exception()
-        return ''  # "&lt;no caller name&gt;"
-</t>
-<t tx="ekr.20160318141204.95">def callers(self, n=4, count=0, excludeCaller=True, files=False):
-    '''Return a list containing the callers of the function that called g.callerList.
-
-    If the excludeCaller keyword is True (the default), g.callers is not on the list.
-
-    If the files keyword argument is True, filenames are included in the list.
-    '''
-    # sys._getframe throws ValueError in both cpython and jython if there are less than i entries.
-    # The jython stack often has less than 8 entries,
-    # so we must be careful to call g._callerName with smaller values of i first.
-    result = []
-    i = 3 if excludeCaller else 2
-    while 1:
-        s = self._callerName(i, files=files)
-        # print(i,s)
-        if s:
-            result.append(s)
-        if not s or len(result) &gt;= n: break
-        i += 1
-    result.reverse()
-    if count &gt; 0: result = result[:count]
-    sep = '\n' if files else ','
-    return sep.join(result)
-</t>
-<t tx="ekr.20160318141204.96">def cls(self):
-    '''Clear the screen.'''
-    if sys.platform.lower().startswith('win'):
-        os.system('cls')
-</t>
-<t tx="ekr.20160318141204.97">def isString(self, s):
-    '''Return True if s is any string, but not bytes.'''
-    # pylint: disable=no-member
-    if isPython3:
-        return isinstance(s, str)
-    return isinstance(s, types.StringTypes)
-
-def isUnicode(self, s):
-    '''Return True if s is a unicode string.'''
-    # pylint: disable=no-member
-    if isPython3:
-        return isinstance(s, str)
-    return isinstance(s, types.UnicodeType)
-</t>
-<t tx="ekr.20160318141204.98">def pdb(self):
-    try:
-        import leo.core.leoGlobals as leo_g
-        leo_g.pdb()
-    except ImportError:
-        import pdb
-        pdb.set_trace()
-</t>
-<t tx="ekr.20160318141204.99">def shortFileName(self, fileName, n=None):
-    # pylint: disable=invalid-unary-operand-type
-    if n is None or n &lt; 1:
-        return os.path.basename(fileName)
-    return '/'.join(fileName.replace('\\', '/').split('/')[-n :])
-</t>
 <t tx="ekr.20160330201030.1">Metadata-Version: 1.0
 Name: make_stub_files
 Version: 0.1
@@ -4812,7 +3472,13 @@ Added "tail" logic in st.visit_ClassDef and f.ClassDef.
 Added two new unit tests.
 
 @language python</t>
-<t tx="ekr.20180706073205.1">@nosearch</t>
+<t tx="ekr.20180706073205.1">@language rest
+@wrap
+
+Move all notes into the appropriate PR or issue.
+
+@language python
+@nosearch</t>
 <t tx="ekr.20180706073424.1"># pyflakes complains about the TestClass class.</t>
 <t tx="ekr.20180831102536.1">@language rest
 @wrap
@@ -4837,242 +3503,424 @@ class UnsupportedAlgorithm(Exception):
 
 @language python
 </t>
-<t tx="ekr.20180901040718.1"># https://github.com/edreamleo/make-stub-files/issues/2
-commands = [
-    # 'cls',
-    'python make_stub_files.py -o -s bug2.py',
-]
-g.execute_shell_commands(commands, trace=True)
-with open('bug2.pyi') as f:
-    s = f.read()
-lines = g.splitLines(s)
-expected = 'class InvalidTag(Exception): ...\n'
-got = lines[1]
-assert got == expected, 'expected: %r\ngot:    %r' % (expected, got)</t>
-<t tx="ekr.20180901044640.1"># https://github.com/edreamleo/make-stub-files/issues/2
-commands = [
-    # 'cls',
-    'python make_stub_files.py -o -s bug2a.py',
-]
-g.execute_shell_commands(commands, trace=True)
-with open('bug2a.pyi') as f:
-    s = f.read()
-lines = g.splitLines(s)
-expected = 'class NonEmptyClass:\n'
-got = lines[1]
-assert got == expected, 'expected: %r\ngot:    %r' % (expected, got)</t>
+<t tx="ekr.20180901040718.1">def test_bug2_empty(self):
+    # https://github.com/edreamleo/make-stub-files/issues/2
+    tag = 'test_bug2_empty'
+    s = 'class InvalidTag(Exception):\n    pass'
+    controller = Controller()
+    node = ast.parse(s, filename=tag, mode='exec')
+    st = StubTraverser(controller=controller)
+    # From StubTraverser.run.
+    st.parent_stub = Stub(kind='root', name='&lt;new-stubs&gt;')
+    st.visit(node)
+    # Allocate a StringIo file for output_stubs.
+    st.output_file = io.StringIO()
+    st.output_stubs(st.parent_stub)
+    # Test.
+    lines = g.splitLines(st.output_file.getvalue())
+    expected = ['class InvalidTag(Exception): ...\n']
+    self.assertEqual(lines, expected)
+</t>
+<t tx="ekr.20180901044640.1">def test_bug2_non_empty(self):
+    # https://github.com/edreamleo/make-stub-files/issues/2
+    tag = 'test_bug2_non_empty'
+    s = (
+        'class NonEmptyClass:\n'
+        '\n'
+        '    def spam():\n'
+        '        pass\n'
+    )
+    expected = [
+        'class NonEmptyClass:\n',
+        '    def spam() -&gt; None: ...\n',
+    ]
+    controller = Controller()
+    node = ast.parse(s, filename=tag, mode='exec')
+    st = StubTraverser(controller=controller)
+    # From StubTraverser.run.
+    st.parent_stub = Stub(kind='root', name='&lt;new-stubs&gt;')
+    st.visit(node)
+    # Allocate a StringIo file for output_stubs.
+    st.output_file = io.StringIO()
+    st.output_stubs(st.parent_stub)
+    # Test.
+    lines = g.splitLines(st.output_file.getvalue())
+    self.assertEqual(lines, expected)
+</t>
 <t tx="ekr.20180901050914.1"></t>
-<t tx="ekr.20180901051603.1"># https://github.com/edreamleo/make-stub-files/issues/3
-commands = [
-    # 'cls',
-    'python make_stub_files.py -c make_stub_files.cfg -o -s bug3.py',
-]
-g.execute_shell_commands(commands, trace=True)
-with open('bug3.pyi') as f:
-    s = f.read()
-lines = g.splitLines(s)
-# Test only the last two linse of the generated .pyi file.
-got = ''.join(lines[-2:])
-# The input file 
-expected = (
-    'class UnsupportedAlgorithm(Exception):\n' 
-    '    def __init__(self, message: Any, reason: Optional[str]=None) -&gt; None: ...\n'
-)
-assert got == expected, '\nexpected:\n%s\ngot:\n%s' % (expected, got)</t>
-<t tx="ekr.20180902034437.1">def objToString(self, obj, indent='', printCaller=False, tag=None):
-    '''Pretty print any Python object to a string.'''
-    # pylint: disable=undefined-loop-variable
-        # Looks like a a pylint bug.
-    #
-    # Compute s.
-    if isinstance(obj, dict):
-        s = self.dictToString(obj, indent=indent)
-    elif isinstance(obj, list):
-        s = self.listToString(obj, indent=indent)
-    elif isinstance(obj, tuple):
-        s = self.tupleToString(obj, indent=indent)
-    elif g.isString(obj):
-        # Print multi-line strings as lists.
-        s = obj
-        lines = g.splitLines(s)
-        if len(lines) &gt; 1:
-            s = self.objToString(lines, indent=indent)
-        else:
-            s = repr(s)
-    else:
-        s = repr(obj)
-    #
-    # Compute the return value.
-    if printCaller and tag:
-        prefix = '%s: %s' % (g.caller(), tag)
-    elif printCaller or tag:
-        prefix = self.caller() if printCaller else tag
-    else:
-        prefix = None
-    return '%s...\n%s\n' % (prefix, s) if prefix else s
-
-toString = objToString
-</t>
-<t tx="ekr.20180902034446.1">def printObj(self, obj, indent='', printCaller=False, tag=None):
-    '''Pretty print any Python object using g.pr.'''
-    print(self.objToString(obj, indent=indent, printCaller=printCaller, tag=tag))
-
-</t>
-<t tx="ekr.20180902035806.1">def caller(self, i=1):
-    '''Return the caller name i levels up the stack.'''
-    return self.callers(i + 1).split(',')[0]
-</t>
-<t tx="ekr.20180902041247.1">def dictToString(self, d, indent='', tag=None):
-    '''Pretty print a Python dict to a string.'''
-    # pylint: disable=unnecessary-lambda
-    if not d:
-        return '{}'
-    result = ['{\n']
-    indent2 = indent + ' ' * 4
-    n = 2 + len(indent) + max([len(repr(z)) for z in d.keys()])
-    for i, key in enumerate(sorted(d, key=lambda z: repr(z))):
-        pad = ' ' * max(0, (n - len(repr(key))))
-        result.append('%s%s:' % (pad, key))
-        result.append(self.objToString(d.get(key), indent=indent2))
-        if i + 1 &lt; len(d.keys()):
-            result.append(',')
-        result.append('\n')
-    result.append(indent + '}')
-    s = ''.join(result)
-    return '%s...\n%s\n' % (tag, s) if tag else s
-</t>
-<t tx="ekr.20180902041311.1">def listToString(self, obj, indent='', tag=None):
-    '''Pretty print a Python list to a string.'''
-    if not obj:
-        return '[]'
-    result = ['[']
-    indent2 = indent + ' ' * 4
-    for i, obj2 in enumerate(obj):
-        if len(obj) &gt; 1:
-            result.append('\n' + indent2)
-        result.append(self.objToString(obj2, indent=indent2))
-        if i + 1 &lt; len(obj) &gt; 1:
-            result.append(',')
-        elif len(obj) &gt; 1:
-            result.append('\n' + indent)
-    result.append(']')
-    s = ''.join(result)
-    return '%s...\n%s\n' % (tag, s) if tag else s
-</t>
-<t tx="ekr.20180902041320.1">def tupleToString(self, obj, indent='', tag=None):
-    '''Pretty print a Python tuple to a string.'''
-    if not obj:
-        return '(),'
-    result = ['(']
-    indent2 = indent + ' ' * 4
-    for i, obj2 in enumerate(obj):
-        if len(obj) &gt; 1:
-            result.append('\n' + indent2)
-        result.append(self.objToString(obj2, indent=indent2))
-        if len(obj) == 1 or i + 1 &lt; len(obj):
-            result.append(',')
-        elif len(obj) &gt; 1:
-            result.append('\n' + indent)
-    result.append(')')
-    s = ''.join(result)
-    return '%s...\n%s\n' % (tag, s) if tag else s
-</t>
-<t tx="ekr.20180902073131.1" annotate="7d71002e">def execute_shell_commands(self, commands, trace=False):
-    '''
-    Execute each shell command in a separate process.
-    Wait for each command to complete, except those starting with '&amp;'
-    '''
-    if g.isString(commands): commands = [commands]
-    for command in commands:
-        wait = not command.startswith('&amp;')
-        if command.startswith('&amp;'): command = command[1:].strip()
-        proc = subprocess.Popen(command, shell=True)
-        if wait: proc.communicate()
+<t tx="ekr.20180901051603.1">def test_bug3(self):
+    # https://github.com/edreamleo/make-stub-files/issues/3
+    tag = 'test_bug3'
+    s = (
+        'class UnsupportedAlgorithm(Exception):\n'
+        '    def __init__(self, message: Any, reason: Optional[str]=None) -&gt; None:\n'
+        '        pass\n'
+    )
+    expected = [
+        'class UnsupportedAlgorithm(Exception):\n',
+        '    def __init__(self, message: Any, reason: Optional[str]=None) -&gt; None: ...\n',
+    ]
+    controller = Controller()
+    node = ast.parse(s, filename=tag, mode='exec')
+    st = StubTraverser(controller=controller)
+    # From StubTraverser.run.
+    st.parent_stub = Stub(kind='root', name='&lt;new-stubs&gt;')
+    st.visit(node)
+    # Allocate a StringIo file for output_stubs.
+    st.output_file = io.StringIO()
+    st.output_stubs(st.parent_stub)
+    # Test.
+    lines = g.splitLines(st.output_file.getvalue())
+    self.assertEqual(lines, expected)
 </t>
 <t tx="ekr.20200402143651.1"></t>
-<t tx="ekr.20210803060314.1"></t>
+<t tx="ekr.20210803055042.1">class TestMakeStubFiles(unittest.TestCase):  # pragma: no cover
+    """Unit tests for make_stub_files.py"""
+    @others
+</t>
 <t tx="ekr.20210803060400.1"></t>
-<t tx="ekr.20210803060804.1">- Convert to @file
-- Write on new unit test.
-  - Adjust args.
-
-- Rewrite the readme.
-</t>
-<t tx="ekr.20210803061104.1">def main():
-    """Run commands specified by sys.argv."""
-    usage = '\n'.join([
-        '',
-        '    leoAst.py --help',
-        '    leoAst.py [--fstringify | --fstringify-diff | --orange | --orange-diff] PATHS',
-        '    leoAst.py --py-cov [ARGS]',
-        '    leoAst.py --pytest [ARGS]',
-        '    leoAst.py --unittest [ARGS]',
-        '\n',
-        'examples...'
-        '',
-        '   --py-cov "-f TestOrange"',
-        '   --pytest "-f TestOrange"',
-        '   --unittest TestOrange',
-    ])
-    parser = argparse.ArgumentParser(description=None, usage=usage)
-    parser.add_argument('PATHS', nargs='*', help='directory or list of files')
-    group = parser.add_mutually_exclusive_group(required=False)  # Don't require any args.
-    add = group.add_argument
-    add('--fstringify', dest='f', action='store_true', help='leonine fstringify')
-    add('--fstringify-diff', dest='fd', action='store_true', help='show fstringify diff')
-    add('--orange', dest='o', action='store_true', help='leonine Black')
-    add('--orange-diff', dest='od', action='store_true', help='show orange diff')
-    add('--py-cov', dest='pycov', metavar='ARGS', nargs='?', const=[], default=False, help='run pytest --cov')
-    add('--pytest', dest='pytest', metavar='ARGS', nargs='?', const=[], default=False, help='run pytest')
-    add('--unittest', dest='unittest', metavar='ARGS', nargs='?', const=[], default=False, help='run unittest')
-    args = parser.parse_args()
-    # g.printObj(args, tag='ARGS')
-    # Default to --py-cov
-    if not any(getattr(args, z) for z in ('f', 'fd', 'o', 'od', 'pycov', 'pytest', 'unittest')):
-        if not pytest:
-            print('pytest not found')
-            return
-        pycov_args = sys.argv + [
-            '--cov-report=html',
-            '--cov-report=term-missing',
-            '--cov=leo.core.leoAst',
-            'leo/core/leoAst.py',
-        ]
-        pytest.main(args=pycov_args)
-        return  # Seems necessary.
-    files = args.PATHS
-    if len(files) == 1 and os.path.isdir(files[0]):
-        files = glob.glob(f"{files[0]}{os.sep}*.py")
-    if args.f:
-        fstringify_command(files)
-    if args.fd:
-        fstringify_diff_command(files)
-    if args.o:
-        orange_command(files)
-    if args.od:
-        orange_diff_command(files)
-    if isinstance(args.pycov, (str, list)):
-        if not pytest:
-            print('pytest not found')
-            return
-        sys.argv.remove('--py-cov')
-        pycov_args = sys.argv + [
-            '--cov-report=html',
-            '--cov-report=term-missing',
-            '--cov=leo.core.leoAst',
-            'leo/core/leoAst.py',
-        ]
-        pytest.main(args=pycov_args)
-        return
-    if isinstance(args.pytest, (str, list)):
-        if not pytest:
-            print('pytest not found')
-            return
-        sys.argv.remove('--pytest')
-        pytest.main(args=sys.argv)
-    if isinstance(args.unittest, (str, list)):
-        sys.argv.remove('--unittest')
-        unittest.main()
+<t tx="ekr.20210803131439.1">@language rest
+@wrap
+
+https://github.com/edreamleo/make-stub-files/issues/10
+https://github.com/edreamleo/make-stub-files/issues/11
+https://github.com/edreamleo/make-stub-files/issues/13
+
+@language python
+@nosearch</t>
+<t tx="ekr.20210804020706.1">g.cls()
+command = r"python -m pytest --cov-report html --cov-report term-missing --cov make_stub_files make_stub_files.py"
+g.execute_shell_commands(command, trace=False)
+</t>
+<t tx="ekr.20210804021331.1">g.cls()
+"""Run make_stub_files with the given set of arguments."""
+cfg = r'c:\users\Edward~1\make_stub_files.cfg'
+msf = 'make_stub_files.py'
+src = 'make_stub_files.py'
+command = f"python {msf} -c {cfg} -o -v {src}"
+g.execute_shell_commands(command, trace=False)</t>
+<t tx="ekr.20210804025215.1">c.backup_helper(sub_dir='MakeStubFiles')
+</t>
+<t tx="ekr.20210804060105.1">g.cls()
+test = '' # '.TestMakeStubFiles.test_bug3' # Run all tests.
+command = f"python -m unittest make_stub_files{test}"
+g.execute_shell_commands(command, trace=False)</t>
+<t tx="ekr.20210804070157.1"></t>
+<t tx="ekr.20210804103146.1">def test_pattern_class(self):
+    g = LeoGlobals() # Use the g available to the script.
+    table = (
+        # s,  Pattern.find_s, Pattern.repl_s, expected
+        ('aabbcc', '(a+)(b+)(c+)$', r'\3\2\1', 'ccbbaa'),
+        ('[str]', r'\[str\]$', 'xxx', 'xxx'), # Guido bug.
+        ('s3', r's[1-3]?\b$', 'str', 'str'), # lengthening bug.
+        ('s', 's', 'str', 'str'),
+        ('abc', 'abc', 'ABC', 'ABC'),
+        ('str(str)', 'str(*)', 'str', 'str'),
+        ('[whatever]', '[*]', 'List[*]', 'List[whatever]'), # * on the RHS.
+        ('(int,str)', '(*)', 'Tuple[*]', 'Tuple[int,str]'), # Guido bug 2.
+        ('abcxyz', 'abc*', 'xxx', 'xxx'), # New test for trailing *.
+        ('list(self.regex.finditer(str))','list(*)','List[*]',
+         'List[self.regex.finditer(str)]'),
+    )
+    for s, find, repl, expected in table:
+        pattern = Pattern(find, repl)
+        result = pattern.match_entire_string(s)
+        self.assertTrue(result, msg=repr(s))
+        aList = pattern.all_matches(s)
+        self.assertTrue(len(aList) == 1, msg=repr(aList))
+        found, s2 = pattern.match(s)
+        self.assertTrue(found, msg=f"after pattern.match({s!r})")
+        assert s2 == expected, (s, pattern, 'expected', expected, 'got', s2)
+    p1 = Pattern('abc','xyz')
+    p2 = Pattern('abc','xyz')
+    p3 = Pattern('abc','pdq')
+    self.assertEqual(p1, p2)
+    self.assertNotEqual(p1, p3)
+    self.assertNotEqual(p2, p3)
+    aSet = set()
+    aSet.add(p1)
+    self.assertTrue(p1 in aSet)
+    self.assertTrue(p2 in aSet)
+    self.assertFalse(p3 in aSet)
+    self.assertEqual(list(aSet), [p1])
+    self.assertEqual(list(aSet), [p2])
+    aSet.add(p3)
+    self.assertTrue(p1.match_entire_string('abc'))
+    self.assertFalse(p1.match_entire_string('abcx'))
+</t>
+<t tx="ekr.20210804104214.1"></t>
+<t tx="ekr.20210804105256.1">def test_reduce_numbers(self):
+    a, c, f, i, l, n = ('Any', 'complex', 'float', 'int', 'long', 'number')
+    table = (
+        ([i,i],     [i]),
+        ([i],       [i]),
+        ([f, i],    [f]),
+        ([c, i],    [c]),
+        ([l, a],    [a, l]),
+    )
+    for aList, expected in table:
+        got = ReduceTypes().reduce_numbers(aList)
+        self.assertEqual(expected, got, msg=repr(aList))
+</t>
+<t tx="ekr.20210804111613.1">def test_reduce_types(self):
+
+    a, c, f, i, l, n = ('Any', 'complex', 'float', 'int', 'long', 'number')
+    none = 'None'
+    x = 'xyzzy'
+    y = 'pdq'
+    table = (
+        ([i,i],         i),
+        ([i],           i),
+        ([f, i],        f),
+        ([c, i],        c),
+        ([l, a],        'Union[Any, long]'),
+        # Handle None
+        ([None],        none),
+        ([None, None],  none),
+        ([None, a, c],  'Optional[Union[Any, complex]]'),
+        # Handle unknown types, and special cases
+        ([i, x],        'Union[Any, int]'),
+        ([None, x],     'Optional[Any]'),
+        ([none, x],     'Optional[Any]'),
+        (['', x],       'Optional[Any]'),
+        ([none, x, c],  'Optional[Union[Any, complex]]'),
+        ([x, y],        'Any'),
+        # Collection merging.  More could be done...
+        (['Dict[int, str]', 'Dict[Any, str]'],          'Union[Dict[Any, str], Dict[int, str]]'),
+        (['List[int, str]', 'List[Any, str]'],          'Union[List[Any, str], List[int, str]]'),
+        (['Union[int, str]', 'Union[Any, str]'],        'Union[Union[Any, str], Union[int, str]]'),
+        (['Union[int, str]', 'int', 'Union[Any, str]'], 'Union[Union[Any, str], Union[int, str], int]'),
+        (['Tuple[xyz, pdq]'],                           'Tuple[Any, Any]'),
+    )
+    for aList, expected in table:
+        got = ReduceTypes(aList).reduce_types()
+        self.assertEqual(expected, got, msg=repr(aList))
+</t>
+<t tx="ekr.20210804111803.1">def test_split_types(self):
+    table = (
+        ('list',                    ['list']),
+        ('List[a,b]',               ['List[a,b]']),
+        ('List[a,b], List[c,d]',    ['List[a,b]', 'List[c,d]']),
+    )
+    for s, expected in table:
+        got = ReduceTypes().split_types(s)
+        self.assertEqual(expected, got, msg=repr(s))
+</t>
+<t tx="ekr.20210804111915.1">def test_st_find(self):
+
+    s = """\
+def is_known_type(s: str) -&gt; Union[Any,bool]: ...
+def main() -&gt; None: ...
+def merge_types(a1: Any, a2: Any) -&gt; str: ...
+
+class AstFormatter:
+    def format(self, node: Node) -&gt; Union[Any,str]: ...
+        def helper(self): -&gt; None
+    def visit(self, node: Node) -&gt; str: ...
+    def do_ClassDef(self, node: Node) -&gt; str: ...
+    def do_FunctionDef(self, node: Node) -&gt; str: ...
+"""
+    g = LeoGlobals() # Use the g available to the script.
+    st = StubTraverser(controller=g.NullObject())
+    d, root = st.parse_stub_file(s, root_name='&lt;root&gt;')  # Root *is* used below.
+    if 0:
+        print(st.trace_stubs(root, header='root'))
+    stub1 = Stub(kind='class', name='AstFormatter')
+    stub2 = Stub(kind='def', name='format', parent=stub1, stack=['AstFormatter'])
+    stub3 = Stub(kind='def', name='helper', parent = stub2, stack=['AstFormatter', 'format'])
+    # stub4 = Stub(kind='def', name='main')
+    for stub in (stub1, stub2, stub3,):  # (stub1, stub2, stub3):
+        found = st.find_stub(stub, root)
+        id_found = found and id(found) or None
+        if 0:
+            print('found  %s =&gt; %9s %35s ==&gt; %s' % (id(stub), id_found, stub, found))
+        found = st.find_parent_stub(stub, root)
+        id_found = found and id(found) or None
+        if 0:
+            print('parent %s =&gt; %9s %35s ==&gt; %s' % (id(stub), id_found, stub, found))
+</t>
+<t tx="ekr.20210804112211.1">def test_st_flatten_stubs(self):
+    s = """\
+    def is_known_type(s: str) -&gt; Union[Any,bool]: ...
+    def main() -&gt; None: ...
+    def merge_types(a1: Any, a2: Any) -&gt; str: ...
+    
+    class AstFormatter:
+        def format(self, node: Node) -&gt; Union[Any,str]: ...
+            def helper(self): -&gt; None
+        def visit(self, node: Node) -&gt; str: ...
+        def do_ClassDef(self, node: Node) -&gt; str: ...
+        def do_FunctionDef(self, node: Node) -&gt; str: ...
+    """
+    g = LeoGlobals()  # Use the g available to the script.
+    st = StubTraverser(controller=g.NullObject())
+    d, root = st.parse_stub_file(s, root_name='&lt;root&gt;')
+    if 0:
+        print(st.trace_stubs(root, header='root'))
+    aList = st.flatten_stubs(root)
+    self.assertTrue(aList)
+    if 0:
+        for i, stub in enumerate(aList):
+            print('%2s %s' % (i, stub))
+    for stub in aList:
+        found = st.find_stub(stub, root)
+        self.assertTrue(found, msg=repr(stub))
+</t>
+<t tx="ekr.20210804112405.1">def test_st_merge_stubs(self):
+    # To do:
+    # - Test between-stub lines and leading lines.
+    # - Round-trip tests!
+    &lt;&lt; old_stubs &gt;&gt;
+    &lt;&lt; new_stubs &gt;&gt;
+    g = LeoGlobals() # Use the g available to the script.
+    st = StubTraverser(controller=g.NullObject())
+    # dump('old_s', old_s)
+    # dump('new_s', new_s)
+    old_d, old_root = st.parse_stub_file(old_s, root_name='&lt;old-root&gt;')
+    new_d, new_root = st.parse_stub_file(new_s, root_name='&lt;new-root&gt;')
+    if 0:
+        dump_dict('old_d', old_d)
+        dump_dict('new_d', new_d)
+        print(st.trace_stubs(old_root, header='trace_stubs(old_root)'))
+        print(st.trace_stubs(new_root, header='trace_stubs(new_root)'))
+    if 0:  # separate unit test. Passed.
+        aList = st.sort_stubs_by_hierarchy(new_root)
+        dump_list(aList, 'after sort_stubs_by_hierarcy')
+    new_stubs = new_d.values()
+    st.merge_stubs(new_stubs, old_root, new_root, trace=False)
+    if 0:
+        print(st.trace_stubs(old_root, header='trace_stubs(old_root)'))
+</t>
+<t tx="ekr.20210804112405.3"># To be INSERTED (They exist in new stubs, but not here.)
+# def is_known_type(s: str) -&gt; Union[Any,bool]: ...
+# def reduce_numbers(aList: List[Any]) -&gt; List[Any]: ...
+# class AstFormatter:
+    # def format(self, node: Node) -&gt; Union[Any,str]: ...
+    # def visit(self, node: Node) -&gt; str: ...
+    # def do_ClassDef(self, node: Node) -&gt; str: ...
+    # def do_FunctionDef(self, node: Node) -&gt; str: ...
+old_s = """\
+def main() -&gt; None: ...
+def merge_types(a1: Any, a2: Any) -&gt; str: ...
+def pdb(self) -&gt; None: ...
+def reduce_types(aList: List[Any], name: str=None, trace: bool=False) -&gt; Any: ...
+class Pattern(object):
+    def __init__(self, find_s: str, repl_s: str='') -&gt; None: ...
+    def __eq__(self, obj: Any) -&gt; bool: ...
+    def __ne__(self, obj: Any) -&gt; bool: ...
+    def __hash__(self) -&gt; int: ...
+    def __repr__(self) -&gt; str: ...
+    def is_balanced(self) -&gt; bool: ...
+    def is_regex(self) -&gt; Any: ...
+        #   0: return self.find_s.endswith('$')
+        # ? 0: return self.find_s.endswith(str)
+"""
+</t>
+<t tx="ekr.20210804112405.4"># To be DELETED (They exist in old_stubs, but not here)
+# class Pattern(object):
+    # def __init__(self, find_s: str, repl_s: str='') -&gt; None: ...
+    # def __eq__(self, obj: Any) -&gt; bool: ...
+    # def __ne__(self, obj: Any) -&gt; bool: ...
+    # def __hash__(self) -&gt; int: ...
+    # def __repr__(self) -&gt; str: ...
+    # def is_balanced(self) -&gt; bool: ...
+    # def is_regex(self) -&gt; Any: ...
+        # #   0: return self.find_s.endswith('$')
+        # # ? 0: return self.find_s.endswith(str)
+new_s = """\
+def is_known_type(s: str) -&gt; Union[Any,bool]: ...
+def main() -&gt; None: ...
+def merge_types(a1: Any, a2: Any) -&gt; str: ...
+def pdb(self) -&gt; None: ...
+def reduce_numbers(aList: List[Any]) -&gt; List[Any]: ...
+def reduce_types(aList: List[Any], name: str=None, trace: bool=False) -&gt; Any: ...
+
+class AstFormatter:
+    def format(self, node: Node) -&gt; Union[Any,str]: ...
+    def visit(self, node: Node) -&gt; str: ...
+    def do_ClassDef(self, node: Node) -&gt; str: ...
+    def do_FunctionDef(self, node: Node) -&gt; str: ...
+"""
+</t>
+<t tx="ekr.20210804112556.1">def test_stub_class(self):
+    g = LeoGlobals()  # Use the g available to the script.
+    # Test equality...
+    stub1 = Stub(kind='def', name='foo')
+    stub2 = Stub(kind='class', name='foo')
+    stub3 = Stub(kind='def', name='bar')
+    stub4 = Stub(kind='def', name='foo')
+    stub4.out_list = ['xyzzy']  # Contents of out_list must not affect equality!
+    aList = [stub1, stub3]
+    self.assertNotEqual(stub1, stub2)
+    self.assertNotEqual(stub1, stub3)
+    self.assertEqual(stub1, stub4)
+    self.assertTrue(stub1 in aList)
+    self.assertFalse(stub2 in aList)
+    self.assertTrue(stub3 in aList)
+    # Test __hash__
+    d = {stub1: 'stub1'}
+    self.assertTrue(stub1 in d)
+    self.assertFalse(stub2 in d)
+    # Test parents and level.
+    stub_1 = Stub(kind='def', name='stub_1')
+    stub_2 = Stub(kind='def', name='stub_2', parent=stub_1, stack=['stub_1'])
+    stub_3 = Stub(kind='def', name='stub_3', parent=stub_2, stack=['stub_1', 'stub_2'])
+    self.assertEqual(stub_1.parents(), [], msg=repr(stub_1.parents()))
+    self.assertEqual(stub_2.parents(), ['stub_1'], msg=repr(stub_2.parents()))
+    self.assertEqual(stub_3.parents(), ['stub_1', 'stub_2'], msg=repr(stub_3.parents()))
+    self.assertEqual(stub_1.level(), 0)
+    self.assertEqual(stub_2.level(), 1)
+    self.assertEqual(stub_3.level(), 2)
+</t>
+<t tx="ekr.20210804153200.1">"""Convert all following @test nodes."""
+g.cls()
+
+def body(p):
+    def_s = f"def {headline(p)}(self):\n"
+    lines = [' '*4 + z.rstrip()+'\n' for z in g.splitLines(p.b) if '@others' not in z]
+    return def_s + ''.join(lines)
+
+def headline(p):
+    return p.h[1:].replace('-','_').replace(' ','_').replace('__', '_')
+
+while p:
+    if p.h.startswith('@test'):
+        if 1:  # Change.
+            print(p.h)
+            p.b = body(p)  # body first, to use original headline.
+            p.h = headline(p)
+            p.setDirty()
+        else:  # Report.
+            h = headline(p)
+            b = body(p)
+            print(h.rstrip())
+            print(b.rstrip())
+            print('-'*40)
+    p.moveToThreadNext()
+c.redraw()
+</t>
+<t tx="ekr.20210804161223.1"># Found 3 marked nodes</t>
+<t tx="ekr.20210804214511.1">def do_Constant(self, node):  # #13
+    return repr(node.value)
+</t>
+<t tx="ekr.20210804215428.1"></t>
+<t tx="ekr.20210805051331.1"></t>
+<t tx="ekr.20210805053830.1">
+@language rest
+@wrap
+https://github.com/edreamleo/make-stub-files/issues/18
+
+
+@language python
+@nosearch
+</t>
+<t tx="ekr.20210805054028.1">@language rest
+@wrap
+
+https://github.com/edreamleo/make-stub-files/issues/19
+
+@language python
+@nosearch
 </t>
 </tnodes>
 </leo_file>
diff --git a/make_stub_files.py b/make_stub_files.py
index 7f9de22..fbb6efa 100755
--- a/make_stub_files.py
+++ b/make_stub_files.py
@@ -1,7 +1,7 @@
 #@+leo-ver=5-thin
 #@+node:ekr.20160318141204.1: * @file make_stub_files.py
 #!/usr/bin/env python
-'''
+"""
 This script makes a stub (.pyi) file in the output directory for each
 source file listed on the command line (wildcard file names are supported).
 
@@ -10,11 +10,11 @@
 This file is in the public domain.
 
 Written by Edward K. Ream.
-'''
+"""
 #@+<< imports >>
 #@+node:ekr.20160318141204.2: **  << imports >> (make_stub_files.py)
+import argparse
 import ast
-# from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
 from collections import OrderedDict
     # Requires Python 2.7 or above. Without OrderedDict
     # the configparser will give random order for patterns.
@@ -23,88 +23,75 @@
 except ImportError:
     import configparser  # Python 3
 import glob
-import optparse
 import os
 import re
 import subprocess
 import sys
 import time
 import types
+import unittest
 try:
     import StringIO as io  # Python 2
 except ImportError:
     import io  # Python 3
-
 #@-<< imports >>
 isPython3 = sys.version_info >= (3, 0, 0)
 #@+others
 #@+node:ekr.20160318141204.3: **   type functions
 #@+node:ekr.20160318141204.4: *3* is_known_type
 def is_known_type(s):
-    '''
+    """
     Return True if s is nothing but a single known type.
     Recursively test inner types in square brackets.
-    '''
+    """
     return ReduceTypes().is_known_type(s)
 
-#@+node:ekr.20160318141204.5: *3* merge_types (not used)
-def merge_types(a1, a2):
-    '''
-    a1 and a2 may be strings or lists.
-    return a list containing both of them, flattened, without duplicates.
-    '''
-    # Only useful if visitors could return either lists or strings.
-    assert a1 is not None
-    assert a2 is not None
-    r1 = a1 if isinstance(a1, (list, tuple)) else [a1]
-    r2 = a2 if isinstance(a2, (list, tuple)) else [a2]
-    return sorted(set(r1 + r2))
-
 #@+node:ekr.20160318141204.6: *3* reduce_types
 def reduce_types(aList, name=None, trace=False):
-    '''
+    """
     Return a string containing the reduction of all types in aList.
-    The --trace-reduce command-line option sets trace=True.
+    The --trace-reduce option sets trace=True.
     If present, name is the function name or class_name.method_name.
-    '''
+    """
     return ReduceTypes(aList, name, trace).reduce_types()
 
 #@+node:ekr.20160318141204.7: **   utility functions
 # Top-level functions
 #@+node:ekr.20160318141204.8: *3* dump
-def dump(title, s=None):
+def dump(title, s=None):  # pragma: no cover
     if s:
         print('===== %s...\n%s\n' % (title, s.rstrip()))
     else:
         print('===== %s...\n' % title)
 #@+node:ekr.20160318141204.9: *3* dump_dict
-def dump_dict(title, d):
-    '''Dump a dictionary with a header.'''
+def dump_dict(title, d):  # pragma: no cover
+    """Dump a dictionary with a header."""
     dump(title)
     for z in sorted(d):
         print('%30s %s' % (z, d.get(z)))
     print('')
 
 #@+node:ekr.20160318141204.10: *3* dump_list
-def dump_list(title, aList):
-    '''Dump a list with a header.'''
+def dump_list(title, aList):  # pragma: no cover
+    """Dump a list with a header."""
     dump(title)
     for z in aList:
         print(z)
     print('')
 #@+node:ekr.20160318141204.11: *3* main
-def main():
-    '''
+def main():  # pragma: no cover
+    """
     The driver for the stand-alone version of make-stub-files.
-    All options come from ~/stubs/make_stub_files.cfg.
-    '''
+    All options come from the configuration file.
+    """
     controller = Controller()
     controller.scan_command_line()
     controller.scan_options()
-    controller.run()
+    for fn in controller.files:
+        controller.make_stub_file(fn)
 #@+node:ekr.20160318141204.12: *3* pdb
-def pdb(self):
-    '''Invoke a debugger during unit testing.'''
+def pdb(self):  # pragma: no cover
+    """Invoke a debugger during unit testing."""
     try:
         import leo.core.leoGlobals as leo_g
         assert leo_g
@@ -114,17 +101,15 @@ def pdb(self):
         pdb.set_trace()
 #@+node:ekr.20160318141204.13: *3* truncate
 def truncate(s, n):
-    '''Return s truncated to n characters.'''
+    """Return s truncated to n characters."""
     return s if len(s) <= n else s[: n - 3] + '...'
 #@+node:ekr.20160318141204.14: **  class AstFormatter
-
-
 class AstFormatter:
-    '''
+    """
     A class to recreate source code from an AST.
     
     This does not have to be perfect, but it should be close.
-    '''
+    """
     # pylint: disable=consider-using-enumerate
     #@+others
     #@+node:ekr.20160318141204.15: *3*  f.Entries
@@ -132,29 +117,31 @@ class AstFormatter:
     # Entries...
     #@+node:ekr.20160318141204.17: *4* f.format
     def format(self, node):
-        '''Format the node (or list of nodes) and its descendants.'''
+        """Format the node (or list of nodes) and its descendants."""
         self.level = 0
         val = self.visit(node)
-        # pylint: disable=consider-using-ternary
-        return val and val.strip() or ''
+        return val.strip()  # val is a string.
     #@+node:ekr.20160318141204.18: *4* f.visit
     def visit(self, node):
-        '''Return the formatted version of an Ast node, or list of Ast nodes.'''
+        """Return the formatted version of an Ast node, or list of Ast nodes."""
+        tag = 'AstFormatter.visit'
+        name = node.__class__.__name__
         if isinstance(node, (list, tuple)):
             return ','.join([self.visit(z) for z in node])
         if node is None:
             return 'None'
-        assert isinstance(node, ast.AST), node.__class__.__name__
+        if not isinstance(node, ast.AST):
+            # #13: Insert an error comment directly into the output.
+            return f"\n#{tag}: not an AST node: {name}\n"
         method_name = 'do_' + node.__class__.__name__
-        try:
-            method = getattr(self, method_name)
-        except AttributeError:
-            return ''
-
-        s = method(node)
-        # assert type(s) == type('abc'), (node, type(s))
-        assert g.isString(s), type(s)
-        return s
+        # #13: *Never* ignore missing visitors!
+        method = getattr(self, method_name, None)
+        if method:
+            s = method(node)
+            assert g.isString(s), s.__class__.__name__
+            return s
+        # #13: Insert an error comment directly into the output.
+        return f"\n#{tag}: no visitor: do_{name}\n"
     #@+node:ekr.20160318141204.19: *3* f.Contexts
 
     # Contexts...
@@ -201,7 +188,7 @@ def do_ClassDef(self, node):
     #                expr? returns)
 
     def do_FunctionDef(self, node):
-        '''Format a FunctionDef node.'''
+        """Format a FunctionDef node."""
         result = []
         if node.decorator_list:
             for z in node.decorator_list:
@@ -241,12 +228,12 @@ def do_Lambda(self, node):
 
     #@+node:ekr.20160318141204.26: *4* f.Expr
     def do_Expr(self, node):
-        '''An outer expression: must be indented.'''
+        """An outer expression: must be indented."""
         return self.indent('%s\n' % self.visit(node.value))
 
     #@+node:ekr.20160318141204.27: *4* f.Expression
     def do_Expression(self, node):
-        '''An inner expression: do not indent.'''
+        """An inner expression: do not indent."""
         return '%s\n' % self.visit(node.body)
 
     #@+node:ekr.20160318141204.28: *4* f.GeneratorExp
@@ -282,7 +269,7 @@ def do_Store(self, node):
     #                arg? kwarg, expr* defaults)
 
     def do_arguments(self, node):
-        '''Format the arguments node.'''
+        """Format the arguments node."""
         kind = self.kind(node)
         assert kind == 'arguments', kind
         args = [self.visit(z) for z in node.args]
@@ -360,6 +347,9 @@ def do_keyword(self, node):
         # This is a keyword *arg*, not a Python keyword!
         return '%s=%s' % (node.arg, value)
 
+    #@+node:ekr.20210804214511.1: *4* f.Constant
+    def do_Constant(self, node):  # #13
+        return repr(node.value)
     #@+node:ekr.20160318141204.37: *4* f.comprehension
     def do_comprehension(self, node):
         result = []
@@ -431,7 +421,7 @@ def do_Num(self, node):
         return repr(node.n)
 
     #@+node:ekr.20160318141204.46: *4* f.Repr
-    def do_Repr(self, node):  # Python 2.x only
+    def do_Repr(self, node):  # pragma: no cover (Python 2.x only)
         return 'repr(%s)' % self.visit(node.value)
 
     #@+node:ekr.20160318141204.47: *4* f.Slice
@@ -449,7 +439,7 @@ def do_Slice(self, node):
 
     #@+node:ekr.20160318141204.48: *4* f.Str
     def do_Str(self, node):
-        '''This represents a string constant.'''
+        """This represents a string constant."""
         return repr(node.s)
 
     #@+node:ekr.20160318141204.49: *4* f.Subscript
@@ -564,7 +554,7 @@ def do_ExceptHandler(self, node):
         return ''.join(result)
 
     #@+node:ekr.20160318141204.65: *4* f.Exec
-    def do_Exec(self, node):  # Python 2.x only
+    def do_Exec(self, node):  # pragma: no cover (Python 2.x only)
         body = self.visit(node.body)
         args = []  # Globals before locals.
         if getattr(node, 'globals', None):
@@ -629,7 +619,7 @@ def do_Import(self, node):
 
     #@+node:ekr.20160318141204.70: *5* f.get_import_names
     def get_import_names(self, node):
-        '''Return a list of the the full file names in the import statement.'''
+        """Return a list of the the full file names in the import statement."""
         result = []
         for ast2 in node.names:
             if self.kind(ast2) == 'alias':
@@ -661,7 +651,7 @@ def do_Pass(self, node):
         return self.indent('pass\n')
 
     #@+node:ekr.20160318141204.74: *4* f.Print
-    def do_Print(self, node):  # Python 2.x only
+    def do_Print(self, node):  # pragma: no cover (Python 2.x only)
         vals = []
         for z in node.values:
             vals.append(self.visit(z))
@@ -829,7 +819,7 @@ def do_YieldFrom(self, node):
 
     #@+node:ekr.20160318141204.87: *4* f.kind
     def kind(self, node):
-        '''Return the name of node's class.'''
+        """Return the name of node's class."""
         return node.__class__.__name__
     #@+node:ekr.20160318141204.88: *4* f.indent
     def indent(self, s):
@@ -838,7 +828,7 @@ def indent(self, s):
     #@@nobeautify
 
     def op_name(self, node, strict=True):
-        '''Return the print name of an operator node.'''
+        """Return the print name of an operator node."""
         d = {
             # Binary operators.
             'Add': '+',
@@ -885,18 +875,16 @@ def op_name(self, node, strict=True):
         return name
     #@-others
 #@+node:ekr.20160318141204.90: ** class AstArgFormatter (AstFormatter)
-
-
 class AstArgFormatter(AstFormatter):
-    '''
+    """
     Just like the AstFormatter class, except it prints the class
     names of constants instead of actual values.
-    '''
+    """
     #@+others
     #@+node:ekr.20160318141204.91: *3* sf.Constants & Name
     # Return generic markers to allow better pattern matches.
 
-    def do_BoolOp(self, node):  # Python 2.x only.
+    def do_BoolOp(self, node):  # pragma: no cover (Python 2.x only)
         return 'bool'
 
     def do_Bytes(self, node):  # Python 3.x only.
@@ -909,25 +897,23 @@ def do_Num(self, node):
         return 'number'  # return repr(node.n)
 
     def do_Str(self, node):
-        '''This represents a string constant.'''
+        """This represents a string constant."""
         return 'str'  # return repr(node.s)
     #@-others
 #@+node:ekr.20160318141204.125: ** class Controller
-
-
 class Controller:
-    '''
-    A class to make Python stub (.pyi) files in the ~/stubs directory for
-    every file mentioned in the [Source Files] section of
-    ~/stubs/make_stub_files.cfg.
-    '''
+    """
+    Make Python stub (.pyi) files in the ~/stubs directory for every file
+    in the [Source Files] section of the configuration file.
+    """
     #@+others
     #@+node:ekr.20160318141204.126: *3* msf.ctor
     def __init__(self):
-        '''Ctor for Controller class.'''
+        """Ctor for Controller class."""
         self.options = {}
         # Ivars set on the command line...
         self.config_fn = None
+        self.enable_coverage_tests = False
         self.enable_unit_tests = False
         self.files = []
         # Ivars set in the config file...
@@ -953,19 +939,16 @@ def __init__(self):
         self.regex_patterns = []
     #@+node:ekr.20160318141204.127: *3* msf.finalize
     def finalize(self, fn):
-        '''Finalize and regularize a filename.'''
-        fn = os.path.expanduser(fn)
-        fn = os.path.abspath(fn)
-        fn = os.path.normpath(fn)
-        return fn
+        """Finalize and regularize a filename."""
+        return os.path.normpath(os.path.abspath(os.path.expanduser(fn)))
     #@+node:ekr.20160318141204.128: *3* msf.make_stub_file
     directory_warning_given = False
 
     def make_stub_file(self, fn):
-        '''
+        """
         Make a stub file in ~/stubs for all source files mentioned in the
-        [Source Files] section of ~/stubs/make_stub_files.cfg
-        '''
+        [Source Files] section of the configuration file.
+        """
         global g_input_file_name
         extension = fn[fn.rfind('.'):]
         if not extension == '.py' and not (self.force_pyx and extension == '.pyx'):
@@ -1002,46 +985,26 @@ def make_stub_file(self, fn):
         # Process s.
         node = ast.parse(s, filename=fn, mode='exec')
         StubTraverser(controller=self).run(node)
-    #@+node:ekr.20160318141204.129: *3* msf.run
-    def run(self):
-        '''
-        Make stub files for all files.
-        Do nothing if the output directory does not exist.
-        '''
-        if self.enable_unit_tests:
-            self.run_all_unit_tests()
-        if not self.files:
-            if not self.enable_unit_tests:
-                print('no input files')
-            return
-        for fn in self.files:
-            self.make_stub_file(fn)
-    #@+node:ekr.20160318141204.130: *3* msf.run_all_unit_tests
-    def run_all_unit_tests(self):
-        '''Run all unit tests in the make_stub_files/test directory.'''
-        import unittest
-        loader = unittest.TestLoader()
-        suite = loader.discover(os.path.abspath('.'),
-                                pattern='test*.py',
-                                top_level_dir=None)
-        unittest.TextTestRunner(verbosity=1).run(suite)
     #@+node:ekr.20160318141204.131: *3* msf.scan_command_line
     def scan_command_line(self):
-        '''Set ivars from command-line arguments.'''
-        # This automatically implements the --help option.
-        usage = "usage: make_stub_files.py [options] file1, file2, ..."
-        parser = optparse.OptionParser(usage=usage)
-        add = parser.add_option
-        add('-c', '--config', dest='fn',
+        """Set ivars from command-line arguments."""
+        # The parser implements the --help option.
+        description='make_stub_file: Create stub (.pyi) files from python files'
+        usage = 'make_stub_files.py [options] file1, file2, ...'
+        parser = argparse.ArgumentParser(description=description, usage=usage)
+        add = parser.add_argument
+        add('files', metavar='FILE', type=str, nargs='+',
+            help='input files')
+        add('-c', '--config', dest='fn', metavar='FILE',
             help='full path to configuration file')
         add('-d', '--dir', dest='dir',
             help='full path to the output directory')
+        add('-f', '--force-pyx', action='store_true', default=False,
+            help='force the parsing of .pyx files')
         add('-o', '--overwrite', action='store_true', default=False,
             help='overwrite existing stub (.pyi) files')
         add('-s', '--silent', action='store_true', default=False,
             help='run without messages')
-        add('-t', '--test', action='store_true', default=False,
-            help='run unit tests on startup')
         add('--trace-matches', action='store_true', default=False,
             help='trace Pattern.matches')
         add('--trace-patterns', action='store_true', default=False,
@@ -1056,25 +1019,23 @@ def scan_command_line(self):
             help='verbose output in .pyi file')
         add('-w', '--warn', action='store_true', default=False,
             help='warn about unannotated args')
-        add('--force-pyx', action='store_true', default=False, help='force the parsing of .pyx files')
-        # Parse the options
-        options, args = parser.parse_args()
-        # Handle the options...
-        self.enable_unit_tests = options.test
-        self.overwrite = options.overwrite
-        self.silent = options.silent
-        self.trace_matches = options.trace_matches
-        self.trace_patterns = options.trace_patterns
-        self.trace_reduce = options.trace_reduce
-        self.trace_visitors = options.trace_visitors
-        self.update_flag = options.update
-        self.verbose = options.verbose
-        self.warn = options.warn
-        self.force_pyx = options.force_pyx
-        if options.fn:
-            self.config_fn = options.fn
-        if options.dir:
-            dir_ = options.dir and options.dir.strip()
+        # Parse.
+        args = parser.parse_args()
+        # Handle the args...
+        self.overwrite = args.overwrite
+        self.silent = args.silent
+        self.trace_matches = args.trace_matches
+        self.trace_patterns = args.trace_patterns
+        self.trace_reduce = args.trace_reduce
+        self.trace_visitors = args.trace_visitors
+        self.update_flag = args.update
+        self.verbose = args.verbose
+        self.warn = args.warn
+        self.force_pyx = args.force_pyx
+        if args.fn:
+            self.config_fn = args.fn
+        if args.dir:
+            dir_ = args.dir and args.dir.strip()
             dir_ = self.finalize(dir_)
             g.trace('dir', dir_)
             if os.path.exists(dir_):
@@ -1083,18 +1044,15 @@ def scan_command_line(self):
                 print('--dir: directory does not exist: %s' % dir_)
                 print('exiting')
                 sys.exit(1)
-        if options.force_pyx:
+        if args.force_pyx:
             print('--force-pyx: .pyx files will be parsed as regular python, cython syntax is not supported')
-        # If any files remain, set self.files.
-        if args:
-            args = [self.finalize(z) for z in args]
-            if args:
-                self.files = args
+        self.files = args.files
     #@+node:ekr.20160318141204.132: *3* msf.scan_options & helpers
     def scan_options(self):
-        '''Set all configuration-related ivars.'''
+        """Set all configuration-related ivars."""
         if self.verbose:
-            g.trace('config file: %s' % self.config_fn)
+            print('')
+            print(f"configuration file: {self.config_fn}")
         if not self.config_fn:
             return
         self.parser = parser = self.create_parser()
@@ -1103,50 +1061,65 @@ def scan_options(self):
         if self.files:
             files_source = 'command-line'
             files = self.files
+            if isinstance(files, str):
+                files = [files]
         elif parser.has_section('Global'):
             files_source = 'config file'
             files = parser.get('Global', 'files')
             files = [z.strip() for z in files.split('\n') if z.strip()]
         else:
             return
+        if self.verbose:
+            print(f"Files (from {files_source})...")
         files2 = []
+        not_found = []
         for z in files:
-            files2.extend(glob.glob(self.finalize(z)))
-        self.files = [z for z in files2 if z and os.path.exists(z)]
-        if self.verbose:
-            print('Files (from %s)...\n' % files_source)
-            for z in self.files:
-                print(z)
-            print('')
+            # 2021/08/04: Warn if z does not exist.
+            files3 = glob.glob(self.finalize(z))
+            if files3:
+                if self.verbose:
+                    for z in files3:
+                        print(f"  {z}")
+                files2.extend(files3)
+            else:
+                not_found.append(z)
+        if not_found:
+            print('Not found...')
+            for z in not_found:
+                print(f"  {z}")
+        self.files = files2
         if 'output_directory' in parser.options('Global'):
             s = parser.get('Global', 'output_directory').strip()
             output_dir = self.finalize(s)
             if os.path.exists(output_dir):
                 self.output_directory = output_dir
                 if self.verbose:
-                    print('output directory: %s\n' % output_dir)
+                    print(f"output directory: {output_dir}")
             else:
-                print('output directory not found: %s\n' % output_dir)
+                print(f"output directory not found: {output_dir}")
                 self.output_directory = None  # inhibit run().
         if 'prefix_lines' in parser.options('Global'):
             prefix = parser.get('Global', 'prefix_lines')
             prefix_lines = prefix.split('\n')
                 # The parser does not preserve leading whitespace.
             self.prefix_lines = [z for z in prefix_lines if z.strip()]
-            if self.verbose:
-                print('Prefix lines...\n')
-                for z in self.prefix_lines:
-                    print('  %s' % z)
-                print('')
+            # Annoying
+            # if self.verbose:
+                # print('Prefix lines...\n')
+                # for z in self.prefix_lines:
+                    # print('  %s' % z)
+                # print('')
+        if self.verbose:
+            print('')
         self.def_patterns = self.scan_patterns('Def Name Patterns')
         self.general_patterns = self.scan_patterns('General Patterns')
         self.make_patterns_dict()
     #@+node:ekr.20160318141204.133: *4* msf.make_op_name_dict
     def make_op_name_dict(self):
-        '''
+        """
         Make a dict whose keys are operators ('+', '+=', etc),
         and whose values are lists of values of ast.Node.__class__.__name__.
-        '''
+        """
         d = {
             '.': ['Attr',],
             '(*)': ['Call', 'Tuple',],
@@ -1168,14 +1141,14 @@ def make_op_name_dict(self):
         return d
     #@+node:ekr.20160318141204.134: *4* msf.create_parser
     def create_parser(self):
-        '''Create a RawConfigParser and return it.'''
+        """Create a RawConfigParser and return it."""
         parser = configparser.RawConfigParser(dict_type=OrderedDict)
             # Requires Python 2.7
         parser.optionxform = str
         return parser
     #@+node:ekr.20160318141204.135: *4* msf.find_pattern_ops
     def find_pattern_ops(self, pattern):
-        '''Return a list of operators in pattern.find_s.'''
+        """Return a list of operators in pattern.find_s."""
         trace = False or self.trace_patterns
         if pattern.is_regex():
             # Add the pattern to the regex patterns list.
@@ -1215,21 +1188,17 @@ def find_pattern_ops(self, pattern):
         return ops
     #@+node:ekr.20160318141204.136: *4* msf.get_config_string
     def get_config_string(self):
-
+        """Read the configuration file."""
         fn = self.finalize(self.config_fn)
         if os.path.exists(fn):
-            if self.verbose:
-                print('\nconfiguration file: %s\n' % fn)
-            f = open(fn, 'r')
-            s = f.read()
-            f.close()
-            return s
-        print('\nconfiguration file not found: %s' % fn)
+            with open(fn, 'r') as f:
+                return f.read()
+        print(f"\nconfiguration file not found: {fn}")
         return ''
 
     #@+node:ekr.20160318141204.137: *4* msf.init_parser
     def init_parser(self, s):
-        '''Add double back-slashes to all patterns starting with '['.'''
+        """Add double back-slashes to all patterns starting with '['."""
         if not s:
             return
         aList = []
@@ -1258,7 +1227,7 @@ def munge(s):
         return False
     #@+node:ekr.20160318141204.139: *4* msf.make_patterns_dict
     def make_patterns_dict(self):
-        '''Assign all patterns to the appropriate ast.Node.'''
+        """Assign all patterns to the appropriate ast.Node."""
         for pattern in self.general_patterns:
             ops = self.find_pattern_ops(pattern)
             if ops:
@@ -1295,7 +1264,7 @@ def make_patterns_dict(self):
         # Note: retain self.general_patterns for use in argument lists.
     #@+node:ekr.20160318141204.140: *4* msf.scan_patterns
     def scan_patterns(self, section_name):
-        '''Parse the config section into a list of patterns, preserving order.'''
+        """Parse the config section into a list of patterns, preserving order."""
         trace = False or self.trace_patterns
         parser = self.parser
         aList = []
@@ -1319,10 +1288,8 @@ def scan_patterns(self, section_name):
         return aList
     #@-others
 #@+node:ekr.20160318141204.92: ** class LeoGlobals
-
-
 class LeoGlobals:
-    '''A class supporting g.pdb and g.trace for compatibility with Leo.'''
+    """A class supporting g.pdb and g.trace for compatibility with Leo."""
     #@+others
     #@+node:ekr.20160318141204.93: *3* class NullObject (Python Cookbook)
 
@@ -1361,16 +1328,16 @@ def _callerName(self, n=1, files=False):
             return ''  # "<no caller name>"
     #@+node:ekr.20180902035806.1: *3* g.caller
     def caller(self, i=1):
-        '''Return the caller name i levels up the stack.'''
+        """Return the caller name i levels up the stack."""
         return self.callers(i + 1).split(',')[0]
     #@+node:ekr.20160318141204.95: *3* g.callers
     def callers(self, n=4, count=0, excludeCaller=True, files=False):
-        '''Return a list containing the callers of the function that called g.callerList.
+        """Return a list containing the callers of the function that called g.callerList.
 
         If the excludeCaller keyword is True (the default), g.callers is not on the list.
 
         If the files keyword argument is True, filenames are included in the list.
-        '''
+        """
         # sys._getframe throws ValueError in both cpython and jython if there are less than i entries.
         # The jython stack often has less than 8 entries,
         # so we must be careful to call g._callerName with smaller values of i first.
@@ -1389,15 +1356,15 @@ def callers(self, n=4, count=0, excludeCaller=True, files=False):
         return sep.join(result)
     #@+node:ekr.20160318141204.96: *3* g.cls
     def cls(self):
-        '''Clear the screen.'''
+        """Clear the screen."""
         if sys.platform.lower().startswith('win'):
             os.system('cls')
     #@+node:ekr.20180902073131.1: *3* g.execute_shell_commands
     def execute_shell_commands(self, commands, trace=False):
-        '''
+        """
         Execute each shell command in a separate process.
         Wait for each command to complete, except those starting with '&'
-        '''
+        """
         if g.isString(commands): commands = [commands]
         for command in commands:
             wait = not command.startswith('&')
@@ -1406,21 +1373,21 @@ def execute_shell_commands(self, commands, trace=False):
             if wait: proc.communicate()
     #@+node:ekr.20160318141204.97: *3* g.isString & isUnicode (make_stub_files.py)
     def isString(self, s):
-        '''Return True if s is any string, but not bytes.'''
+        """Return True if s is any string, but not bytes."""
         # pylint: disable=no-member
         if isPython3:
             return isinstance(s, str)
         return isinstance(s, types.StringTypes)
 
     def isUnicode(self, s):
-        '''Return True if s is a unicode string.'''
+        """Return True if s is a unicode string."""
         # pylint: disable=no-member
         if isPython3:
             return isinstance(s, str)
         return isinstance(s, types.UnicodeType)
     #@+node:ekr.20180902034437.1: *3* g.objToSTring & helpers
     def objToString(self, obj, indent='', printCaller=False, tag=None):
-        '''Pretty print any Python object to a string.'''
+        """Pretty print any Python object to a string."""
         # pylint: disable=undefined-loop-variable
             # Looks like a a pylint bug.
         #
@@ -1454,7 +1421,7 @@ def objToString(self, obj, indent='', printCaller=False, tag=None):
     toString = objToString
     #@+node:ekr.20180902041247.1: *4* g.dictToString
     def dictToString(self, d, indent='', tag=None):
-        '''Pretty print a Python dict to a string.'''
+        """Pretty print a Python dict to a string."""
         # pylint: disable=unnecessary-lambda
         if not d:
             return '{}'
@@ -1473,7 +1440,7 @@ def dictToString(self, d, indent='', tag=None):
         return '%s...\n%s\n' % (tag, s) if tag else s
     #@+node:ekr.20180902041311.1: *4* g.listToString
     def listToString(self, obj, indent='', tag=None):
-        '''Pretty print a Python list to a string.'''
+        """Pretty print a Python list to a string."""
         if not obj:
             return '[]'
         result = ['[']
@@ -1491,7 +1458,7 @@ def listToString(self, obj, indent='', tag=None):
         return '%s...\n%s\n' % (tag, s) if tag else s
     #@+node:ekr.20180902041320.1: *4* g.tupleToString
     def tupleToString(self, obj, indent='', tag=None):
-        '''Pretty print a Python tuple to a string.'''
+        """Pretty print a Python tuple to a string."""
         if not obj:
             return '(),'
         result = ['(']
@@ -1517,7 +1484,7 @@ def pdb(self):
             pdb.set_trace()
     #@+node:ekr.20180902034446.1: *3* g.printObj
     def printObj(self, obj, indent='', printCaller=False, tag=None):
-        '''Pretty print any Python object using g.pr.'''
+        """Pretty print any Python object using g.pr."""
         print(self.objToString(obj, indent=indent, printCaller=printCaller, tag=tag))
 
     #@+node:ekr.20160318141204.99: *3* g.shortFileName
@@ -1528,7 +1495,7 @@ def shortFileName(self, fileName, n=None):
         return '/'.join(fileName.replace('\\', '/').split('/')[-n :])
     #@+node:ekr.20160318141204.100: *3* g.splitLines
     def splitLines(self, s):
-        '''Split s into lines, preserving trailing newlines.'''
+        """Split s into lines, preserving trailing newlines."""
         return s.splitlines(True) if s else []
     #@+node:ekr.20160318141204.101: *3* g.trace
     def trace(self, *args, **keys):
@@ -1545,21 +1512,19 @@ def trace(self, *args, **keys):
         
     #@-others
 #@+node:ekr.20160318141204.102: ** class Pattern
-
-
 class Pattern:
-    '''
+    """
     A class representing regex or balanced patterns.
     
     Sample matching code, for either kind of pattern:
         
         for m in reversed(pattern.all_matches(s)):
             s = pattern.replace(m, s)
-    '''
+    """
     #@+others
     #@+node:ekr.20160318141204.103: *3* pattern.ctor
     def __init__(self, find_s, repl_s=''):
-        '''Ctor for the Pattern class.'''
+        """Ctor for the Pattern class."""
         self.find_s = find_s
         self.repl_s = repl_s
         if self.is_regex():
@@ -1587,17 +1552,17 @@ def __ne__(self, obj):
         return not self.__eq__(obj)
 
     def __hash__(self):
-        '''Pattern.__hash__'''
+        """Pattern.__hash__"""
         return len(self.find_s) + len(self.repl_s)
     #@+node:ekr.20160318141204.105: *3* pattern.str & repr
     def __repr__(self):
-        '''Pattern.__repr__'''
+        """Pattern.__repr__"""
         return '%s: %s' % (self.find_s, self.repl_s)
 
     __str__ = __repr__
     #@+node:ekr.20160318141204.106: *3* pattern.is_balanced
     def is_balanced(self):
-        '''Return True if self.find_s is a balanced pattern.'''
+        """Return True if self.find_s is a balanced pattern."""
         s = self.find_s
         if s.endswith('*'):
             return True
@@ -1607,18 +1572,18 @@ def is_balanced(self):
         return False
     #@+node:ekr.20160318141204.107: *3* pattern.is_regex
     def is_regex(self):
-        '''
+        """
         Return True if self.find_s is a regular pattern.
         For now a kludgy convention suffices.
-        '''
+        """
         return self.find_s.endswith('$')
             # A dollar sign is not valid in any Python expression.
     #@+node:ekr.20160318141204.108: *3* pattern.all_matches & helpers
     def all_matches(self, s):
-        '''
+        """
         Return a list of match objects for all matches in s.
         These are regex match objects or (start, end) for balanced searches.
-        '''
+        """
         if self.is_balanced():
             aList, i = [], 0
             while i < len(s):
@@ -1634,7 +1599,7 @@ def all_matches(self, s):
         return list(self.regex.finditer(s))
     #@+node:ekr.20160318141204.109: *4* pattern.full_balanced_match
     def full_balanced_match(self, s, i):
-        '''Return the index of the end of the match found at s[i:] or None.'''
+        """Return the index of the end of the match found at s[i:] or None."""
         pattern = self.find_s
         j = 0  # index into pattern
         while i < len(s) and j < len(pattern) and pattern[j] in ('*', s[i]):
@@ -1656,10 +1621,10 @@ def full_balanced_match(self, s, i):
         return i if found else None
     #@+node:ekr.20160318141204.110: *4* pattern.match_balanced
     def match_balanced(self, delim, s, i):
-        '''
+        """
         delim == s[i] and delim is in '([{'
         Return the index of the end of the balanced parenthesized string, or len(s)+1.
-        '''
+        """
         global g_input_file_name
         assert s[i] == delim, s[i]
         assert delim in '([{'
@@ -1683,10 +1648,10 @@ def match_balanced(self, delim, s, i):
         return len(s) + 1
     #@+node:ekr.20160318141204.111: *3* pattern.match (trace-matches)
     def match(self, s, trace=False):
-        '''
+        """
         Perform the match on the entire string if possible.
         Return (found, new s)
-        '''
+        """
         # caller = g.callers(2).split(',')[0].strip()
         # s1 = truncate(s,40)
         if self.is_balanced():
@@ -1703,7 +1668,7 @@ def match(self, s, trace=False):
         return False, s
     #@+node:ekr.20160318141204.112: *3* pattern.match_entire_string
     def match_entire_string(self, s):
-        '''Return True if s matches self.find_s'''
+        """Return True if s matches self.find_s"""
         if self.is_balanced():
             j = self.full_balanced_match(s, 0)
             return j == len(s)
@@ -1711,17 +1676,17 @@ def match_entire_string(self, s):
         return m and m.group(0) == s
     #@+node:ekr.20160318141204.113: *3* pattern.replace & helpers
     def replace(self, m, s):
-        '''Perform any kind of replacement.'''
+        """Perform any kind of replacement."""
         if self.is_balanced():
             start, end = m
             return self.replace_balanced(s, start, end)
         return self.replace_regex(m, s)
     #@+node:ekr.20160318141204.114: *4* pattern.replace_balanced
     def replace_balanced(self, s1, start, end):
-        '''
+        """
         Use m (returned by all_matches) to replace s by the string implied by repr_s.
         Within repr_s, * star matches corresponding * in find_s
-        '''
+        """
         s = s1[start:end]
         f, r = self.find_s, self.repl_s
         i1 = f.find('(*)')
@@ -1742,7 +1707,7 @@ def replace_balanced(self, s1, start, end):
         return s1[:start] + repl + s1[end:]
     #@+node:ekr.20160318141204.115: *4* pattern.replace_regex
     def replace_regex(self, m, s):
-        '''Do the replacement in s specified by m.'''
+        """Do the replacement in s specified by m."""
         s = self.repl_s
         for i in range(9):
             group = '\\%s' % i
@@ -1752,28 +1717,28 @@ def replace_regex(self, m, s):
     #@-others
 #@+node:ekr.20160318141204.116: ** class ReduceTypes
 class ReduceTypes:
-    '''
+    """
     A helper class for the top-level reduce_types function.
     
     This class reduces a list of type hints to a string containing the
     reduction of all types in the list.
-    '''
+    """
     #@+others
     #@+node:ekr.20160318141204.117: *3* rt.ctor
     def __init__(self, aList=None, name=None, trace=False):
-        '''Ctor for ReduceTypes class.'''
+        """Ctor for ReduceTypes class."""
         self.aList = aList
         self.name = name
         self.optional = False
         self.trace = trace
     #@+node:ekr.20160318141204.118: *3* rt.is_known_type
     def is_known_type(self, s):
-        '''
+        """
         Return True if s is nothing but a single known type.
 
         It suits the other methods of this class *not* to test inside inner
         brackets. This prevents unwanted Any types.
-        '''
+        """
         s = s.strip()
         table = (
             '', 'None',  # Tricky.
@@ -1827,10 +1792,10 @@ def is_known_type(self, s):
         return False
     #@+node:ekr.20160318141204.119: *3* rt.reduce_collection
     def reduce_collection(self, aList, kind):
-        '''
+        """
         Reduce the inner parts of a collection for the given kind.
         Return a list with only collections of the given kind reduced.
-        '''
+        """
         assert isinstance(aList, list)
         assert None not in aList, aList
         pattern = Pattern('%s[*]' % kind)
@@ -1852,10 +1817,10 @@ def reduce_collection(self, aList, kind):
         return sorted(set(result))
     #@+node:ekr.20160318141204.120: *3* rt.reduce_numbers
     def reduce_numbers(self, aList):
-        '''
+        """
         Return aList with all number types in aList replaced by the most
         general numeric type in aList.
-        '''
+        """
         found = None
         numbers = ('number', 'complex', 'float', 'long', 'int')
         for kind in numbers:
@@ -1872,14 +1837,14 @@ def reduce_numbers(self, aList):
         return aList
     #@+node:ekr.20160318141204.121: *3* rt.reduce_types
     def reduce_types(self):
-        '''
+        """
         self.aList consists of arbitrarily many types because this method is
         called from format_return_expressions.
         
         Return a *string* containing the reduction of all types in this list.
         Returning a string means that all traversers always return strings,
         never lists.
-        '''
+        """
         r = [('None' if z in ('', None) else z) for z in self.aList]
         assert None not in r
         self.optional = 'None' in r
@@ -1903,11 +1868,11 @@ def reduce_types(self):
         return self.show('Union[%s]' % (', '.join(sorted(r))))
     #@+node:ekr.20160318141204.122: *3* rt.reduce_unknowns
     def reduce_unknowns(self, aList):
-        '''Replace all unknown types in aList with Any.'''
+        """Replace all unknown types in aList with Any."""
         return [z if self.is_known_type(z) else 'Any' for z in aList]
     #@+node:ekr.20160318141204.123: *3* rt.show
     def show(self, s, known=True):
-        '''Show the result of reduce_types.'''
+        """Show the result of reduce_types."""
         aList, name = self.aList, self.name
         trace = self.trace
         s = s.strip()
@@ -1930,7 +1895,7 @@ def show(self, s, known=True):
         return s
     #@+node:ekr.20160318141204.124: *3* rt.split_types
     def split_types(self, s):
-        '''Split types on *outer level* commas.'''
+        """Split types on *outer level* commas."""
         aList, i1, level = [], 0, 0
         for i, ch in enumerate(s):
             if ch == '[':
@@ -1945,14 +1910,14 @@ def split_types(self, s):
     #@-others
 #@+node:ekr.20160318141204.141: ** class Stub
 class Stub:
-    '''
+    """
     A class representing all the generated stub for a class or def.
     stub.full_name should represent the complete context of a def.
-    '''
+    """
     #@+others
     #@+node:ekr.20160318141204.142: *3* stub.ctor
     def __init__(self, kind, name, parent=None, stack=None):
-        '''Stub ctor. Equality depends only on full_name and kind.'''
+        """Stub ctor. Equality depends only on full_name and kind."""
         self.children = []
         self.full_name = '%s.%s' % ('.'.join(stack), name) if stack else name
         self.kind = kind
@@ -1967,10 +1932,10 @@ def __init__(self, kind, name, parent=None, stack=None):
             parent.children.append(self)
     #@+node:ekr.20160318141204.143: *3* stub.__eq__ and __ne__
     def __eq__(self, obj):
-        '''
+        """
         Stub.__eq__. Return whether two stubs refer to the same method.
         Do *not* test parent links. That would interfere with --update logic.
-        '''
+        """
         if isinstance(obj, Stub):
             return self.full_name == obj.full_name and self.kind == obj.kind
         return NotImplemented
@@ -1980,34 +1945,34 @@ def __ne__(self, obj):
         return not self.__eq__(obj)
     #@+node:ekr.20160318141204.144: *3* stub.__hash__
     def __hash__(self):
-        '''Stub.__hash__. Equality depends *only* on full_name and kind.'''
+        """Stub.__hash__. Equality depends *only* on full_name and kind."""
         return len(self.kind) + sum([ord(z) for z in self.full_name])
     #@+node:ekr.20160318141204.145: *3* stub.__repr__and __str__
     def __repr__(self):
-        '''Stub.__repr__.'''
+        """Stub.__repr__."""
         # return 'Stub: %s %s' % (id(self), self.full_name)
         return 'Stub: %s\n%s' % (self.full_name, g.objToString(self.out_list))
 
     __str__ = __repr__
     #@+node:ekr.20160318141204.146: *3* stub.parents and level
     def level(self):
-        '''Return the number of parents.'''
+        """Return the number of parents."""
         return len(self.parents())
 
     def parents(self):
-        '''Return a list of this stub's parents.'''
+        """Return a list of this stub's parents."""
         return self.full_name.split('.')[:-1]
     #@-others
 #@+node:ekr.20160318141204.147: ** class StubFormatter (AstFormatter)
 class StubFormatter(AstFormatter):
-    '''
+    """
     Formats an ast.Node and its descendants,
     making pattern substitutions in Name and operator nodes.
-    '''
+    """
     #@+others
     #@+node:ekr.20160318141204.148: *3* sf.ctor
     def __init__(self, controller, traverser):
-        '''Ctor for StubFormatter class.'''
+        """Ctor for StubFormatter class."""
         self.controller = x = controller
         self.traverser = traverser
             # 2016/02/07: to give the formatter access to the class_stack.
@@ -2028,7 +1993,7 @@ def __init__(self, controller, traverser):
     matched_d = {}
 
     def match_all(self, node, s, trace=False):
-        '''Match all the patterns for the given node.'''
+        """Match all the patterns for the given node."""
         trace = trace or self.trace_matches
         # verbose = True
         d = self.matched_d
@@ -2048,14 +2013,9 @@ def match_all(self, node, s, trace=False):
                         print('match_all:    %-12s %26s %40s ==> %s' % (caller, pattern, s1, s))
                 break
         return s
-    #@+node:ekr.20160318141204.150: *3* sf.visit
-    def visit(self, node):
-        '''StubFormatter.visit: supports --verbose tracing.'''
-        s = AstFormatter.visit(self, node)
-        return s
     #@+node:ekr.20160318141204.151: *3* sf.trace_visitor
     def trace_visitor(self, node, op, s):
-        '''Trace node's visitor.'''
+        """Trace node's visitor."""
         if self.trace_visitors:
             caller = g.callers(2).split(',')[1]
             s1 = AstFormatter().format(node).strip()
@@ -2068,7 +2028,7 @@ def trace_visitor(self, node, op, s):
     attrs_seen = []
 
     def do_Attribute(self, node):
-        '''StubFormatter.do_Attribute.'''
+        """StubFormatter.do_Attribute."""
         s = '%s.%s' % (
             self.visit(node.value),
             node.attr)  # Don't visit node.attr: it is always a string.
@@ -2089,7 +2049,7 @@ def do_Num(self, node):
         return 'number'  # return repr(node.n)
 
     def do_Str(self, node):
-        '''This represents a string constant.'''
+        """This represents a string constant."""
         return 'str'  # return repr(node.s)
     #@+node:ekr.20160318141204.155: *4* sf.Dict
     def do_Dict(self, node):
@@ -2110,7 +2070,7 @@ def do_Dict(self, node):
         return 'Dict[%s]' % ''.join(result) if result else 'Dict'
     #@+node:ekr.20160318141204.156: *4* sf.List
     def do_List(self, node):
-        '''StubFormatter.List.'''
+        """StubFormatter.List."""
         elts = [self.visit(z) for z in node.elts]
         elts = [z for z in elts if z]  # Defensive.
         return 'List[%s]' % ', '.join(elts) if elts else 'List'
@@ -2118,7 +2078,7 @@ def do_List(self, node):
     # seen_names = [] # t--ype: List[str]
 
     def do_Name(self, node):
-        '''StubFormatter ast.Name visitor.'''
+        """StubFormatter ast.Name visitor."""
         d = self.names_dict
         name = d.get(node.id, node.id)
         s = 'bool' if name in ('True', 'False') else name
@@ -2131,7 +2091,7 @@ def do_Name(self, node):
         return s
     #@+node:ekr.20160318141204.158: *4* sf.Tuple
     def do_Tuple(self, node):
-        '''StubFormatter.Tuple.'''
+        """StubFormatter.Tuple."""
         elts = [self.visit(z) for z in node.elts]
         return 'Tuple[%s]' % ', '.join(elts)
     #@+node:ekr.20160318141204.159: *3* sf.Operators
@@ -2140,7 +2100,7 @@ def do_Tuple(self, node):
     # BinOp(expr left, operator op, expr right)
 
     def do_BinOp(self, node):
-        '''StubFormatter.BinOp visitor.'''
+        """StubFormatter.BinOp visitor."""
         trace = self.trace_reduce
         numbers = ['number', 'complex', 'float', 'long', 'int',]
         op = self.op_name(node.op)
@@ -2170,8 +2130,8 @@ def do_BinOp(self, node):
     #@+node:ekr.20160318141204.161: *4* sf.BoolOp
     # BoolOp(boolop op, expr* values)
 
-    def do_BoolOp(self, node):  # Python 2.x only.
-        '''StubFormatter.BoolOp visitor for 'and' and 'or'.'''
+    def do_BoolOp(self, node):  # pragma: no cover (Python 2.x only)
+        """StubFormatter.BoolOp visitor for 'and' and 'or'."""
         trace = self.trace_reduce
         op = self.op_name(node.op)
         values = [self.visit(z).strip() for z in node.values]
@@ -2183,7 +2143,7 @@ def do_BoolOp(self, node):  # Python 2.x only.
     # Call(expr func, expr* args, keyword* keywords, expr? starargs, expr? kwargs)
 
     def do_Call(self, node):
-        '''StubFormatter.Call visitor.'''
+        """StubFormatter.Call visitor."""
         func = self.visit(node.func)
         args = [self.visit(z) for z in node.args]
         for z in node.keywords:
@@ -2217,10 +2177,10 @@ def do_keyword(self, node):
     # Compare(expr left, cmpop* ops, expr* comparators)
 
     def do_Compare(self, node):
-        '''
+        """
         StubFormatter ast.Compare visitor for these ops:
         '==', '!=', '<', '<=', '>', '>=', 'is', 'is not', 'in', 'not in',
-        '''
+        """
         s = 'bool'  # Correct regardless of arguments.
         ops = ','.join([self.op_name(z) for z in node.ops])
         self.trace_visitor(node, ops, s)
@@ -2229,7 +2189,7 @@ def do_Compare(self, node):
     # If(expr test, stmt* body, stmt* orelse)
 
     def do_IfExp(self, node):
-        '''StubFormatterIfExp (ternary operator).'''
+        """StubFormatterIfExp (ternary operator)."""
         trace = self.trace_reduce
         aList = [
             self.match_all(node, self.visit(node.body)),
@@ -2243,7 +2203,7 @@ def do_IfExp(self, node):
     # Subscript(expr value, slice slice, expr_context ctx)
 
     def do_Subscript(self, node):
-        '''StubFormatter.Subscript.'''
+        """StubFormatter.Subscript."""
         s = '%s[%s]' % (
             self.visit(node.value),
             self.visit(node.slice))
@@ -2254,7 +2214,7 @@ def do_Subscript(self, node):
     # UnaryOp(unaryop op, expr operand)
 
     def do_UnaryOp(self, node):
-        '''StubFormatter.UnaryOp for unary +, -, ~ and 'not' operators.'''
+        """StubFormatter.UnaryOp for unary +, -, ~ and 'not' operators."""
         op = self.op_name(node.op)
         if op.strip() == 'not':
             return 'bool'
@@ -2264,25 +2224,25 @@ def do_UnaryOp(self, node):
         return s
     #@+node:ekr.20160318141204.168: *3* sf.Return
     def do_Return(self, node):
-        '''
+        """
         StubFormatter ast.Return vsitor.
         Return only the return expression itself.
-        '''
+        """
         s = AstFormatter.do_Return(self, node)
         assert s.startswith('return'), repr(s)
         return s[len('return') :].strip()
     #@-others
 #@+node:ekr.20160318141204.169: ** class StubTraverser (ast.NodeVisitor)
 class StubTraverser(ast.NodeVisitor):
-    '''
+    """
     An ast.Node traverser class that outputs a stub for each class or def.
     Names of visitors must start with visit_. The order of traversal does
     not matter, because so few visitors do anything.
-    '''
+    """
     #@+others
     #@+node:ekr.20160318141204.170: *3* st.ctor
     def __init__(self, controller):
-        '''Ctor for StubTraverser class.'''
+        """Ctor for StubTraverser class."""
         self.controller = x = controller  # A Controller instance.
         # Internal state ivars...
         self.class_name_stack = []
@@ -2320,7 +2280,7 @@ def __init__(self, controller):
         self.patterns_dict = x.patterns_dict
     #@+node:ekr.20160318141204.171: *3* st.add_stub
     def add_stub(self, d, stub):
-        '''Add the stub to d, checking that it does not exist.'''
+        """Add the stub to d, checking that it does not exist."""
         global g_input_file_name
         key = stub.full_name
         assert key
@@ -2331,12 +2291,12 @@ def add_stub(self, d, stub):
             d[key] = stub
     #@+node:ekr.20160318141204.172: *3* st.indent & out
     def indent(self, s):
-        '''Return s, properly indented.'''
+        """Return s, properly indented."""
         # This version of indent *is* used.
         return '%s%s' % (' ' * 4 * self.level, s)
 
     def out(self, s):
-        '''Output the string to the console or the file.'''
+        """Output the string to the console or the file."""
         s = self.indent(s)
         if self.parent_stub:
             self.parent_stub.out_list.append(s)
@@ -2346,7 +2306,7 @@ def out(self, s):
             print(s)
     #@+node:ekr.20160318141204.173: *3* st.run (main line) & helpers
     def run(self, node):
-        '''StubTraverser.run: write the stubs in node's tree to self.output_fn.'''
+        """StubTraverser.run: write the stubs in node's tree to self.output_fn."""
         fn = self.output_fn
         dir_ = os.path.dirname(fn)
         if os.path.exists(fn) and not self.overwrite:
@@ -2373,7 +2333,7 @@ def run(self, node):
             print('wrote: %s' % fn)
     #@+node:ekr.20160318141204.174: *4* st.output_stubs
     def output_stubs(self, stub):
-        '''Output this stub and all its descendants.'''
+        """Output this stub and all its descendants."""
         for s in stub.out_list or []:
             # Indentation must be present when an item is added to stub.out_list.
             if self.output_file:
@@ -2385,20 +2345,20 @@ def output_stubs(self, stub):
             self.output_stubs(child)
     #@+node:ekr.20160318141204.175: *4* st.output_time_stamp
     def output_time_stamp(self):
-        '''Put a time-stamp in the output file.'''
+        """Put a time-stamp in the output file."""
         if self.output_file:
             self.output_file.write('# make_stub_files: %s\n' %
                 time.strftime("%a %d %b %Y at %H:%M:%S"))
     #@+node:ekr.20160318141204.176: *4* st.update & helpers
     def update(self, fn, new_root):
-        '''
+        """
         Merge the new_root tree with the old_root tree in fn (a .pyi file).
 
         new_root is the root of the stub tree from the .py file.
         old_root (read below) is the root of stub tree from the .pyi file.
         
         Return old_root, or new_root if there are any errors.
-        '''
+        """
         s = self.get_stub_file(fn)
         if not s or not s.strip():
             return new_root
@@ -2420,7 +2380,7 @@ def update(self, fn, new_root):
         return new_root
     #@+node:ekr.20160318141204.177: *5* st.get_stub_file
     def get_stub_file(self, fn):
-        '''Read the stub file into s.'''
+        """Read the stub file into s."""
         if os.path.exists(fn):
             try:
                 s = open(fn, 'r').read()
@@ -2432,11 +2392,11 @@ def get_stub_file(self, fn):
         return None
     #@+node:ekr.20160318141204.178: *5* st.parse_stub_file
     def parse_stub_file(self, s, root_name):
-        '''
+        """
         Parse s, the contents of a stub file, into a tree of Stubs.
         
         Parse by hand, so that --update can be run with Python 2.
-        '''
+        """
         assert '\t' not in s
         d = {}
         root = Stub(kind='root', name=root_name)
@@ -2484,12 +2444,12 @@ def parse_stub_file(self, s, root_name):
         return d, root
     #@+node:ekr.20160318141204.179: *5* st.merge_stubs & helpers
     def merge_stubs(self, new_stubs, old_root, new_root, trace=False):
-        '''
+        """
         Merge the new_stubs *list* into the old_root *tree*.
         - new_stubs is a list of Stubs from the .py file.
         - old_root is the root of the stubs from the .pyi file.
         - new_root is the root of the stubs from the .py file.
-        '''
+        """
         # Part 1: Delete old stubs do *not* exist in the *new* tree.
         aList = self.check_delete(new_stubs, old_root, new_root, trace)
             # Checks that all ancestors of deleted nodes will be deleted.
@@ -2511,7 +2471,7 @@ def merge_stubs(self, new_stubs, old_root, new_root, trace=False):
             assert self.find_stub(stub, old_root), stub
     #@+node:ekr.20160318141204.180: *6* st.check_delete
     def check_delete(self, new_stubs, old_root, new_root, trace):
-        '''Return a list of nodes that can be deleted.'''
+        """Return a list of nodes that can be deleted."""
         old_stubs = self.flatten_stubs(old_root)
         old_stubs.remove(old_root)
         aList = [z for z in old_stubs if z not in new_stubs]
@@ -2541,24 +2501,24 @@ def check_delete(self, new_stubs, old_root, new_root, trace):
         return delete_list
     #@+node:ekr.20160318141204.181: *6* st.flatten_stubs
     def flatten_stubs(self, root):
-        '''Return a flattened list of all stubs in root's tree.'''
+        """Return a flattened list of all stubs in root's tree."""
         aList = [root]
         for child in root.children:
             self.flatten_stubs_helper(child, aList)
         return aList
 
     def flatten_stubs_helper(self, root, aList):
-        '''Append all stubs in root's tree to aList.'''
+        """Append all stubs in root's tree to aList."""
         aList.append(root)
         for child in root.children:
             self.flatten_stubs_helper(child, aList)
     #@+node:ekr.20160318141204.182: *6* st.find_parent_stub
     def find_parent_stub(self, stub, root):
-        '''Return stub's parent **in root's tree**.'''
+        """Return stub's parent **in root's tree**."""
         return self.find_stub(stub.parent, root) if stub.parent else None
     #@+node:ekr.20160318141204.183: *6* st.find_stub
     def find_stub(self, stub, root):
-        '''Return the stub **in root's tree** that matches stub.'''
+        """Return the stub **in root's tree** that matches stub."""
         if stub == root:  # Must use Stub.__eq__!
             return root  # not stub!
         for child in root.children:
@@ -2567,10 +2527,10 @@ def find_stub(self, stub, root):
         return None
     #@+node:ekr.20160318141204.184: *6* st.sort_stubs_by_hierarchy
     def sort_stubs_by_hierarchy(self, stubs1):
-        '''
+        """
         Sort the list of Stubs so that parents appear before all their
         descendants.
-        '''
+        """
         stubs, result = stubs1[:], []
         for i in range(50):
             if stubs:
@@ -2585,7 +2545,7 @@ def sort_stubs_by_hierarchy(self, stubs1):
         return []  # Abort the merge.
     #@+node:ekr.20160318141204.185: *5* st.trace_stubs
     def trace_stubs(self, stub, aList=None, header=None, level=-1):
-        '''Return a trace of the given stub and all its descendants.'''
+        """Return a trace of the given stub and all its descendants."""
         indent = ' ' * 4 * max(0, level)
         if level == -1:
             aList = ['===== %s...\n' % (header) if header else '']
@@ -2679,10 +2639,10 @@ def visit_FunctionDef(self, node):
     # arguments = (expr* args, identifier? vararg, identifier? kwarg, expr* defaults)
 
     def format_arguments(self, node):
-        '''
+        """
         Format the arguments node.
         Similar to AstFormat.do_arguments, but it is not a visitor!
-        '''
+        """
         assert isinstance(node, ast.arguments), node
         args = [self.raw_format(z) for z in node.args]
         defaults = [self.raw_format(z) for z in node.defaults]
@@ -2711,7 +2671,7 @@ def format_arguments(self, node):
     type_pattern = re.compile(r'.*:.*')
 
     def munge_arg(self, s):
-        '''Add an annotation for s if possible.'''
+        """Add an annotation for s if possible."""
         if s == 'self':
             return s
         for pattern in self.general_patterns:
@@ -2726,16 +2686,17 @@ def munge_arg(self, s):
         return s + ': Any'
     #@+node:ekr.20160318141204.190: *4* st.format_returns & helpers
     def format_returns(self, node):
-        '''
+        """
         Calculate the return type:
         - Return None if there are no return statements.
         - Patterns in [Def Name Patterns] override all other patterns.
         - Otherwise, return a list of return values.
-        '''
+        """
         name = self.get_def_name(node)
         raw = [self.raw_format(z) for z in self.returns]
         # Allow StubFormatter.do_Return to do the hack.
         r = [self.format(z) for z in self.returns]
+        ### g.trace('1', raw, r)
         # Step 1: Return None if there are no return statements.
         if not [z for z in self.returns if z.value is not None]:
             empty = not any(isinstance(z, ast.FunctionDef) for z in node.body)
@@ -2744,25 +2705,27 @@ def format_returns(self, node):
         # Step 2: [Def Name Patterns] override all other patterns.
         for pattern in self.def_patterns:
             found, s = pattern.match(name)
+            ### g.trace('2', found, name, pattern, s)
             if found:
                 return s + ': ...'
         # Step 3: remove recursive calls.
         raw, r = self.remove_recursive_calls(name, raw, r)
+        ### g.trace('3', raw, r)
         # Step 4: Calculate return types.
         return self.format_return_expressions(node, name, raw, r)
     #@+node:ekr.20160318141204.191: *5* st.format_return_expressions
     def format_return_expressions(self, node, name, raw_returns, reduced_returns):
-        '''
+        """
         aList is a list of maximally reduced return expressions.
         For each expression e in Alist:
         - If e is a single known type, add e to the result.
         - Otherwise, add Any # e to the result.
         Return the properly indented result.
-        '''
+        """
         assert len(raw_returns) == len(reduced_returns)
         lws = '\n' + ' ' * 4
         n = len(raw_returns)
-        known = all([is_known_type(e) for e in reduced_returns])
+        known = all(is_known_type(e) for e in reduced_returns)
         empty = not any(isinstance(z, ast.FunctionDef) for z in node.body)
         tail = ': ...' if empty else ':'
         if not known or self.verbose:
@@ -2783,7 +2746,7 @@ def format_return_expressions(self, node, name, raw_returns, reduced_returns):
         return s + tail
     #@+node:ekr.20160318141204.192: *5* st.get_def_name
     def get_def_name(self, node):
-        '''Return the representaion of a function or method name.'''
+        """Return the representaion of a function or method name."""
         if self.class_name_stack:
             name = '%s.%s' % (self.class_name_stack[-1], node.name)
             # All ctors should return None
@@ -2794,7 +2757,7 @@ def get_def_name(self, node):
         return name
     #@+node:ekr.20160318141204.193: *5* st.remove_recursive_calls
     def remove_recursive_calls(self, name, raw, reduced):
-        '''Remove any recursive calls to name from both lists.'''
+        """Remove any recursive calls to name from both lists."""
         # At present, this works *only* if the return is nothing but the recursive call.
         assert len(raw) == len(reduced)
         pattern = Pattern('%s(*)' % name)
@@ -2813,7 +2776,359 @@ def visit_Return(self, node):
         self.returns.append(node)
             # New: return the entire node, not node.value.
     #@-others
-#@+node:ekr.20210803055042.1: ** class Test(unittest.testCase)
+#@+node:ekr.20210803055042.1: ** class TestMakeStubFiles(unittest.TestCase)
+class TestMakeStubFiles(unittest.TestCase):  # pragma: no cover
+    """Unit tests for make_stub_files.py"""
+    #@+others
+    #@+node:ekr.20180901040718.1: *3* test_bug2_empty
+    def test_bug2_empty(self):
+        # https://github.com/edreamleo/make-stub-files/issues/2
+        tag = 'test_bug2_empty'
+        s = 'class InvalidTag(Exception):\n    pass'
+        controller = Controller()
+        node = ast.parse(s, filename=tag, mode='exec')
+        st = StubTraverser(controller=controller)
+        # From StubTraverser.run.
+        st.parent_stub = Stub(kind='root', name='<new-stubs>')
+        st.visit(node)
+        # Allocate a StringIo file for output_stubs.
+        st.output_file = io.StringIO()
+        st.output_stubs(st.parent_stub)
+        # Test.
+        lines = g.splitLines(st.output_file.getvalue())
+        expected = ['class InvalidTag(Exception): ...\n']
+        self.assertEqual(lines, expected)
+    #@+node:ekr.20180901044640.1: *3* test_bug2_non_empty
+    def test_bug2_non_empty(self):
+        # https://github.com/edreamleo/make-stub-files/issues/2
+        tag = 'test_bug2_non_empty'
+        s = (
+            'class NonEmptyClass:\n'
+            '\n'
+            '    def spam():\n'
+            '        pass\n'
+        )
+        expected = [
+            'class NonEmptyClass:\n',
+            '    def spam() -> None: ...\n',
+        ]
+        controller = Controller()
+        node = ast.parse(s, filename=tag, mode='exec')
+        st = StubTraverser(controller=controller)
+        # From StubTraverser.run.
+        st.parent_stub = Stub(kind='root', name='<new-stubs>')
+        st.visit(node)
+        # Allocate a StringIo file for output_stubs.
+        st.output_file = io.StringIO()
+        st.output_stubs(st.parent_stub)
+        # Test.
+        lines = g.splitLines(st.output_file.getvalue())
+        self.assertEqual(lines, expected)
+    #@+node:ekr.20180901051603.1: *3* test_bug3
+    def test_bug3(self):
+        # https://github.com/edreamleo/make-stub-files/issues/3
+        tag = 'test_bug3'
+        s = (
+            'class UnsupportedAlgorithm(Exception):\n'
+            '    def __init__(self, message: Any, reason: Optional[str]=None) -> None:\n'
+            '        pass\n'
+        )
+        expected = [
+            'class UnsupportedAlgorithm(Exception):\n',
+            '    def __init__(self, message: Any, reason: Optional[str]=None) -> None: ...\n',
+        ]
+        controller = Controller()
+        node = ast.parse(s, filename=tag, mode='exec')
+        st = StubTraverser(controller=controller)
+        # From StubTraverser.run.
+        st.parent_stub = Stub(kind='root', name='<new-stubs>')
+        st.visit(node)
+        # Allocate a StringIo file for output_stubs.
+        st.output_file = io.StringIO()
+        st.output_stubs(st.parent_stub)
+        # Test.
+        lines = g.splitLines(st.output_file.getvalue())
+        self.assertEqual(lines, expected)
+    #@+node:ekr.20210804103146.1: *3* test_pattern_class
+    def test_pattern_class(self):
+        g = LeoGlobals() # Use the g available to the script.
+        table = (
+            # s,  Pattern.find_s, Pattern.repl_s, expected
+            ('aabbcc', '(a+)(b+)(c+)$', r'\3\2\1', 'ccbbaa'),
+            ('[str]', r'\[str\]$', 'xxx', 'xxx'), # Guido bug.
+            ('s3', r's[1-3]?\b$', 'str', 'str'), # lengthening bug.
+            ('s', 's', 'str', 'str'),
+            ('abc', 'abc', 'ABC', 'ABC'),
+            ('str(str)', 'str(*)', 'str', 'str'),
+            ('[whatever]', '[*]', 'List[*]', 'List[whatever]'), # * on the RHS.
+            ('(int,str)', '(*)', 'Tuple[*]', 'Tuple[int,str]'), # Guido bug 2.
+            ('abcxyz', 'abc*', 'xxx', 'xxx'), # New test for trailing *.
+            ('list(self.regex.finditer(str))','list(*)','List[*]',
+             'List[self.regex.finditer(str)]'),
+        )
+        for s, find, repl, expected in table:
+            pattern = Pattern(find, repl)
+            result = pattern.match_entire_string(s)
+            self.assertTrue(result, msg=repr(s))
+            aList = pattern.all_matches(s)
+            self.assertTrue(len(aList) == 1, msg=repr(aList))
+            found, s2 = pattern.match(s)
+            self.assertTrue(found, msg=f"after pattern.match({s!r})")
+            assert s2 == expected, (s, pattern, 'expected', expected, 'got', s2)
+        p1 = Pattern('abc','xyz')
+        p2 = Pattern('abc','xyz')
+        p3 = Pattern('abc','pdq')
+        self.assertEqual(p1, p2)
+        self.assertNotEqual(p1, p3)
+        self.assertNotEqual(p2, p3)
+        aSet = set()
+        aSet.add(p1)
+        self.assertTrue(p1 in aSet)
+        self.assertTrue(p2 in aSet)
+        self.assertFalse(p3 in aSet)
+        self.assertEqual(list(aSet), [p1])
+        self.assertEqual(list(aSet), [p2])
+        aSet.add(p3)
+        self.assertTrue(p1.match_entire_string('abc'))
+        self.assertFalse(p1.match_entire_string('abcx'))
+    #@+node:ekr.20210804105256.1: *3* test_reduce_numbers
+    def test_reduce_numbers(self):
+        a, c, f, i, l, n = ('Any', 'complex', 'float', 'int', 'long', 'number')
+        table = (
+            ([i,i],     [i]),
+            ([i],       [i]),
+            ([f, i],    [f]),
+            ([c, i],    [c]),
+            ([l, a],    [a, l]),
+        )
+        for aList, expected in table:
+            got = ReduceTypes().reduce_numbers(aList)
+            self.assertEqual(expected, got, msg=repr(aList))
+    #@+node:ekr.20210804111613.1: *3* test_reduce_types
+    def test_reduce_types(self):
+
+        a, c, f, i, l, n = ('Any', 'complex', 'float', 'int', 'long', 'number')
+        none = 'None'
+        x = 'xyzzy'
+        y = 'pdq'
+        table = (
+            ([i,i],         i),
+            ([i],           i),
+            ([f, i],        f),
+            ([c, i],        c),
+            ([l, a],        'Union[Any, long]'),
+            # Handle None
+            ([None],        none),
+            ([None, None],  none),
+            ([None, a, c],  'Optional[Union[Any, complex]]'),
+            # Handle unknown types, and special cases
+            ([i, x],        'Union[Any, int]'),
+            ([None, x],     'Optional[Any]'),
+            ([none, x],     'Optional[Any]'),
+            (['', x],       'Optional[Any]'),
+            ([none, x, c],  'Optional[Union[Any, complex]]'),
+            ([x, y],        'Any'),
+            # Collection merging.  More could be done...
+            (['Dict[int, str]', 'Dict[Any, str]'],          'Union[Dict[Any, str], Dict[int, str]]'),
+            (['List[int, str]', 'List[Any, str]'],          'Union[List[Any, str], List[int, str]]'),
+            (['Union[int, str]', 'Union[Any, str]'],        'Union[Union[Any, str], Union[int, str]]'),
+            (['Union[int, str]', 'int', 'Union[Any, str]'], 'Union[Union[Any, str], Union[int, str], int]'),
+            (['Tuple[xyz, pdq]'],                           'Tuple[Any, Any]'),
+        )
+        for aList, expected in table:
+            got = ReduceTypes(aList).reduce_types()
+            self.assertEqual(expected, got, msg=repr(aList))
+    #@+node:ekr.20210804111803.1: *3* test_split_types
+    def test_split_types(self):
+        table = (
+            ('list',                    ['list']),
+            ('List[a,b]',               ['List[a,b]']),
+            ('List[a,b], List[c,d]',    ['List[a,b]', 'List[c,d]']),
+        )
+        for s, expected in table:
+            got = ReduceTypes().split_types(s)
+            self.assertEqual(expected, got, msg=repr(s))
+    #@+node:ekr.20210804111915.1: *3* test_st_find
+    def test_st_find(self):
+
+        s = """\
+    def is_known_type(s: str) -> Union[Any,bool]: ...
+    def main() -> None: ...
+    def merge_types(a1: Any, a2: Any) -> str: ...
+
+    class AstFormatter:
+        def format(self, node: Node) -> Union[Any,str]: ...
+            def helper(self): -> None
+        def visit(self, node: Node) -> str: ...
+        def do_ClassDef(self, node: Node) -> str: ...
+        def do_FunctionDef(self, node: Node) -> str: ...
+    """
+        g = LeoGlobals() # Use the g available to the script.
+        st = StubTraverser(controller=g.NullObject())
+        d, root = st.parse_stub_file(s, root_name='<root>')  # Root *is* used below.
+        if 0:
+            print(st.trace_stubs(root, header='root'))
+        stub1 = Stub(kind='class', name='AstFormatter')
+        stub2 = Stub(kind='def', name='format', parent=stub1, stack=['AstFormatter'])
+        stub3 = Stub(kind='def', name='helper', parent = stub2, stack=['AstFormatter', 'format'])
+        # stub4 = Stub(kind='def', name='main')
+        for stub in (stub1, stub2, stub3,):  # (stub1, stub2, stub3):
+            found = st.find_stub(stub, root)
+            id_found = found and id(found) or None
+            if 0:
+                print('found  %s => %9s %35s ==> %s' % (id(stub), id_found, stub, found))
+            found = st.find_parent_stub(stub, root)
+            id_found = found and id(found) or None
+            if 0:
+                print('parent %s => %9s %35s ==> %s' % (id(stub), id_found, stub, found))
+    #@+node:ekr.20210804112211.1: *3* test_st_flatten_stubs
+    def test_st_flatten_stubs(self):
+        s = """\
+        def is_known_type(s: str) -> Union[Any,bool]: ...
+        def main() -> None: ...
+        def merge_types(a1: Any, a2: Any) -> str: ...
+        
+        class AstFormatter:
+            def format(self, node: Node) -> Union[Any,str]: ...
+                def helper(self): -> None
+            def visit(self, node: Node) -> str: ...
+            def do_ClassDef(self, node: Node) -> str: ...
+            def do_FunctionDef(self, node: Node) -> str: ...
+        """
+        g = LeoGlobals()  # Use the g available to the script.
+        st = StubTraverser(controller=g.NullObject())
+        d, root = st.parse_stub_file(s, root_name='<root>')
+        if 0:
+            print(st.trace_stubs(root, header='root'))
+        aList = st.flatten_stubs(root)
+        self.assertTrue(aList)
+        if 0:
+            for i, stub in enumerate(aList):
+                print('%2s %s' % (i, stub))
+        for stub in aList:
+            found = st.find_stub(stub, root)
+            self.assertTrue(found, msg=repr(stub))
+    #@+node:ekr.20210804112405.1: *3* test_st_merge_stubs
+    def test_st_merge_stubs(self):
+        # To do:
+        # - Test between-stub lines and leading lines.
+        # - Round-trip tests!
+        #@+<< old_stubs >>
+        #@+node:ekr.20210804112405.3: *4* << old_stubs >>
+        # To be INSERTED (They exist in new stubs, but not here.)
+        # def is_known_type(s: str) -> Union[Any,bool]: ...
+        # def reduce_numbers(aList: List[Any]) -> List[Any]: ...
+        # class AstFormatter:
+            # def format(self, node: Node) -> Union[Any,str]: ...
+            # def visit(self, node: Node) -> str: ...
+            # def do_ClassDef(self, node: Node) -> str: ...
+            # def do_FunctionDef(self, node: Node) -> str: ...
+        old_s = """\
+        def main() -> None: ...
+        def merge_types(a1: Any, a2: Any) -> str: ...
+        def pdb(self) -> None: ...
+        def reduce_types(aList: List[Any], name: str=None, trace: bool=False) -> Any: ...
+        class Pattern(object):
+            def __init__(self, find_s: str, repl_s: str='') -> None: ...
+            def __eq__(self, obj: Any) -> bool: ...
+            def __ne__(self, obj: Any) -> bool: ...
+            def __hash__(self) -> int: ...
+            def __repr__(self) -> str: ...
+            def is_balanced(self) -> bool: ...
+            def is_regex(self) -> Any: ...
+                #   0: return self.find_s.endswith('$')
+                # ? 0: return self.find_s.endswith(str)
+        """
+        #@-<< old_stubs >>
+        #@+<< new_stubs >>
+        #@+node:ekr.20210804112405.4: *4* << new_stubs >>
+        # To be DELETED (They exist in old_stubs, but not here)
+        # class Pattern(object):
+            # def __init__(self, find_s: str, repl_s: str='') -> None: ...
+            # def __eq__(self, obj: Any) -> bool: ...
+            # def __ne__(self, obj: Any) -> bool: ...
+            # def __hash__(self) -> int: ...
+            # def __repr__(self) -> str: ...
+            # def is_balanced(self) -> bool: ...
+            # def is_regex(self) -> Any: ...
+                # #   0: return self.find_s.endswith('$')
+                # # ? 0: return self.find_s.endswith(str)
+        new_s = """\
+        def is_known_type(s: str) -> Union[Any,bool]: ...
+        def main() -> None: ...
+        def merge_types(a1: Any, a2: Any) -> str: ...
+        def pdb(self) -> None: ...
+        def reduce_numbers(aList: List[Any]) -> List[Any]: ...
+        def reduce_types(aList: List[Any], name: str=None, trace: bool=False) -> Any: ...
+
+        class AstFormatter:
+            def format(self, node: Node) -> Union[Any,str]: ...
+            def visit(self, node: Node) -> str: ...
+            def do_ClassDef(self, node: Node) -> str: ...
+            def do_FunctionDef(self, node: Node) -> str: ...
+        """
+        #@-<< new_stubs >>
+        g = LeoGlobals() # Use the g available to the script.
+        st = StubTraverser(controller=g.NullObject())
+        # dump('old_s', old_s)
+        # dump('new_s', new_s)
+        old_d, old_root = st.parse_stub_file(old_s, root_name='<old-root>')
+        new_d, new_root = st.parse_stub_file(new_s, root_name='<new-root>')
+        if 0:
+            dump_dict('old_d', old_d)
+            dump_dict('new_d', new_d)
+            print(st.trace_stubs(old_root, header='trace_stubs(old_root)'))
+            print(st.trace_stubs(new_root, header='trace_stubs(new_root)'))
+        if 0:  # separate unit test. Passed.
+            aList = st.sort_stubs_by_hierarchy(new_root)
+            dump_list(aList, 'after sort_stubs_by_hierarcy')
+        new_stubs = new_d.values()
+        st.merge_stubs(new_stubs, old_root, new_root, trace=False)
+        if 0:
+            print(st.trace_stubs(old_root, header='trace_stubs(old_root)'))
+    #@+node:ekr.20210804112556.1: *3* test_stub_class
+    def test_stub_class(self):
+        g = LeoGlobals()  # Use the g available to the script.
+        # Test equality...
+        stub1 = Stub(kind='def', name='foo')
+        stub2 = Stub(kind='class', name='foo')
+        stub3 = Stub(kind='def', name='bar')
+        stub4 = Stub(kind='def', name='foo')
+        stub4.out_list = ['xyzzy']  # Contents of out_list must not affect equality!
+        aList = [stub1, stub3]
+        self.assertNotEqual(stub1, stub2)
+        self.assertNotEqual(stub1, stub3)
+        self.assertEqual(stub1, stub4)
+        self.assertTrue(stub1 in aList)
+        self.assertFalse(stub2 in aList)
+        self.assertTrue(stub3 in aList)
+        # Test __hash__
+        d = {stub1: 'stub1'}
+        self.assertTrue(stub1 in d)
+        self.assertFalse(stub2 in d)
+        # Test parents and level.
+        stub_1 = Stub(kind='def', name='stub_1')
+        stub_2 = Stub(kind='def', name='stub_2', parent=stub_1, stack=['stub_1'])
+        stub_3 = Stub(kind='def', name='stub_3', parent=stub_2, stack=['stub_1', 'stub_2'])
+        self.assertEqual(stub_1.parents(), [], msg=repr(stub_1.parents()))
+        self.assertEqual(stub_2.parents(), ['stub_1'], msg=repr(stub_2.parents()))
+        self.assertEqual(stub_3.parents(), ['stub_1', 'stub_2'], msg=repr(stub_3.parents()))
+        self.assertEqual(stub_1.level(), 0)
+        self.assertEqual(stub_2.level(), 1)
+        self.assertEqual(stub_3.level(), 2)
+    #@+node:ekr.20160207115947.1: *3* test_truncate
+    def test_truncate(self):
+        table = (
+            ('abc',     'abc'),
+            ('abcd',    'abcd'),
+            ('abcde',   'abcde'),
+            ('abcdef',  'ab...'),
+            ('abcdefg', 'ab...'),
+        )
+        for s1, s2 in table:
+            got = truncate(s1, 5)
+            self.assertEqual(s2, got, msg=f"s1: {s1!r}")
+    #@-others
 #@-others
 g = LeoGlobals()
 g_input_file_name = None
diff --git a/make_stub_files.pyi b/make_stub_files.pyi
index 66b84fd..614112a 100644
--- a/make_stub_files.pyi
+++ b/make_stub_files.pyi
@@ -1,23 +1,36 @@
-# make_stub_files: Thu 17 Mar 2016 at 19:40:51
-
-from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
+# make_stub_files: Wed 04 Aug 2021 at 07:00:36
+from typing import Any, Dict, Optional, Sequence, Tuple, Union
 Node = Any
 def is_known_type(s: str) -> Any: ...
     #   0: return ReduceTypes().is_known_type(s)
     # ? 0: return ReduceTypes().is_known_type(str)
 def merge_types(a1: Any, a2: Any) -> str: ...
-def reduce_types(aList: List[Any], name: str=None, trace: bool=bool) -> Any: ...
+    #   0: return sorted(set(r1+r2))
+    #   0: return str
+def reduce_types(aList: List[Any], name: str=, trace: bool=) -> Any: ...
     #   0: return ReduceTypes(aList,name,trace).reduce_types()
     # ? 0: return ReduceTypes(List[Any], str, bool).reduce_types()
-def dump(title: Any, s: str=None) -> None: ...
+def dump(title: Any, s: str=) -> None: ...
 def dump_dict(title: Any, d: Any) -> None: ...
 def dump_list(title: Any, aList: List[Any]) -> None: ...
 def main() -> None: ...
 def pdb(self) -> None: ...
 def truncate(s: str, n: int) -> str: ...
+    #   0: return s if len(s)<=n else s[:n-]+
+    #   0: return str
 class AstFormatter:
-    def format(self, node: Node) -> Union[Any, str]: ...
-    def visit(self, node: Node) -> str: ...
+    def format(self, node: Node) -> Optional[Any]: ...
+        #   0: return val and val.strip() or
+        #   0: return Optional[Any]
+    def visit(self, node: Node) -> Optional[Union[Any, str]]: ...
+        #   0: return .join(self.visit(z) for z in node)
+        # ? 0: return .join(self.visit(z) for z in Node)
+        #   1: return
+        #   1: return 
+        #   2: return
+        #   2: return 
+        #   3: return s
+        #   3: return str
     def do_ClassDef(self, node: Node) -> str: ...
     def do_FunctionDef(self, node: Node) -> str: ...
     def do_Interactive(self, node: Node) -> None: ...
@@ -89,24 +102,87 @@ class AstFormatter:
     def kind(self, node: Node) -> Any: ...
         #   0: return node.__class__.__name__
         # ? 0: return Node.__class__.__name__
-    def indent(self, s: str) -> str: ...
-    def op_name(self, node: Node, strict: bool=bool) -> str: ...
+    def indent(self, s: str) -> Any: ...
+        #   0: return %(**self.level, s)
+        # ? 0: return %Tuple[*self.level, str]
+    def op_name(self, node: Node, strict: bool=) -> str: ...
+        #   0: return name
+        #   0: return str
 class AstArgFormatter(AstFormatter):
-    def do_BoolOp(self, node: Node) -> str: ...
-    def do_Bytes(self, node: Node) -> str: ...
-    def do_Name(self, node: Node) -> Union[Any, str]: ...
-    def do_Num(self, node: Node) -> str: ...
-    def do_Str(self, node: Node) -> str: ...
+    def do_BoolOp(self, node: Node) -> None: ...
+        #   0: return
+        #   0: return
+    def do_Bytes(self, node: Node) -> None: ...
+        #   0: return
+        #   0: return
+    def do_Name(self, node: Node) -> Optional[Any]: ...
+        #   0: return if node.id in (, ) else node.id
+        #   0: return Optional[Any]
+    def do_Num(self, node: Node) -> None: ...
+        #   0: return
+        #   0: return
+    def do_Str(self, node: Node) -> None: ...
+        #   0: return
+        #   0: return
+class Controller:
+    def __init__(self) -> None: ...
+    def finalize(self, fn: str) -> Any: ...
+        #   0: return os.path.normpath(os.path.abspath(os.path.expanduser(fn)))
+        # ? 0: return os.path.normpath(os.path.abspath(os.path.expanduser(str)))
+    def make_stub_file(self, fn: str) -> None: ...
+    def run(self) -> None: ...
+    def run_all_unit_tests(self) -> None: ...
+    def scan_command_line(self) -> None: ...
+    def scan_options(self) -> None: ...
+    def make_op_name_dict(self) -> Any: ...
+        #   0: return d
+        # ? 0: return d
+    def create_parser(self) -> Any: ...
+        #   0: return parser
+        # ? 0: return optparse.OptionParser
+    def find_pattern_ops(self, pattern: Any) -> Union[Any, List]: ...
+        #   0: return []
+        #   0: return List
+        #   1: return ops
+        # ? 1: return ops
+    def get_config_string(self) -> Optional[Any]: ...
+        #   0: return f.read()
+        # ? 0: return f.read()
+        #   1: return
+        #   1: return
+    def init_parser(self, s: str) -> None: ...
+    def is_section_name(self, s: str) -> Optional[Any]:
+        #   0: return s.strip().lower().replace()
+        # ? 0: return str.strip().lower().replace()
+        #   1: return
+        #   1: return 
+        #   2: return
+        #   2: return
+        def munge(s: str) -> Any: ...
+            #   0: return s.strip().lower().replace()
+            # ? 0: return str.strip().lower().replace()
+    def make_patterns_dict(self) -> None: ...
+    def scan_patterns(self, section_name: Any) -> List[Any]: ...
+        #   0: return aList
+        #   0: return List[Any]
 class LeoGlobals:
     class NullObject:
         def __init__(self, *args, **keys) -> None: ...
         def __call__(self, *args, **keys) -> Any: ...
             #   0: return self
             # ? 0: return self
-        def __repr__(self) -> str: ...
-        def __str__(self) -> str: ...
-        def __bool__(self) -> bool: ...
-        def __nonzero__(self) -> number: ...
+        def __repr__(self) -> None: ...
+            #   0: return
+            #   0: return
+        def __str__(self) -> None: ...
+            #   0: return
+            #   0: return
+        def __bool__(self) -> None: ...
+            #   0: return
+            #   0: return
+        def __nonzero__(self) -> None: ...
+            #   0: return
+            #   0: return
         def __delattr__(self, attr: Any) -> Any: ...
             #   0: return self
             # ? 0: return self
@@ -116,103 +192,197 @@ class LeoGlobals:
         def __setattr__(self, attr: Any, val: Any) -> Any: ...
             #   0: return self
             # ? 0: return self
-    def _callerName(self, n: int=1, files: Any=bool) -> str: ...
-    def callers(self, n: int=4, count: Any=0, excludeCaller: Any=bool, files: Any=bool) -> str: ...
+    def _callerName(self, n: int=, files: Any=) -> Optional[Union[Any, str]]: ...
+        #   0: return %(self.shortFileName(code1.co_filename), name)
+        # ? 0: return %Tuple[self.shortFileName(code1.co_filename), str]
+        #   1: return name
+        #   1: return str
+        #   2: return
+        #   2: return 
+        #   3: return
+        #   3: return
+    def caller(self, i: int=) -> Any: ...
+        #   0: return self.callers(i+).split()[]
+        # ? 0: return self.callers(int+).split()[]
+    def callers(self, n: int=, count: Any=, excludeCaller: Any=, files: Any=) -> str: ...
+        #   0: return sep.join(result)
+        #   0: return str
     def cls(self) -> None: ...
+    def execute_shell_commands(self, commands: Any, trace: bool=) -> None: ...
+    def isString(self, s: str) -> Any: ...
+        #   0: return isinstance(s,str)
+        # ? 0: return isinstance(str, str)
+        #   1: return isinstance(s,types.StringTypes)
+        # ? 1: return isinstance(str, types.StringTypes)
+    def isUnicode(self, s: str) -> Any: ...
+        #   0: return isinstance(s,str)
+        # ? 0: return isinstance(str, str)
+        #   1: return isinstance(s,types.UnicodeType)
+        # ? 1: return isinstance(str, types.UnicodeType)
+    def objToString(self, obj: Any, indent: Any=, printCaller: Any=, tag: Any=) -> Union[Any, str]: ...
+        #   0: return %(prefix, s) if prefix else s
+        #   0: return Union[Any, str]
+    def dictToString(self, d: Any, indent: Any=, tag: Any=) -> Optional[Union[Any, str]]: ...
+        #   0: return
+        #   0: return 
+        #   1: return %(tag, s) if tag else s
+        #   1: return Union[Any, str]
+    def listToString(self, obj: Any, indent: Any=, tag: Any=) -> Optional[Union[Any, str]]: ...
+        #   0: return
+        #   0: return 
+        #   1: return %(tag, s) if tag else s
+        #   1: return Union[Any, str]
+    def tupleToString(self, obj: Any, indent: Any=, tag: Any=) -> Optional[Union[Any, str]]: ...
+        #   0: return
+        #   0: return 
+        #   1: return %(tag, s) if tag else s
+        #   1: return Union[Any, str]
     def pdb(self) -> None: ...
-    def shortFileName(self, fileName: Any, n: int=None) -> str: ...
-    def splitLines(self, s: str) -> Union[Any, List[]]: ...
+    def printObj(self, obj: Any, indent: Any=, printCaller: Any=, tag: Any=) -> None: ...
+    def shortFileName(self, fileName: Any, n: int=) -> Union[Any, str]: ...
+        #   0: return os.path.basename(fileName)
+        #   0: return str
+        #   1: return .join(fileName.replace().split()[-n:])
+        # ? 1: return .join(fileName.replace().split()[int:])
+    def splitLines(self, s: str) -> Union[Any, List]: ...
+        #   0: return s.splitlines() if s else []
+        #   0: return Union[Any, List]
     def trace(self, *args, **keys) -> None: ...
-class Pattern(object):
-    def __init__(self, find_s: str, repl_s: str='') -> None: ...
+class Pattern:
+    def __init__(self, find_s: str, repl_s: str=) -> None: ...
     def __eq__(self, obj: Any) -> bool: ...
+        #   0: return self.find_s==obj.find_s and self.repl_s==obj.repl_s
+        #   0: return bool
+        #   1: return NotImplemented
+        #   1: return bool
     def __ne__(self, obj: Any) -> bool: ...
+        #   0: return not self.__eq__(obj)
+        #   0: return bool
     def __hash__(self) -> int: ...
-    def __repr__(self) -> str: ...
-    def is_balanced(self) -> bool: ...
+    def __repr__(self) -> Any: ...
+        #   0: return %(self.find_s, self.repl_s)
+        # ? 0: return %Tuple[self.find_s, self.repl_s]
+    def is_balanced(self) -> None: ...
+        #   0: return
+        #   0: return 
+        #   1: return
+        #   1: return 
+        #   2: return
+        #   2: return
     def is_regex(self) -> Any: ...
-        #   0: return self.find_s.endswith('$')
-        # ? 0: return self.find_s.endswith(str)
+        #   0: return self.find_s.endswith()
+        # ? 0: return self.find_s.endswith()
     def all_matches(self, s: str) -> List[Any]: ...
+        #   0: return aList
+        #   0: return List[Any]
+        #   1: return list(self.regex.finditer(s))
+        #   1: return List[self.regex.finditer(str)]
     def full_balanced_match(self, s: str, i: int) -> Optional[int]: ...
-    def match_balanced(self, delim: Any, s: str, i: int) -> number: ...
-    def match(self, s: str, trace: bool=bool) -> Tuple[bool, str]: ...
+        #   0: return i if found else
+        #   0: return Optional[int]
+    def match_balanced(self, delim: Any, s: str, i: int) -> Union[Any, int]: ...
+        #   0: return i
+        #   0: return int
+        #   1: return len(s)+
+        # ? 1: return int+
+    def match(self, s: str, trace: bool=) -> Tuple[, str]: ...
+        #   0: return (, s)
+        #   0: return Tuple[, str]
+        #   1: return (, s)
+        #   1: return Tuple[, str]
+        #   2: return (, s)
+        #   2: return Tuple[, str]
+        #   3: return (, s)
+        #   3: return Tuple[, str]
     def match_entire_string(self, s: str) -> Union[Union[Any, bool], bool]: ...
+        #   0: return j==len(s)
+        #   0: return bool
+        #   1: return m and m.group()==s
+        #   1: return Union[Any, bool]
     def replace(self, m: Any, s: str) -> Any: ...
         #   0: return self.replace_balanced(s,start,end)
         # ? 0: return self.replace_balanced(str, start, end)
         #   1: return self.replace_regex(m,s)
         # ? 1: return self.replace_regex(m, str)
     def replace_balanced(self, s1: str, start: Any, end: Any) -> str: ...
+        #   0: return s1[:start]+r+s1[end:]
+        #   0: return str
+        #   1: return s1[:start]+r+s1[end:]
+        #   1: return str
+        #   2: return s1[:start]+repl+s1[end:]
+        #   2: return str
     def replace_regex(self, m: Any, s: str) -> str: ...
+        #   0: return s
+        #   0: return str
 class ReduceTypes:
-    def __init__(self, aList: List[Any]=None, name: str=None, trace: bool=bool) -> None: ...
-    def is_known_type(self, s: str) -> Union[Union[Any, bool], bool]: ...
-    def reduce_collection(self, aList: List[Any], kind: Any) -> Any: ...
-        #   0: return result
-        # ? 0: return result
+    def __init__(self, aList: List[Any]=, name: str=, trace: bool=) -> None: ...
+    def is_known_type(self, s: str) -> Optional[Optional[Any]]: ...
+        #   0: return
+        #   0: return 
+        #   1: return
+        #   1: return 
+        #   2: return self.is_known_type(inner) if inner else
+        #   2: return Optional[Any]
+        #   3: return self.is_known_type(inner) if inner else
+        #   3: return Optional[Any]
+        #   4: return
+        #   4: return 
+        #   5: return
+        #   5: return 
+        #   6: return
+        #   6: return 
+        #   7: return
+        #   7: return
+    def reduce_collection(self, aList: List[Any], kind: Any) -> str: ...
+        #   0: return sorted(set(result))
+        #   0: return str
     def reduce_numbers(self, aList: List[Any]) -> List[Any]: ...
+        #   0: return aList
+        #   0: return List[Any]
     def reduce_types(self) -> Any: ...
-        #   0: return self.show('None')
-        # ? 0: return self.show(str)
-        #   1: return self.show(r[0])
+        #   0: return self.show()
+        # ? 0: return self.show()
+        #   1: return self.show(r[])
         # ? 1: return self.show(str)
-        #   2: return self.show('Union[%s]'%', '.join(sorted(r)))
-        # ? 2: return self.show(str)
+        #   2: return self.show(%.join(sorted(r)))
+        # ? 2: return self.show(%.join(str))
     def reduce_unknowns(self, aList: List[Any]) -> Any: ...
-        #   0: return z if self.is_known_type(z) else 'Any'  for z in aList
-        # ? 0: return Union[Any, str] for z in List[Any]
-    def show(self, s: str, known: Any=bool) -> str: ...
+        #   0: return z if self.is_known_type(z) else   for z in aList
+        # ? 0: return Optional[Any] for z in List[Any]
+    def show(self, s: str, known: Any=) -> str: ...
+        #   0: return s
+        #   0: return str
     def split_types(self, s: str) -> List[Any]: ...
-class StandAloneMakeStubFile:
-    def __init__(self) -> None: ...
-    def finalize(self, fn: str) -> str: ...
-    def make_stub_file(self, fn: str) -> None: ...
-    def run(self) -> None: ...
-    def run_all_unit_tests(self) -> None: ...
-    def scan_command_line(self) -> None: ...
-    def scan_options(self) -> None: ...
-    def make_op_name_dict(self) -> Any: ...
-        #   0: return d
-        # ? 0: return d
-    def create_parser(self) -> Any: ...
-        #   0: return parser
-        # ? 0: return optparse.OptionParser
-    def find_pattern_ops(self, pattern: Any) -> Union[Any, List[]]: ...
-        #   0: return []
-        #   0: return List[]
-        #   1: return ops
-        # ? 1: return ops
-    def get_config_string(self) -> str: ...
-    def init_parser(self, s: str) -> None: ...
-    def is_section_name(self, s: str) -> Union[Any, bool]: ...
-        #   0: return s.strip().lower().replace(' ','')
-        # ? 0: return str.strip().lower().replace(str, str)
-        #   1: return bool
-        #   1: return bool
-        #   2: return bool
-        #   2: return bool
-        def munge(s: str) -> Any: ...
-            #   0: return s.strip().lower().replace(' ','')
-            # ? 0: return str.strip().lower().replace(str, str)
-    def make_patterns_dict(self) -> None: ...
-    def scan_patterns(self, section_name: Any) -> List[Any]: ...
-class Stub(object):
-    def __init__(self, kind: Any, name: str, parent: Any=None, stack: Any=None) -> None: ...
+        #   0: return aList
+        #   0: return List[Any]
+class Stub:
+    def __init__(self, kind: Any, name: str, parent: Any=, stack: Any=) -> None: ...
     def __eq__(self, obj: Any) -> bool: ...
+        #   0: return self.full_name==obj.full_name and self.kind==obj.kind
+        #   0: return bool
+        #   1: return NotImplemented
+        #   1: return bool
     def __ne__(self, obj: Any) -> bool: ...
+        #   0: return not self.__eq__(obj)
+        #   0: return bool
     def __hash__(self) -> int: ...
-    def __repr__(self) -> str: ...
-    def __str__(self) -> str: ...
+    def __repr__(self) -> Any: ...
+        #   0: return %(self.full_name, g.objToString(self.out_list))
+        # ? 0: return %Tuple[self.full_name, g.objToString(self.out_list)]
     def level(self) -> int: ...
+        #   0: return len(self.parents())
+        #   0: return int
     def parents(self) -> Any: ...
-        #   0: return self.full_name.split('.')[:-1]
-        # ? 0: return self.full_name.split(str)[:number]
+        #   0: return self.full_name.split()[:-]
+        # ? 0: return self.full_name.split()[:]
 class StubFormatter(AstFormatter):
-    seen_names = Any # Added...
     def __init__(self, controller: StandAloneMakeStubFile, traverser: Any) -> None: ...
-    def match_all(self, node: Node, s: str, trace: bool=bool) -> str: ...
-    # def visit(self, node: Node) -> str: ...
+    def match_all(self, node: Node, s: str, trace: bool=) -> str: ...
+        #   0: return s
+        #   0: return str
+    def visit(self, node: Node) -> str: ...
+        #   0: return s
+        #   0: return str
     def trace_visitor(self, node: Node, op: Any, s: str) -> None: ...
     def do_Attribute(self, node: Node) -> str: ...
     def do_Bytes(self, node: Node) -> str: ...
@@ -232,10 +402,11 @@ class StubFormatter(AstFormatter):
     def do_UnaryOp(self, node: Node) -> str: ...
     def do_Return(self, node: Node) -> str: ...
 class StubTraverser(ast.NodeVisitor):
-    attrs_seen = List[Any]
     def __init__(self, controller: StandAloneMakeStubFile) -> None: ...
     def add_stub(self, d: Any, stub: Any) -> None: ...
-    def indent(self, s: str) -> str: ...
+    def indent(self, s: str) -> Any: ...
+        #   0: return %(**self.level, s)
+        # ? 0: return %Tuple[*self.level, str]
     def out(self, s: str) -> None: ...
     def run(self, node: Node) -> None: ...
     def output_stubs(self, stub: Any) -> None: ...
@@ -250,47 +421,73 @@ class StubTraverser(ast.NodeVisitor):
         #   3: return new_root
         # ? 3: return new_root
     def get_stub_file(self, fn: str) -> Optional[str]: ...
+        #   0: return s
+        #   0: return str
+        #   1: return
+        #   1: return
     def parse_stub_file(self, s: str, root_name: Any) -> Tuple[Any, Any]: ...
-    def merge_stubs(self, new_stubs: Any, old_root: Any, new_root: Any, trace: bool=bool) -> None: ...
+        #   0: return (d, root)
+        #   0: return Tuple[d, root]
+    def merge_stubs(self, new_stubs: Any, old_root: Any, new_root: Any, trace: bool=) -> None: ...
     def check_delete(self, new_stubs: Any, old_root: Any, new_root: Any, trace: bool) -> Any: ...
         #   0: return delete_list
         # ? 0: return delete_list
     def flatten_stubs(self, root: Any) -> List[Any]: ...
+        #   0: return aList
+        #   0: return List[Any]
     def flatten_stubs_helper(self, root: Any, aList: List[Any]) -> None: ...
     def find_parent_stub(self, stub: Any, root: Any) -> Optional[Any]: ...
+        #   0: return self.find_stub(stub.parent,root) if stub.parent else
+        #   0: return Optional[Any]
     def find_stub(self, stub: Any, root: Any) -> Optional[Any]: ...
         #   0: return root
         # ? 0: return root
         #   1: return stub2
         # ? 1: return stub2
-        #   2: return None
-        #   2: return None
-    def sort_stubs_by_hierarchy(self, stubs1: Any) -> Union[Any, List[]]: ...
+        #   2: return
+        #   2: return
+    def sort_stubs_by_hierarchy(self, stubs1: Any) -> Union[Any, List]: ...
         #   0: return result
         # ? 0: return result
         #   1: return []
-        #   1: return List[]
-    def trace_stubs(self, stub: Any, aList: List[Any]=None, header: Any=None, level: Any=-1) -> str: ...
+        #   1: return List
+    def trace_stubs(self, stub: Any, aList: List[Any]=, header: Any=, level: Any=-) -> Optional[Any]: ...
+        #   0: return .join(aList)+
+        # ? 0: return .join(List[Any])+
+        #   1: return
+        #   1: return
     def visit_ClassDef(self, node: Node) -> None: ...
     def visit_FunctionDef(self, node: Node) -> None: ...
-    def format_arguments(self, node: Node) -> str: ...
-    def munge_arg(self, s: str) -> str: ...
-    def format_returns(self, node: Node) -> Union[Any, str]: ...
-        #   0: return 'None: ...'
+    def format_arguments(self, node: Node) -> Any: ...
+        #   0: return .join(result)
+        # ? 0: return .join(result)
+    def munge_arg(self, s: str) -> Union[Any, str]: ...
+        #   0: return s
         #   0: return str
-        #   1: return s+': ...'
+        #   1: return %(s, pattern.repl_s)
+        # ? 1: return %Tuple[str, pattern.repl_s]
+        #   2: return s
+        #   2: return str
+        #   3: return s+
+        #   3: return str
+    def format_returns(self, node: Node) -> Union[Any, str]: ...
+        #   0: return +tail
+        # ? 0: return +tail
+        #   1: return s+
         #   1: return str
-        #   2: return self.format_return_expressions(name,raw,r)
-        # ? 2: return self.format_return_expressions(str, raw, r)
-    def format_return_expressions(self, name: str, raw_returns: Any, reduced_returns: Any) -> str: ...
+        #   2: return self.format_return_expressions(node,name,raw,r)
+        # ? 2: return self.format_return_expressions(Node, str, raw, r)
+    def format_return_expressions(self, node: Node, name: str, raw_returns: Any, reduced_returns: Any) -> Union[Any, str]: ...
+        #   0: return s+tail+results
+        #   0: return str
+        #   1: return +tail+results
+        # ? 1: return +tail+results
+        #   2: return s+tail
+        #   2: return str
     def get_def_name(self, node: Node) -> str: ...
+        #   0: return name
+        #   0: return str
     def remove_recursive_calls(self, name: str, raw: Any, reduced: Any) -> Tuple[Any, Any]: ...
+        #   0: return (raw_result, reduced_result)
+        #   0: return Tuple[raw_result, reduced_result]
     def visit_Return(self, node: Node) -> None: ...
-class TestClass:
-    def parse_group(group: str) -> Tuple[int, str]: ...
-    def return_all(self) -> bool: ...
-    def return_array() -> Any: ...
-        #   0: return f(s[1:-1])
-        # ? 0: return f(str)
-    def return_list(self, a: str) -> List[str]: ...
-    def return_two_lists(s: str) -> List[Any]: ...
diff --git a/theory.md b/theory.md
index 27aaf55..b1d44f0 100644
--- a/theory.md
+++ b/theory.md
@@ -48,7 +48,7 @@ The previous section is really you should need to know about this program.  Howe
 Given the file truncate.py:
 
     def truncate(s, n):
-        '''Return s truncated to n characters.'''
+        """Return s truncated to n characters."""
         return s if len(s) <= n else s[:n-3] + '...'
         
 The script produces this output with the --verbose option in effect:


Test patch:
diff --git a/test.cfg b/test.cfg
deleted file mode 100644
index 65d53e0..0000000
--- a/test.cfg
+++ /dev/null
@@ -1,42 +0,0 @@
-
-# A configuration file to make stubs for make_stub_files.py itself.
-
-[Global]
-
-files: test/test.py
-output_directory: ./test
-# prefix_lines:
-
-[Def Name Patterns]
-
-[General Patterns]
-
-# Declarations of names..
-a: Any
-s: str
-aList: List[Any]
-is_known_type(*): bool
-
-# Known functions...
-all(*): bool
-any(*): bool
-endswith(*): bool
-g.match_word(*): bool
-hash(*): int
-hasattr(*): bool
-int(*): int
-len(*): int
-os.path.basename(*): str
-os.sep.join(*): str
-repr(*): str
-sep.join(*): str
-self.__eq__(*): bool
-self.__ne__(*): bool
-self.__gt__(*): bool
-self.__lt__(*): bool
-sorted(*): str
-str%(*): str
-str.join(*): str
-str.isspace(*): str
-str[*]: str
-sum(*): number