pyouter 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyouter/__init__.py +16 -0
- pyouter/__main__.py +5 -0
- pyouter/app.py +97 -0
- pyouter/bash/__init__.py +0 -0
- pyouter/bash/install.py +27 -0
- pyouter/default.py +73 -0
- pyouter/errors.py +10 -0
- pyouter/executor.py +46 -0
- pyouter/fish/__init__.py +0 -0
- pyouter/fish/install.py +38 -0
- pyouter/oh_my_zsh/__init__.py +0 -0
- pyouter/oh_my_zsh/install.py +72 -0
- pyouter/router.py +139 -0
- pyouter/web.py +718 -0
- pyouter/web_page.py +870 -0
- pyouter-0.1.2.dist-info/METADATA +226 -0
- pyouter-0.1.2.dist-info/RECORD +26 -0
- pyouter-0.1.2.dist-info/WHEEL +5 -0
- pyouter-0.1.2.dist-info/entry_points.txt +2 -0
- pyouter-0.1.2.dist-info/licenses/LICENSE +21 -0
- pyouter-0.1.2.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test.py +102 -0
- tests/test_async.py +35 -0
- tests/test_router_tree_view.py +156 -0
- tests/test_web.py +312 -0
pyouter/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .router import Router
|
|
2
|
+
from .oh_my_zsh.install import install as omz
|
|
3
|
+
from .fish.install import install as fish
|
|
4
|
+
from .bash.install import install as bash
|
|
5
|
+
from .app import App
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def runner():
|
|
9
|
+
app = App()
|
|
10
|
+
app.use(
|
|
11
|
+
router=Router(
|
|
12
|
+
install=Router(
|
|
13
|
+
ohmyzsh=omz,
|
|
14
|
+
fish=fish,
|
|
15
|
+
bash=bash)))
|
|
16
|
+
app.run()
|
pyouter/__main__.py
ADDED
pyouter/app.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Any
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from pyouter.default import create_parser
|
|
6
|
+
from pyouter.errors import NotInit
|
|
7
|
+
from pyouter.router import Router
|
|
8
|
+
|
|
9
|
+
class App(object):
|
|
10
|
+
def __init__(self, config=None, parser=None):
|
|
11
|
+
opt_parser = parser if parser is not None else create_parser("tasks router runner")
|
|
12
|
+
|
|
13
|
+
self.opt_parser = opt_parser
|
|
14
|
+
self.options = None
|
|
15
|
+
self.executor = None
|
|
16
|
+
|
|
17
|
+
if config is None:
|
|
18
|
+
self.config = {}
|
|
19
|
+
elif type(config)==type(''):
|
|
20
|
+
import json
|
|
21
|
+
with open(config,'r') as f:
|
|
22
|
+
self.config = json.load(f)
|
|
23
|
+
elif type(config)==type({}):
|
|
24
|
+
self.config = config
|
|
25
|
+
else:
|
|
26
|
+
raise NotInit("config type unknonw")
|
|
27
|
+
|
|
28
|
+
self.router: Router
|
|
29
|
+
|
|
30
|
+
def use(self, router: Router):
|
|
31
|
+
self.router = router
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
def option(self, *args: Any, **kwds: Any):
|
|
35
|
+
self.opt_parser.add_argument(*args, **kwds)
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def web(self, host="127.0.0.1", port=8765, open_browser=True, focus=None):
|
|
39
|
+
"""Start the local web ui for this app's router."""
|
|
40
|
+
if self.router is None:
|
|
41
|
+
raise NotInit("self.router in App")
|
|
42
|
+
|
|
43
|
+
if self.config is None:
|
|
44
|
+
raise NotInit("self.config in App")
|
|
45
|
+
|
|
46
|
+
if self.options is None:
|
|
47
|
+
self.options = self.opt_parser.parse_args()
|
|
48
|
+
|
|
49
|
+
self.router.context(self.config, self.options, self.executor)
|
|
50
|
+
|
|
51
|
+
from pyouter.web import serve
|
|
52
|
+
serve(
|
|
53
|
+
self,
|
|
54
|
+
host=host,
|
|
55
|
+
port=port,
|
|
56
|
+
open_browser=open_browser,
|
|
57
|
+
focus="" if focus is None else focus,
|
|
58
|
+
)
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def run(self):
|
|
62
|
+
if self.router is None:
|
|
63
|
+
raise NotInit("self.router in App")
|
|
64
|
+
|
|
65
|
+
if self.config is None:
|
|
66
|
+
raise NotInit("self.config in App")
|
|
67
|
+
|
|
68
|
+
if self.options is None:
|
|
69
|
+
self.options = self.opt_parser.parse_args()
|
|
70
|
+
|
|
71
|
+
self.router.context(self.config, self.options, self.executor)
|
|
72
|
+
|
|
73
|
+
if getattr(self.options, 'web', False):
|
|
74
|
+
self.web(
|
|
75
|
+
host=getattr(self.options, "host", "127.0.0.1"),
|
|
76
|
+
port=getattr(self.options, "port", 8765),
|
|
77
|
+
open_browser=not getattr(self.options, "no_browser", False),
|
|
78
|
+
focus=self.options.actions,
|
|
79
|
+
)
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
if getattr(self.options, 'tree', False):
|
|
83
|
+
if self.options.actions:
|
|
84
|
+
# 如果有actions参数,显示路径追踪和子树
|
|
85
|
+
self.router.print_tree(self.options.actions)
|
|
86
|
+
else:
|
|
87
|
+
# 如果没有actions参数,显示完整树
|
|
88
|
+
self.router.print_tree()
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
if self.options.tasks:
|
|
92
|
+
for task in self.router.tasks():
|
|
93
|
+
print(task)
|
|
94
|
+
else:
|
|
95
|
+
asyncio.get_event_loop().run_until_complete(self.router.dispatch('', self.options.actions, 0))
|
|
96
|
+
|
|
97
|
+
return self
|
pyouter/bash/__init__.py
ADDED
|
File without changes
|
pyouter/bash/install.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import string
|
|
2
|
+
|
|
3
|
+
def install(config, options):
|
|
4
|
+
command = string.Template("""#!/usr/bin/env pyouter.bash
|
|
5
|
+
# please save this script into your pyouter.bash completion dir and restart
|
|
6
|
+
# your base or source it use
|
|
7
|
+
# . script_name
|
|
8
|
+
# in MacOS with home brew, it is /usr/local/etc/bash_completion.d/
|
|
9
|
+
|
|
10
|
+
_pipeline_router()
|
|
11
|
+
{
|
|
12
|
+
COMPREPLY=($(compgen -W "$$(pyouter --tasks)" "$${COMP_WORDS[1]}"))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
complete -F _pipeline_router pyouter
|
|
16
|
+
|
|
17
|
+
# if you has used pyouter any old edition, maybe just use below source
|
|
18
|
+
_completion_router()
|
|
19
|
+
{
|
|
20
|
+
COMPREPLY=($(compgen -W "$$(python $script --tasks)" "$${COMP_WORDS[1]}"))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
complete -F _completion_router $script
|
|
24
|
+
""")
|
|
25
|
+
script_name = options.script if options.script else "__main__.py"
|
|
26
|
+
content = command.substitute(script=script_name)
|
|
27
|
+
print(content)
|
pyouter/default.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def create_parser(description, tasks="tasks"):
|
|
5
|
+
opt_parser = argparse.ArgumentParser(description=description)
|
|
6
|
+
|
|
7
|
+
opt_parser.add_argument(
|
|
8
|
+
'actions',
|
|
9
|
+
metavar='A',
|
|
10
|
+
type=str,
|
|
11
|
+
nargs='?',
|
|
12
|
+
help='actions strings in tree')
|
|
13
|
+
|
|
14
|
+
opt_parser.add_argument(
|
|
15
|
+
f'--{tasks}',
|
|
16
|
+
const=True,
|
|
17
|
+
dest='tasks',
|
|
18
|
+
nargs='?',
|
|
19
|
+
help='completion the actions strings'
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
opt_parser.add_argument(
|
|
23
|
+
"-s", '--script-name',
|
|
24
|
+
dest="script",
|
|
25
|
+
nargs='?',
|
|
26
|
+
type=str,
|
|
27
|
+
help='completion the actions strings'
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
opt_parser.add_argument(
|
|
31
|
+
"-v", "--view",
|
|
32
|
+
dest="view",
|
|
33
|
+
help="view the router track",
|
|
34
|
+
action="store_true",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
opt_parser.add_argument(
|
|
38
|
+
"-t", "--tree",
|
|
39
|
+
dest="tree",
|
|
40
|
+
help="print all available router paths or inspect router tree structure",
|
|
41
|
+
action="store_true",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
opt_parser.add_argument(
|
|
45
|
+
"-w", "--web",
|
|
46
|
+
dest="web",
|
|
47
|
+
help="start a local web ui to browse and run routers",
|
|
48
|
+
action="store_true",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
opt_parser.add_argument(
|
|
52
|
+
"--host",
|
|
53
|
+
dest="host",
|
|
54
|
+
default="127.0.0.1",
|
|
55
|
+
help="host for the local web ui (default: 127.0.0.1)",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
opt_parser.add_argument(
|
|
59
|
+
"--port",
|
|
60
|
+
dest="port",
|
|
61
|
+
default=8765,
|
|
62
|
+
type=int,
|
|
63
|
+
help="port for the local web ui (default: 8765)",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
opt_parser.add_argument(
|
|
67
|
+
"--no-browser",
|
|
68
|
+
dest="no_browser",
|
|
69
|
+
help="do not open a browser when starting the web ui",
|
|
70
|
+
action="store_true",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return opt_parser
|
pyouter/errors.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class NotFound(Exception):
|
|
2
|
+
def __init__(self, route, token):
|
|
3
|
+
self.route = route
|
|
4
|
+
self.token = token
|
|
5
|
+
self.message = f"token [{token}] not found in {route}"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NotInit(Exception):
|
|
9
|
+
def __init__(self, info):
|
|
10
|
+
self.message = f"instance {info} not init"
|
pyouter/executor.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextvars
|
|
3
|
+
import inspect
|
|
4
|
+
|
|
5
|
+
from concurrent.futures import Executor
|
|
6
|
+
from functools import partial
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LeafExecutor:
|
|
10
|
+
def __init__(self, config, options, executor, leaf):
|
|
11
|
+
self.config = config
|
|
12
|
+
self.options = options
|
|
13
|
+
self.executor = executor
|
|
14
|
+
self.leaf = leaf
|
|
15
|
+
|
|
16
|
+
async def run(self):
|
|
17
|
+
|
|
18
|
+
def is_continufunction(leaf):
|
|
19
|
+
if inspect.isfunction(leaf):
|
|
20
|
+
return inspect.iscoroutinefunction(leaf)
|
|
21
|
+
else:
|
|
22
|
+
return inspect.iscoroutinefunction(leaf.run)
|
|
23
|
+
|
|
24
|
+
async def run_async(leaf, config, options):
|
|
25
|
+
if inspect.isfunction(leaf):
|
|
26
|
+
return await leaf(config, options)
|
|
27
|
+
else:
|
|
28
|
+
return await leaf.run(config, options)
|
|
29
|
+
|
|
30
|
+
def run_sync(leaf, config, options):
|
|
31
|
+
if inspect.isfunction(leaf):
|
|
32
|
+
return leaf(config, options)
|
|
33
|
+
else:
|
|
34
|
+
return leaf.run(config, options)
|
|
35
|
+
|
|
36
|
+
if is_continufunction(self.leaf):
|
|
37
|
+
await run_async(self.leaf, self.config, self.options)
|
|
38
|
+
else:
|
|
39
|
+
loop = asyncio.get_running_loop()
|
|
40
|
+
# Keep the caller's contextvars in the worker thread so a web run
|
|
41
|
+
# can attribute prints from synchronous actions to the right node.
|
|
42
|
+
ctx = contextvars.copy_context()
|
|
43
|
+
await loop.run_in_executor(
|
|
44
|
+
self.executor,
|
|
45
|
+
partial(ctx.run, partial(run_sync, self.leaf, self.config, self.options)),
|
|
46
|
+
)
|
pyouter/fish/__init__.py
ADDED
|
File without changes
|
pyouter/fish/install.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import string
|
|
2
|
+
import os
|
|
3
|
+
import os.path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def install(config, options):
|
|
7
|
+
base = """!/usr/bin/env fish
|
|
8
|
+
complete -c pyouter -f
|
|
9
|
+
complete -c pyouter -a "(python pyouter --tasks)"
|
|
10
|
+
"""
|
|
11
|
+
command = string.Template("""
|
|
12
|
+
complete -c $script -f
|
|
13
|
+
complete -c $script -a "(python $script --tasks)"
|
|
14
|
+
""")
|
|
15
|
+
script_name = options.script if options.script else "main.py"
|
|
16
|
+
home = os.getenv("HOME")
|
|
17
|
+
plugin_dir = os.path.join(home, ".config/fish/completions")
|
|
18
|
+
plugin_path = os.path.join(plugin_dir, script_name + ".fish")
|
|
19
|
+
|
|
20
|
+
if not os.path.exists(plugin_dir):
|
|
21
|
+
os.mkdir(plugin_dir)
|
|
22
|
+
|
|
23
|
+
content = ""
|
|
24
|
+
if os.path.exists(plugin_path):
|
|
25
|
+
with open(plugin_path) as f:
|
|
26
|
+
content = f.read()
|
|
27
|
+
|
|
28
|
+
if not content.startswith(base):
|
|
29
|
+
content = base + content
|
|
30
|
+
|
|
31
|
+
plugin = command.substitute(script=script_name)
|
|
32
|
+
if plugin not in content:
|
|
33
|
+
content += plugin
|
|
34
|
+
|
|
35
|
+
with open(plugin_path, "w+") as f:
|
|
36
|
+
f.write(content)
|
|
37
|
+
|
|
38
|
+
print("success")
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import string
|
|
2
|
+
import os
|
|
3
|
+
import os.path
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def install(config, options):
|
|
8
|
+
base = """#compdef
|
|
9
|
+
|
|
10
|
+
_pyouter() {
|
|
11
|
+
compls=$(pyouter --tasks)
|
|
12
|
+
|
|
13
|
+
completions=(${=compls})
|
|
14
|
+
compadd -- $completions
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
compdef _pyouter pyouter
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
command = string.Template("""
|
|
21
|
+
_pipeline() {
|
|
22
|
+
compls=$$(python $script --tasks)
|
|
23
|
+
|
|
24
|
+
completions=($${=compls})
|
|
25
|
+
compadd -- $$completions
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
compdef _pipeline $script
|
|
29
|
+
|
|
30
|
+
""")
|
|
31
|
+
script_name = options.script if options.script else "main.py"
|
|
32
|
+
home = os.getenv("HOME")
|
|
33
|
+
plugin_name = "pyouter"
|
|
34
|
+
plugin_dir = os.path.join(home, ".oh-my-zsh/plugins", plugin_name)
|
|
35
|
+
plugin_path = os.path.join(plugin_dir, plugin_name + ".plugin.zsh")
|
|
36
|
+
zshrc = os.path.join(home, ".zshrc")
|
|
37
|
+
regex = re.compile(r"\nplugins=\(.*?\)")
|
|
38
|
+
|
|
39
|
+
if not os.path.exists(plugin_dir):
|
|
40
|
+
os.mkdir(plugin_dir)
|
|
41
|
+
|
|
42
|
+
with open(zshrc, "r") as f:
|
|
43
|
+
zshrc_content = f.read()
|
|
44
|
+
|
|
45
|
+
m = regex.search(zshrc_content)
|
|
46
|
+
if m is None:
|
|
47
|
+
zshrc_content = zshrc_content + f"\nplugins=({plugin_name})\n"
|
|
48
|
+
else:
|
|
49
|
+
plugins_setting = m.group(0)
|
|
50
|
+
if plugin_name not in plugins_setting:
|
|
51
|
+
plugins_setting = plugins_setting[:-1] + f" {plugin_name}" + plugins_setting[-1]
|
|
52
|
+
prev, post = regex.split(zshrc_content)
|
|
53
|
+
zshrc_content = prev + plugins_setting + post
|
|
54
|
+
|
|
55
|
+
if os.path.exists(plugin_path):
|
|
56
|
+
with open(plugin_path) as f:
|
|
57
|
+
plugin_content = f.read()
|
|
58
|
+
else:
|
|
59
|
+
plugin_content = ""
|
|
60
|
+
|
|
61
|
+
if base not in plugin_content:
|
|
62
|
+
plugin_content = base + plugin_content
|
|
63
|
+
script = command.substitute(script=script_name)
|
|
64
|
+
if script not in plugin_content:
|
|
65
|
+
plugin_content += script
|
|
66
|
+
|
|
67
|
+
with open(plugin_path, "w+") as f:
|
|
68
|
+
f.write(plugin_content)
|
|
69
|
+
|
|
70
|
+
with open(zshrc, "w+") as f:
|
|
71
|
+
f.write(zshrc_content)
|
|
72
|
+
print("success")
|
pyouter/router.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from .errors import NotFound
|
|
4
|
+
from .executor import LeafExecutor
|
|
5
|
+
|
|
6
|
+
class Router(object):
|
|
7
|
+
def __init__(self, **args):
|
|
8
|
+
self.route = {}
|
|
9
|
+
for key in args:
|
|
10
|
+
self.route[key] = args[key]
|
|
11
|
+
|
|
12
|
+
def context(self, config, options, executor):
|
|
13
|
+
self.config = config
|
|
14
|
+
self.options = options
|
|
15
|
+
self.executor = executor
|
|
16
|
+
for key in self.route:
|
|
17
|
+
router = self.route[key]
|
|
18
|
+
if type(router) == type(self):
|
|
19
|
+
router.context(config, options, executor)
|
|
20
|
+
|
|
21
|
+
async def dispatch(self, pre:str|None, command: str, depth: int):
|
|
22
|
+
show_path = self.options.view or getattr(self.options, 'tree', False)
|
|
23
|
+
path_prefix = ' '*depth if getattr(self.options, 'tree', False) else ''
|
|
24
|
+
run_action = not getattr(self.options, 'tree', False)
|
|
25
|
+
|
|
26
|
+
def full_path(pre, command):
|
|
27
|
+
if pre is not None and pre!='':
|
|
28
|
+
return f'{pre}.{command}'
|
|
29
|
+
else:
|
|
30
|
+
return command
|
|
31
|
+
|
|
32
|
+
if "." in command:
|
|
33
|
+
crt, nxt = command.split('.', 1)
|
|
34
|
+
if crt not in self.route:
|
|
35
|
+
raise NotFound(self.route, crt)
|
|
36
|
+
|
|
37
|
+
router_path = full_path(pre, crt)
|
|
38
|
+
if show_path:
|
|
39
|
+
print(f'[pyouter] {path_prefix}->router: {router_path}')
|
|
40
|
+
|
|
41
|
+
return await self.route[crt].dispatch(router_path, nxt, depth+1)
|
|
42
|
+
else:
|
|
43
|
+
if command not in self.route.keys():
|
|
44
|
+
raise NotFound(self.route, command)
|
|
45
|
+
|
|
46
|
+
leaf = self.route[command]
|
|
47
|
+
if isinstance(leaf, Router):
|
|
48
|
+
# 只在第一次遇到Router时输出路径信息
|
|
49
|
+
router_path = full_path(pre, command)
|
|
50
|
+
if show_path:
|
|
51
|
+
print(f'[pyouter] {path_prefix}->router2: {router_path}')
|
|
52
|
+
|
|
53
|
+
tasks = []
|
|
54
|
+
for crt in leaf.route:
|
|
55
|
+
task = asyncio.create_task(leaf.dispatch(router_path, crt, depth+1))
|
|
56
|
+
tasks.append(task)
|
|
57
|
+
|
|
58
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
59
|
+
else:
|
|
60
|
+
action_path = full_path(pre, command)
|
|
61
|
+
if show_path:
|
|
62
|
+
print(f'[pyouter] {path_prefix}->action: {action_path}')
|
|
63
|
+
|
|
64
|
+
if run_action:
|
|
65
|
+
executor = LeafExecutor(self.config, self.options, self.executor, leaf)
|
|
66
|
+
await executor.run()
|
|
67
|
+
|
|
68
|
+
def tasks(self, base=None):
|
|
69
|
+
|
|
70
|
+
for key in self.route:
|
|
71
|
+
current = f"{base}.{key}" if base else key
|
|
72
|
+
item = self.route[key]
|
|
73
|
+
if type(item) is Router:
|
|
74
|
+
for task in item.tasks(current):
|
|
75
|
+
yield task
|
|
76
|
+
else:
|
|
77
|
+
yield current
|
|
78
|
+
|
|
79
|
+
def print_tree(self, path=None, prefix="", is_last=True):
|
|
80
|
+
"""
|
|
81
|
+
打印树状结构,类似tree命令的输出格式
|
|
82
|
+
path: 要追踪的路径,如 "a.b.c"
|
|
83
|
+
prefix: 当前行的前缀(用于缩进)
|
|
84
|
+
is_last: 是否是当前层级的最后一个节点
|
|
85
|
+
"""
|
|
86
|
+
if path:
|
|
87
|
+
# 解析路径,先追踪到目标节点
|
|
88
|
+
parts = path.split('.')
|
|
89
|
+
current_router = self
|
|
90
|
+
current_path = ""
|
|
91
|
+
|
|
92
|
+
# 追踪路径
|
|
93
|
+
for i, part in enumerate(parts):
|
|
94
|
+
if part not in current_router.route:
|
|
95
|
+
print(f"路径 '{path}' 不存在")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
current_path = f"{current_path}.{part}" if current_path else part
|
|
99
|
+
current_router = current_router.route[part]
|
|
100
|
+
|
|
101
|
+
# 如果不是Router,说明到达叶子节点
|
|
102
|
+
if not isinstance(current_router, Router):
|
|
103
|
+
if i < len(parts) - 1:
|
|
104
|
+
print(f"路径 '{path}' 在 '{current_path}' 处终止(不是Router节点)")
|
|
105
|
+
return
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
# 打印路径追踪
|
|
109
|
+
print(f"路径追踪: {path}")
|
|
110
|
+
print("└── " + path)
|
|
111
|
+
|
|
112
|
+
# 打印目标节点下的完整树
|
|
113
|
+
if isinstance(current_router, Router):
|
|
114
|
+
print(f"\n{current_path} 下的完整树:")
|
|
115
|
+
current_router._print_subtree("", True)
|
|
116
|
+
else:
|
|
117
|
+
print(f"叶子节点: {current_path}")
|
|
118
|
+
else:
|
|
119
|
+
# 打印完整树
|
|
120
|
+
self._print_subtree("", True)
|
|
121
|
+
|
|
122
|
+
def _print_subtree(self, prefix="", is_last=True):
|
|
123
|
+
"""
|
|
124
|
+
打印子树结构
|
|
125
|
+
"""
|
|
126
|
+
items = list(self.route.items())
|
|
127
|
+
for i, (key, value) in enumerate(items):
|
|
128
|
+
is_last_item = i == len(items) - 1
|
|
129
|
+
connector = "└── " if is_last_item else "├── "
|
|
130
|
+
|
|
131
|
+
if isinstance(value, Router):
|
|
132
|
+
print(f"{prefix}{connector}{key}/")
|
|
133
|
+
# 递归打印子树
|
|
134
|
+
next_prefix = prefix + (" " if is_last_item else "│ ")
|
|
135
|
+
value._print_subtree(next_prefix, is_last_item)
|
|
136
|
+
else:
|
|
137
|
+
# 叶子节点
|
|
138
|
+
func_name = value.__name__ if hasattr(value, '__name__') else type(value).__name__
|
|
139
|
+
print(f"{prefix}{connector}{key} ({func_name})")
|