GNU, делают график переменной отношения, используя Python


Эта программа работает через препроцессированный файл Makefile (make --print-data-base | python make_graph.py [options]) собрать ориентированный граф из своих переменных. Я изначально собрал программа для визуализации избыточности присвоение в особо неопрятно строить, но я не использовал его много, так. Я использовал его на довольно большие строит и не испытывали какой-либо конкретной проблемы с производительностью, хотя я уверен, что можно улучшить в этом отношении.

Я хотел бы знать, что я могу сделать, чтобы улучшить программу. В частности, я хотел бы убедиться, что это-то весть. Комментарии о общий подход приветствуются, но я не очень хочу вникать в GNU сделать грамматику!

import argparse
import graphviz
import re
import subprocess
import sys
import unittest

def all_assignments(database):
    assignments = {}

    # accept target-specific variables
    re_assignment = re.compile(r'.*?([^:#= ]+) :?= .*$')
    re_variable = re.compile(r'\$\(([^:#= ]+?)\)')
    for line in database:
        if not any(assign in line for assign in (' = ', ' := ')):
            continue

        match_assignee = re_assignment.match(line)
        if not match_assignee:
            continue

        assignee = match_assignee.group(1)
        assignments.setdefault(assignee, set())
        for match_variable in re.finditer(re_variable, line):
            assignments[assignee].add(match_variable.group(1))

    return assignments

def without_edges(assignments):
   # not assigned other variables
    singles = {assignee for (assignee, variables) in
               assignments.iteritems() if len(variables) == 0}
    # and not assigned to another variables
    for (_, variables) in assignments.iteritems():
        singles.difference_update(variables)

    return singles

def trim(assignments, vars_to_trim):
    for var in vars_to_trim:
        assignments.pop(var, None)
    return assignments

# Alternatively, can be acquired using make --print-data-base -f /dev/null
echo_internal = """
echo:
    @echo $(subst <,\<,$(.VARIABLES))
"""  # on my system, <D is the first variable

def internal_variables():
    variables = subprocess.check_output(['make', '--eval', echo_internal])
    return set(variables.split())

def graph_assignments(assignments, include_internal):
    qualifying_assignments = trim(assignments,
                                  set(without_edges(assignments)))
    return (qualifying_assignments if include_internal else
            trim(qualifying_assignments, internal_variables()))

def nodes(assignments):
    nodes = {assignee for (assignee, _) in assignments.iteritems()}
    for (_, variables) in assignments.iteritems():
        nodes.update(variables)
    return nodes

class TestAssignments(unittest.TestCase):
    # This particular edge wouldn't appear from --print-data-base
    # output, since GNU Make would expand the variable immediately
    def test_immediate(self):
        s = ('A := a\n'
             'B := $(A)\n')
        self.assertEqual(all_assignments(s.splitlines()),
                         {'A' : set(),
                          'B' : {'A'}})

    def test_deferred(self):
        s = ('A = a\n'
             'B = $(A)\n')
        self.assertEqual(all_assignments(s.splitlines()),
                         {'A' : set(),
                          'B' : {'A'}})

    def test_empty(self):
        self.assertEqual(all_assignments('B = $(A)\n'.splitlines()),
                         {'B' : {'A'}})

    def test_multiple(self):
        self.assertEqual(all_assignments('A = $(B)$(C) $(D)\n'.splitlines()),
                         {'A' : {'B', 'C', 'D'}})

    def test_without_edges(self):
        self.assertEqual(without_edges({'A' : set(),
                                        'B' : {'A'},
                                        'C' : set()}), {'C'})

    def test_nodes(self):
        self.assertEqual(nodes({'A' : set(),
                                'B' : {'A'},
                                'C' : set()}), {'A', 'B', 'C'})

def add_nodes(dot, nodes):
    for node in nodes:
        dot.node(node)

def add_edges(dot, assignments):
    for (assignee, variables) in assignments.iteritems():
        for variable in variables:
            dot.edge(assignee, variable)

def output_graph(assignments, graph_name, view):
    dot = graphviz.Digraph(comment = 'GNU Make Variable Directional Graph')

    add_nodes(dot, nodes(assignments))
    add_edges(dot, assignments)

    dot.render(graph_name, view = view)

