mirror of
https://github.com/KevinMidboe/python-gpiozero.git
synced 2025-10-29 17:50:37 +00:00
Heavily based on @lurch's efforts in #469 but with some additional filtering capabilities.
174 lines
5.6 KiB
Python
Executable File
174 lines
5.6 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
import re
|
|
import sys
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
|
|
ABSTRACT_CLASSES = {
|
|
'Device',
|
|
'GPIODevice',
|
|
'SmoothedInputDevice',
|
|
'AnalogInputDevice',
|
|
'MCP3xxx',
|
|
'MCP33xx',
|
|
'CompositeDevice',
|
|
'CompositeOutputDevice',
|
|
'LEDCollection',
|
|
'InternalDevice',
|
|
}
|
|
|
|
OMIT_CLASSES = {
|
|
'object',
|
|
'GPIOBase',
|
|
'GPIOMeta',
|
|
'frozendict',
|
|
'WeakMethod',
|
|
'_EnergenieMaster',
|
|
}
|
|
|
|
|
|
def main(args=None):
|
|
"""
|
|
A simple application for generating GPIO Zero's charts. Specify the root
|
|
class to generate with -i (multiple roots can be specified). Specify parts
|
|
of the hierarchy to exclude with -x. Output is in a format suitable for
|
|
feeding to graphviz's dot application.
|
|
"""
|
|
if args is None:
|
|
args = sys.argv[1:]
|
|
my_path = Path(__file__).parent
|
|
# XXX make this relative to repo root rather than this script
|
|
default_path = my_path / '..' / '..' / 'gpiozero'
|
|
default_path = default_path.resolve()
|
|
parser = argparse.ArgumentParser(description=main.__doc__)
|
|
parser.add_argument('-p', '--path', action='append', metavar='PATH',
|
|
default=[], help=
|
|
"search under PATH for Python source files; can be "
|
|
"specified multiple times, defaults to %s" % default_path)
|
|
parser.add_argument('-i', '--include', action='append', metavar='BASE',
|
|
default=[], help=
|
|
"only include classes which have BASE somewhere in "
|
|
"their ancestry; can be specified multiple times")
|
|
parser.add_argument('-x', '--exclude', action='append', metavar='BASE',
|
|
default=[], help=
|
|
"exclude any classes which have BASE somewhere in "
|
|
"their ancestry; can be specified multiple times")
|
|
parser.add_argument('output', nargs='?', type=argparse.FileType('w'),
|
|
default=sys.stdout, help=
|
|
"the file to write the output to; defaults to stdout")
|
|
args = parser.parse_args(args)
|
|
if not args.path:
|
|
args.path = [str(default_path)]
|
|
|
|
m = make_class_map(args.path, OMIT_CLASSES)
|
|
if args.include or args.exclude:
|
|
m = filter_map(m, include_roots=set(args.include), exclude_roots=set(args.exclude))
|
|
args.output.write(render_map(m, ABSTRACT_CLASSES))
|
|
|
|
|
|
def make_class_map(search_paths, omit):
|
|
"""
|
|
Find all Python source files under *search_paths*, extract (via a crude
|
|
regex) all class definitions and return a mapping of class-name to the list
|
|
of base classes.
|
|
|
|
All classes listed in *omit* will be excluded from the result, but not
|
|
their descendents (useful for excluding "object" etc.)
|
|
"""
|
|
def find_classes():
|
|
class_re = re.compile(r'^class (?P<name>\w+)(?:\((?P<bases>.*)\))?:', re.MULTILINE)
|
|
for path in search_paths:
|
|
for py_file in Path(path).rglob('*.py'):
|
|
with py_file.open() as f:
|
|
for match in class_re.finditer(f.read()):
|
|
if match.group('name') not in omit:
|
|
yield match.group('name'), [
|
|
base.strip()
|
|
for base in (match.group('bases') or '').split(',')
|
|
if base.strip() not in omit
|
|
]
|
|
return {
|
|
name: bases
|
|
for name, bases in find_classes()
|
|
}
|
|
|
|
|
|
def filter_map(class_map, include_roots, exclude_roots):
|
|
"""
|
|
Returns *class_map* (which is a mapping such as that returned by
|
|
:func:`make_class_map`), with only those classes which have at least one
|
|
of the *include_roots* in their ancestry, and none of the *exclude_roots*.
|
|
"""
|
|
def has_parent(cls, parent):
|
|
return cls == parent or any(
|
|
has_parent(base, parent) for base in class_map.get(cls, ()))
|
|
|
|
return {
|
|
name: bases
|
|
for name, bases in class_map.items()
|
|
if (not include_roots or any(has_parent(name, root) for root in include_roots))
|
|
and not any(has_parent(name, root) for root in exclude_roots)
|
|
}
|
|
|
|
|
|
def render_map(class_map, abstract):
|
|
"""
|
|
Renders *class_map* (which is a mapping such as that returned by
|
|
:func:`make_class_map`) to graphviz's dot language.
|
|
|
|
The *abstract* sequence determines which classes will be rendered lighter
|
|
to indicate their abstract nature. All classes with names ending "Mixin"
|
|
will be implicitly rendered in a different style.
|
|
"""
|
|
def all_names(class_map):
|
|
for name, bases in class_map.items():
|
|
yield name
|
|
for base in bases:
|
|
yield base
|
|
|
|
template = """\
|
|
digraph classes {{
|
|
graph [rankdir=RL];
|
|
node [shape=rect, style=filled, fontname=Sans, fontsize=10];
|
|
edge [];
|
|
|
|
/* Mixin classes */
|
|
node [color="#c69ee0", fontcolor="#000000"]
|
|
|
|
{mixin_nodes}
|
|
|
|
/* Abstract classes */
|
|
node [color="#9ec6e0", fontcolor="#000000"]
|
|
|
|
{abstract_nodes}
|
|
|
|
/* Concrete classes */
|
|
node [color="#2980b9", fontcolor="#ffffff"];
|
|
|
|
{edges}
|
|
}}
|
|
"""
|
|
|
|
return template.format(
|
|
mixin_nodes='\n '.join(
|
|
'{name};'.format(name=name)
|
|
for name in set(all_names(class_map))
|
|
if name.endswith('Mixin')
|
|
),
|
|
abstract_nodes='\n '.join(
|
|
'{name};'.format(name=name)
|
|
for name in abstract & set(all_names(class_map))
|
|
),
|
|
edges='\n '.join(
|
|
'{name}->{base};'.format(name=name, base=base)
|
|
for name, bases in class_map.items()
|
|
for base in bases
|
|
),
|
|
)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|