Skip to content

Commit f5e3cb8

Browse files
committed
minor refactorings
1 parent 07083b8 commit f5e3cb8

File tree

3 files changed

+279
-7
lines changed

3 files changed

+279
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env python3
2+
3+
from importlib import import_module
4+
import sys
5+
6+
7+
SP = '\N{SPACE}'
8+
HLIN = '\N{BOX DRAWINGS LIGHT HORIZONTAL}' * 2 + SP # ──
9+
VLIN = '\N{BOX DRAWINGS LIGHT VERTICAL}' + SP * 3 # │
10+
TEE = '\N{BOX DRAWINGS LIGHT VERTICAL AND RIGHT}' + HLIN # ├──
11+
ELBOW = '\N{BOX DRAWINGS LIGHT UP AND RIGHT}' + HLIN # └──
12+
13+
14+
def subclasses(cls):
15+
try:
16+
return cls.__subclasses__()
17+
except TypeError: # handle the `type` type
18+
return cls.__subclasses__(cls)
19+
20+
21+
def tree(cls, level=0, last_sibling=True):
22+
yield cls, level, last_sibling
23+
chidren = subclasses(cls)
24+
if chidren:
25+
last = chidren[-1]
26+
for child in chidren:
27+
yield from tree(child, level + 1, child is last)
28+
29+
30+
def render_lines(tree_generator):
31+
cls, _, _ = next(tree_generator)
32+
yield cls.__name__
33+
prefix = ''
34+
for cls, level, last in tree_generator:
35+
prefix = prefix[: 4 * (level - 1)]
36+
prefix = prefix.replace(TEE, VLIN).replace(ELBOW, SP * 4)
37+
prefix += ELBOW if last else TEE
38+
yield prefix + cls.__name__
39+
40+
41+
def draw(cls):
42+
for line in render_lines(tree(cls)):
43+
print(line)
44+
45+
46+
def parse(name):
47+
if '.' in name:
48+
return name.rsplit('.', 1)
49+
else:
50+
return 'builtins', name
51+
52+
53+
def main(name):
54+
module_name, cls_name = parse(name)
55+
try:
56+
cls = getattr(import_module(module_name), cls_name)
57+
except ModuleNotFoundError:
58+
print(f'*** Could not import {module_name!r}.')
59+
except AttributeError:
60+
print(f'*** {cls_name!r} not found in {module_name!r}.')
61+
else:
62+
if isinstance(cls, type):
63+
draw(cls)
64+
else:
65+
print(f'*** {cls_name!r} is not a class.')
66+
67+
68+
if __name__ == '__main__':
69+
if len(sys.argv) == 2:
70+
main(sys.argv[1])
71+
else:
72+
print('Usage:'
73+
f'\t{sys.argv[0]} Class # for builtin classes\n'
74+
f'\t{sys.argv[0]} package.Class # for other classes'
75+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
from textwrap import dedent
2+
from typing import SupportsBytes
3+
from classtree import tree, render_lines, main, subclasses
4+
5+
from abc import ABCMeta
6+
7+
8+
def test_subclasses():
9+
result = subclasses(UnicodeError)
10+
assert set(result) >= {UnicodeEncodeError, UnicodeDecodeError}
11+
12+
13+
def test_subclasses_of_type():
14+
"""
15+
The `type` class is a special case because `type.__subclasses__()`
16+
is an unbound method when called on it, so we must call it as
17+
`type.__subclasses__(type)` just for `type`.
18+
19+
This test does not verify the full list of results, but just
20+
checks that `abc.ABCMeta` is included, because that's the only
21+
subclass of `type` (i.e. metaclass) I we get when I run
22+
`$ classtree.py type` at the command line.
23+
24+
However, the Python console and `pytest` both load other modules,
25+
so `subclasses` may find more subclasses of `type`—for example,
26+
`enum.EnumMeta`.
27+
"""
28+
result = subclasses(type)
29+
assert ABCMeta in result
30+
31+
32+
def test_tree_1_level():
33+
result = list(tree(TabError))
34+
assert result == [(TabError, 0, True)]
35+
36+
37+
def test_tree_2_levels():
38+
result = list(tree(IndentationError))
39+
assert result == [
40+
(IndentationError, 0, True),
41+
(TabError, 1, True),
42+
]
43+
44+
45+
def test_render_lines_1_level():
46+
result = list(render_lines(tree(TabError)))
47+
assert result == ['TabError']
48+
49+
50+
def test_render_lines_2_levels_1_leaf():
51+
result = list(render_lines(tree(IndentationError)))
52+
expected = [
53+
'IndentationError',
54+
'└── TabError',
55+
]
56+
assert expected == result
57+
58+
59+
def test_render_lines_3_levels_1_leaf():
60+
class X: pass
61+
class Y(X): pass
62+
class Z(Y): pass
63+
result = list(render_lines(tree(X)))
64+
expected = [
65+
'X',
66+
'└── Y',
67+
' └── Z',
68+
]
69+
assert expected == result
70+
71+
72+
def test_render_lines_4_levels_1_leaf():
73+
class Level0: pass
74+
class Level1(Level0): pass
75+
class Level2(Level1): pass
76+
class Level3(Level2): pass
77+
78+
result = list(render_lines(tree(Level0)))
79+
expected = [
80+
'Level0',
81+
'└── Level1',
82+
' └── Level2',
83+
' └── Level3',
84+
]
85+
assert expected == result
86+
87+
88+
def test_render_lines_2_levels_2_leaves():
89+
class Branch: pass
90+
class Leaf1(Branch): pass
91+
class Leaf2(Branch): pass
92+
result = list(render_lines(tree(Branch)))
93+
expected = [
94+
'Branch',
95+
'├── Leaf1',
96+
'└── Leaf2',
97+
]
98+
assert expected == result
99+
100+
101+
def test_render_lines_3_levels_2_leaves_dedent():
102+
class A: pass
103+
class B(A): pass
104+
class C(B): pass
105+
class D(A): pass
106+
class E(D): pass
107+
108+
result = list(render_lines(tree(A)))
109+
expected = [
110+
'A',
111+
'├── B',
112+
'│ └── C',
113+
'└── D',
114+
' └── E',
115+
]
116+
assert expected == result
117+
118+
119+
def test_render_lines_4_levels_4_leaves_dedent():
120+
class A: pass
121+
class B1(A): pass
122+
class C1(B1): pass
123+
class D1(C1): pass
124+
class D2(C1): pass
125+
class C2(B1): pass
126+
class B2(A): pass
127+
expected = [
128+
'A',
129+
'├── B1',
130+
'│ ├── C1',
131+
'│ │ ├── D1',
132+
'│ │ └── D2',
133+
'│ └── C2',
134+
'└── B2',
135+
]
136+
137+
result = list(render_lines(tree(A)))
138+
assert expected == result
139+
140+
141+
def test_main_simple(capsys):
142+
main('IndentationError')
143+
expected = dedent("""
144+
IndentationError
145+
└── TabError
146+
""").lstrip()
147+
captured = capsys.readouterr()
148+
assert captured.out == expected
149+
150+
151+
def test_main_dotted(capsys):
152+
main('collections.abc.Sequence')
153+
expected = dedent("""
154+
Sequence
155+
├── ByteString
156+
├── MutableSequence
157+
│ └── UserList
158+
""").lstrip()
159+
captured = capsys.readouterr()
160+
assert captured.out.startswith(expected)
161+
162+
163+
def test_main_class_not_found(capsys):
164+
main('NoSuchClass')
165+
expected = "*** 'NoSuchClass' not found in 'builtins'.\n"
166+
captured = capsys.readouterr()
167+
assert captured.out == expected
168+
169+
170+
def test_main_module_not_found(capsys):
171+
main('nosuch.module')
172+
expected = "*** Could not import 'nosuch'.\n"
173+
captured = capsys.readouterr()
174+
assert captured.out == expected
175+
176+
177+
def test_main_not_a_class(capsys):
178+
main('collections.abc')
179+
expected = "*** 'abc' is not a class.\n"
180+
captured = capsys.readouterr()
181+
assert captured.out == expected

17-it-generator/tree/extra/drawtree.py

+23-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
11
from tree import tree
22

3-
SP = '\N{SPACE}'
4-
HLIN = '\N{BOX DRAWINGS LIGHT HORIZONTAL}' # ─
5-
ELBOW = f'\N{BOX DRAWINGS LIGHT UP AND RIGHT}{HLIN*2}{SP}' # └──
6-
TEE = f'\N{BOX DRAWINGS LIGHT VERTICAL AND RIGHT}{HLIN*2}{SP}' # ├──
7-
PIPE = f'\N{BOX DRAWINGS LIGHT VERTICAL}{SP*3}' # │
3+
SP = '\N{SPACE}'
4+
HLIN = '\N{BOX DRAWINGS LIGHT HORIZONTAL}' * 2 + SP # ──
5+
VLIN = '\N{BOX DRAWINGS LIGHT VERTICAL}' + SP * 3 # │
6+
TEE = '\N{BOX DRAWINGS LIGHT VERTICAL AND RIGHT}' + HLIN # ├──
7+
ELBOW = '\N{BOX DRAWINGS LIGHT UP AND RIGHT}' + HLIN # └──
8+
9+
10+
def subclasses(cls):
11+
try:
12+
return cls.__subclasses__()
13+
except TypeError: # handle the `type` type
14+
return cls.__subclasses__(cls)
15+
16+
17+
def tree(cls, level=0, last_sibling=True):
18+
yield cls, level, last_sibling
19+
children = subclasses(cls)
20+
if children:
21+
last = children[-1]
22+
for child in children:
23+
yield from tree(child, level+1, child is last)
824

925

1026
def render_lines(tree_iter):
@@ -13,8 +29,8 @@ def render_lines(tree_iter):
1329
prefix = ''
1430

1531
for cls, level, last in tree_iter:
16-
prefix = prefix[:4 * (level-1)]
17-
prefix = prefix.replace(TEE, PIPE).replace(ELBOW, SP*4)
32+
prefix = prefix[:4 * (level - 1)]
33+
prefix = prefix.replace(TEE, VLIN).replace(ELBOW, SP * 4)
1834
prefix += ELBOW if last else TEE
1935
yield prefix + cls.__name__
2036

0 commit comments

Comments
 (0)