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()
 |