def output_text(assignments):
    for (assignee, variables) in sorted(assignments.iteritems()):
        sys.stdout.write('%s = %s\n' % (assignee, ' '.join(sorted(variables))))

def make_graph(database, graph_name, as_text, include_internal, view):
    assignments = graph_assignments(all_assignments(database), include_internal)
    if as_text:
        output_text(assignments)
    else:
        output_graph(assignments, graph_name, view)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(__file__)
    parser.add_argument('--database', type = argparse.FileType('r'),
                        help = ("GNU Make database filename; if no filename is"
                                " provided the database is expected on the"
                                " standard input stream"))
    parser.add_argument('--graph-name', default = 'graph', dest = 'graph_name',
                        help = ("Graph name; defaults to 'graph'"))
    parser.add_argument('--include-internal', action = 'store_true',
                        help = "Include internal and implicit variables")
    parser.add_argument('--list', dest = 'as_text', action = 'store_true',
                        help = "Output as text to the standard output stream")
    parser.add_argument('--no-view', dest = 'view', action = 'store_false',
                        help = "Don't open the assembled graph")

    args = vars(parser.parse_args())
    database = args['database'] if args['database'] else sys.stdin
    make_graph(database,args['graph_name'], args['as_text'],
               args['include_internal'], args['view'])

    if database != sys.stdin:
        database.close()


144
4
задан 5 февраля 2018 в 02:02 Источник Поделиться
Комментарии
1 ответ

Я только собираюсь комментировать argparse часть.

Для стартера, для вызова функции с длинными именами и достаточно длинное значение (ЭСП. в help один), я в основном предпочитаю второй отступ, предложенный PEP8, но это в основном дело вкуса. Говоря о PEP8, это также государство, что вы должны удалить пробелы вокруг = знак для именованных аргументов.

Во-вторых, я хотел сделать здание анализатор логики в собственную функцию. Кто знает, один из ваших будущих проектов может хотите импортировать этот и расширить свой парсер. (Это на самом деле произошло со мной однажды.)

Вы также не нужны dest = 'graph_name' а это уже по умолчанию название (посмотрите как --include-internal хранится в include_internal); Я предпочел бы использовать metavar='NAME' здесь вместо этого. И говоря о хранении ценностей, я не вижу никакой добавленной стоимости с использованием vars на Namespace возвращаемых parser.parse_args() как обращение к атрибутам напрямую будет работать так же.

Я бы также бросить __file__ от ArgumentParser называю так, как оно уже довольно много, что argparse делает по умолчанию, но добавить description к нему. Это обычно хорошая практика, чтобы использовать модуль строкой документации для этого, так что лучше добавить тоже.

Наконец-то ваша работа sys.stdin по сравнению с путь к файлу, кажется, выкл. Вместо того, чтобы проверить его вручную после разбора аргументов, вы могли бы пройти default=sys.stdin в базе данных аргумента. А поскольку вы имеете дело с файлами, а не закрывать его после того, вы должны использовать with заявление. В следующем коде я не буду делать любой особый случай sys.stdin как закрытие это не влияет на остальные программы (пустой).

Предлагаемые улучшения:

"""GNU Make variable relationship graph.

This program runs through a preprocessed makefile
(make --print-data-base | python make_graph.py [options])
to assemble a directed graph of its variables.
"""

import sys
import argparse

...

def command_line_parser():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
'--database', type=argparse.FileType('r'), default=sys.stdin
help="GNU Make database filename; if no filename is"
" provided the database is expected on the"
" standard input stream")
parser.add_argument(
'--graph-name', default='graph', metavar='NAME',
help="Graph name; defaults to 'graph'")
parser.add_argument(
'--include-internal', action='store_true',
help="Include internal and implicit variables")
parser.add_argument(
'--list', dest='as_text', action='store_true',
help="Output as text to the standard output stream")
parser.add_argument(
'--no-view', dest='view', action='store_false',
help="Don't open the assembled graph")
return parser

if __name__ == '__main__':
args = command_line_parser().parse_args()
with args.database as database:
make_graph(database, args.graph_name, args.as_text,
args.include_internal, args.view)

3
ответ дан 5 февраля 2018 в 03:02 Источник Поделиться