GNU Make variable relationship graph using Python

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;







up vote
4
down vote

favorite
1












This program runs through a preprocessed makefile (make --print-data-base | python make_graph.py [options]) to assemble a directed graph of its variables.
I originally put together the program to visualize assignment redundancies in a particularly messy build, but I haven't used it much since.
I've used it on fairly large builds and haven't experienced any particular performance issues, although I'm sure it can be improved in that regard.



I'd love to know what I can do to improve the program. In particular I'd like to make sure it's somewhat pythonic.
Comments about the overall approach are welcome, but I'm reluctant to delve into GNU Make grammar!



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 := an'
'B := $(A)n')
self.assertEqual(all_assignments(s.splitlines()),
'A' : set(),
'B' : 'A')

def test_deferred(self):
s = ('A = an'
'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 = %sn' % (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()






share|improve this question





















  • Interesting problem; I'm sorry my Python isn't good enough to give you a useful review. Is it the intention that you pipe make --print-data-base into this, so that it sees all of the built-in definitions as well as those in the Makefile? Edit: I should have read the first sentence more thoroughly!
    – Toby Speight
    Feb 5 at 15:58







  • 1




    Micro-review (as a Makefile writer): you might want to exclude lines beginning with t from your regex, so you don't accidentally include shell variable assignments with your Make variables. Secondly, remember that ?= is an assignment operator in Make, so you'll probably want [:?]?= for that part - and the surrounding whitespace is optional.
    – Toby Speight
    Feb 5 at 16:01










  • @TobySpeight That's an unintended consequence, actually, and the reason why I added the --include-internal option (usually you wouldn't be interested in that sort of thing, I figured). The reason I chose to parse the database is that Make then traverse the makefile hierarchy for you (includes/recursive make).
    – Caterpillar
    Feb 5 at 16:05










  • @TobySpeight Thanks! Actually, if I remember correctly, the whitespace surrounding the assignment operator is added by GNU Make when it parses the makefile. So it's strict by design. :)
    – Caterpillar
    Feb 5 at 16:08






  • 1




    Also, ?= is handled and becomes ordinary = (or the assignment is omitted) in the output of --print-data-base, so you don't actually need to handle that. I've just tried it, to confirm.
    – Toby Speight
    Feb 6 at 8:53
















up vote
4
down vote

favorite
1












This program runs through a preprocessed makefile (make --print-data-base | python make_graph.py [options]) to assemble a directed graph of its variables.
I originally put together the program to visualize assignment redundancies in a particularly messy build, but I haven't used it much since.
I've used it on fairly large builds and haven't experienced any particular performance issues, although I'm sure it can be improved in that regard.



I'd love to know what I can do to improve the program. In particular I'd like to make sure it's somewhat pythonic.
Comments about the overall approach are welcome, but I'm reluctant to delve into GNU Make grammar!



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 := an'
'B := $(A)n')
self.assertEqual(all_assignments(s.splitlines()),
'A' : set(),
'B' : 'A')

def test_deferred(self):
s = ('A = an'
'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 = %sn' % (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()






share|improve this question





















  • Interesting problem; I'm sorry my Python isn't good enough to give you a useful review. Is it the intention that you pipe make --print-data-base into this, so that it sees all of the built-in definitions as well as those in the Makefile? Edit: I should have read the first sentence more thoroughly!
    – Toby Speight
    Feb 5 at 15:58







  • 1




    Micro-review (as a Makefile writer): you might want to exclude lines beginning with t from your regex, so you don't accidentally include shell variable assignments with your Make variables. Secondly, remember that ?= is an assignment operator in Make, so you'll probably want [:?]?= for that part - and the surrounding whitespace is optional.
    – Toby Speight
    Feb 5 at 16:01










  • @TobySpeight That's an unintended consequence, actually, and the reason why I added the --include-internal option (usually you wouldn't be interested in that sort of thing, I figured). The reason I chose to parse the database is that Make then traverse the makefile hierarchy for you (includes/recursive make).
    – Caterpillar
    Feb 5 at 16:05










  • @TobySpeight Thanks! Actually, if I remember correctly, the whitespace surrounding the assignment operator is added by GNU Make when it parses the makefile. So it's strict by design. :)
    – Caterpillar
    Feb 5 at 16:08






  • 1




    Also, ?= is handled and becomes ordinary = (or the assignment is omitted) in the output of --print-data-base, so you don't actually need to handle that. I've just tried it, to confirm.
    – Toby Speight
    Feb 6 at 8:53












up vote
4
down vote

favorite
1









up vote
4
down vote

favorite
1






1





This program runs through a preprocessed makefile (make --print-data-base | python make_graph.py [options]) to assemble a directed graph of its variables.
I originally put together the program to visualize assignment redundancies in a particularly messy build, but I haven't used it much since.
I've used it on fairly large builds and haven't experienced any particular performance issues, although I'm sure it can be improved in that regard.



I'd love to know what I can do to improve the program. In particular I'd like to make sure it's somewhat pythonic.
Comments about the overall approach are welcome, but I'm reluctant to delve into GNU Make grammar!



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 := an'
'B := $(A)n')
self.assertEqual(all_assignments(s.splitlines()),
'A' : set(),
'B' : 'A')

def test_deferred(self):
s = ('A = an'
'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 = %sn' % (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()






share|improve this question













This program runs through a preprocessed makefile (make --print-data-base | python make_graph.py [options]) to assemble a directed graph of its variables.
I originally put together the program to visualize assignment redundancies in a particularly messy build, but I haven't used it much since.
I've used it on fairly large builds and haven't experienced any particular performance issues, although I'm sure it can be improved in that regard.



I'd love to know what I can do to improve the program. In particular I'd like to make sure it's somewhat pythonic.
Comments about the overall approach are welcome, but I'm reluctant to delve into GNU Make grammar!



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 := an'
'B := $(A)n')
self.assertEqual(all_assignments(s.splitlines()),
'A' : set(),
'B' : 'A')

def test_deferred(self):
s = ('A = an'
'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 = %sn' % (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()








share|improve this question












share|improve this question




share|improve this question








edited Feb 5 at 16:10
























asked Feb 5 at 14:24









Caterpillar

1284




1284











  • Interesting problem; I'm sorry my Python isn't good enough to give you a useful review. Is it the intention that you pipe make --print-data-base into this, so that it sees all of the built-in definitions as well as those in the Makefile? Edit: I should have read the first sentence more thoroughly!
    – Toby Speight
    Feb 5 at 15:58







  • 1




    Micro-review (as a Makefile writer): you might want to exclude lines beginning with t from your regex, so you don't accidentally include shell variable assignments with your Make variables. Secondly, remember that ?= is an assignment operator in Make, so you'll probably want [:?]?= for that part - and the surrounding whitespace is optional.
    – Toby Speight
    Feb 5 at 16:01










  • @TobySpeight That's an unintended consequence, actually, and the reason why I added the --include-internal option (usually you wouldn't be interested in that sort of thing, I figured). The reason I chose to parse the database is that Make then traverse the makefile hierarchy for you (includes/recursive make).
    – Caterpillar
    Feb 5 at 16:05










  • @TobySpeight Thanks! Actually, if I remember correctly, the whitespace surrounding the assignment operator is added by GNU Make when it parses the makefile. So it's strict by design. :)
    – Caterpillar
    Feb 5 at 16:08






  • 1




    Also, ?= is handled and becomes ordinary = (or the assignment is omitted) in the output of --print-data-base, so you don't actually need to handle that. I've just tried it, to confirm.
    – Toby Speight
    Feb 6 at 8:53
















  • Interesting problem; I'm sorry my Python isn't good enough to give you a useful review. Is it the intention that you pipe make --print-data-base into this, so that it sees all of the built-in definitions as well as those in the Makefile? Edit: I should have read the first sentence more thoroughly!
    – Toby Speight
    Feb 5 at 15:58







  • 1




    Micro-review (as a Makefile writer): you might want to exclude lines beginning with t from your regex, so you don't accidentally include shell variable assignments with your Make variables. Secondly, remember that ?= is an assignment operator in Make, so you'll probably want [:?]?= for that part - and the surrounding whitespace is optional.
    – Toby Speight
    Feb 5 at 16:01










  • @TobySpeight That's an unintended consequence, actually, and the reason why I added the --include-internal option (usually you wouldn't be interested in that sort of thing, I figured). The reason I chose to parse the database is that Make then traverse the makefile hierarchy for you (includes/recursive make).
    – Caterpillar
    Feb 5 at 16:05










  • @TobySpeight Thanks! Actually, if I remember correctly, the whitespace surrounding the assignment operator is added by GNU Make when it parses the makefile. So it's strict by design. :)
    – Caterpillar
    Feb 5 at 16:08






  • 1




    Also, ?= is handled and becomes ordinary = (or the assignment is omitted) in the output of --print-data-base, so you don't actually need to handle that. I've just tried it, to confirm.
    – Toby Speight
    Feb 6 at 8:53















Interesting problem; I'm sorry my Python isn't good enough to give you a useful review. Is it the intention that you pipe make --print-data-base into this, so that it sees all of the built-in definitions as well as those in the Makefile? Edit: I should have read the first sentence more thoroughly!
– Toby Speight
Feb 5 at 15:58





Interesting problem; I'm sorry my Python isn't good enough to give you a useful review. Is it the intention that you pipe make --print-data-base into this, so that it sees all of the built-in definitions as well as those in the Makefile? Edit: I should have read the first sentence more thoroughly!
– Toby Speight
Feb 5 at 15:58





1




1




Micro-review (as a Makefile writer): you might want to exclude lines beginning with t from your regex, so you don't accidentally include shell variable assignments with your Make variables. Secondly, remember that ?= is an assignment operator in Make, so you'll probably want [:?]?= for that part - and the surrounding whitespace is optional.
– Toby Speight
Feb 5 at 16:01




Micro-review (as a Makefile writer): you might want to exclude lines beginning with t from your regex, so you don't accidentally include shell variable assignments with your Make variables. Secondly, remember that ?= is an assignment operator in Make, so you'll probably want [:?]?= for that part - and the surrounding whitespace is optional.
– Toby Speight
Feb 5 at 16:01












@TobySpeight That's an unintended consequence, actually, and the reason why I added the --include-internal option (usually you wouldn't be interested in that sort of thing, I figured). The reason I chose to parse the database is that Make then traverse the makefile hierarchy for you (includes/recursive make).
– Caterpillar
Feb 5 at 16:05




@TobySpeight That's an unintended consequence, actually, and the reason why I added the --include-internal option (usually you wouldn't be interested in that sort of thing, I figured). The reason I chose to parse the database is that Make then traverse the makefile hierarchy for you (includes/recursive make).
– Caterpillar
Feb 5 at 16:05












@TobySpeight Thanks! Actually, if I remember correctly, the whitespace surrounding the assignment operator is added by GNU Make when it parses the makefile. So it's strict by design. :)
– Caterpillar
Feb 5 at 16:08




@TobySpeight Thanks! Actually, if I remember correctly, the whitespace surrounding the assignment operator is added by GNU Make when it parses the makefile. So it's strict by design. :)
– Caterpillar
Feb 5 at 16:08




1




1




Also, ?= is handled and becomes ordinary = (or the assignment is omitted) in the output of --print-data-base, so you don't actually need to handle that. I've just tried it, to confirm.
– Toby Speight
Feb 6 at 8:53




Also, ?= is handled and becomes ordinary = (or the assignment is omitted) in the output of --print-data-base, so you don't actually need to handle that. I've just tried it, to confirm.
– Toby Speight
Feb 6 at 8:53










1 Answer
1






active

oldest

votes

















up vote
3
down vote



accepted










I’m only going to comment on the argparse part.



For starter, for function calls with long names and rather long value (esp. the help one), I mostly prefer the second indentation proposed by PEP8; but it's mostly a matter of taste. Speaking about PEP8, it also state that you should remove spaces around the = sign for named arguments.



Second, I like to make the parser building logic into its own function. Who knows one of your future project may want to import this one and extend its parser. (It actually hapened to me once.)



You also don't need the dest = 'graph_name' as it is already the default name assigned to (look at how --include-internal is stored into include_internal); I’d rather use metavar='NAME' here instead. And speaking about storage of values, I don't see any added value to using vars on the Namespace returned by parser.parse_args() as accessing the attributes directly would work the same.



I would also drop the __file__ from the ArgumentParser call as it is already pretty much what argparse is doing by default, but add a description to it. It is usually good practice to use the module docstring for that, so better add one too.



Finally your handling of sys.stdin compared to a path to a file seems off. Instead of checking it manually after parsing the arguments, you could pass default=sys.stdin to the database argument. And since you are dealing with files, rather than closing it after the fact, you should use a with statement. In the following code I won't make any special case for sys.stdin as closing it does not affect the rest of your program (which is empty).



Proposed improvements:



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





share|improve this answer





















  • Much appreciated - thanks. In particular I appreciate the stdin refactoring.
    – Caterpillar
    Feb 5 at 15:41










Your Answer




StackExchange.ifUsing("editor", function ()
return StackExchange.using("mathjaxEditing", function ()
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix)
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
);
);
, "mathjax-editing");

StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");

StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "196"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);

else
createEditor();

);

function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
convertImagesToLinks: false,
noModals: false,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);



);








 

draft saved


draft discarded


















StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f186830%2fgnu-make-variable-relationship-graph-using-python%23new-answer', 'question_page');

);

Post as a guest






























1 Answer
1






active

oldest

votes








1 Answer
1






active

oldest

votes









active

oldest

votes






active

oldest

votes








up vote
3
down vote



accepted










I’m only going to comment on the argparse part.



For starter, for function calls with long names and rather long value (esp. the help one), I mostly prefer the second indentation proposed by PEP8; but it's mostly a matter of taste. Speaking about PEP8, it also state that you should remove spaces around the = sign for named arguments.



Second, I like to make the parser building logic into its own function. Who knows one of your future project may want to import this one and extend its parser. (It actually hapened to me once.)



You also don't need the dest = 'graph_name' as it is already the default name assigned to (look at how --include-internal is stored into include_internal); I’d rather use metavar='NAME' here instead. And speaking about storage of values, I don't see any added value to using vars on the Namespace returned by parser.parse_args() as accessing the attributes directly would work the same.



I would also drop the __file__ from the ArgumentParser call as it is already pretty much what argparse is doing by default, but add a description to it. It is usually good practice to use the module docstring for that, so better add one too.



Finally your handling of sys.stdin compared to a path to a file seems off. Instead of checking it manually after parsing the arguments, you could pass default=sys.stdin to the database argument. And since you are dealing with files, rather than closing it after the fact, you should use a with statement. In the following code I won't make any special case for sys.stdin as closing it does not affect the rest of your program (which is empty).



Proposed improvements:



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





share|improve this answer





















  • Much appreciated - thanks. In particular I appreciate the stdin refactoring.
    – Caterpillar
    Feb 5 at 15:41














up vote
3
down vote



accepted










I’m only going to comment on the argparse part.



For starter, for function calls with long names and rather long value (esp. the help one), I mostly prefer the second indentation proposed by PEP8; but it's mostly a matter of taste. Speaking about PEP8, it also state that you should remove spaces around the = sign for named arguments.



Second, I like to make the parser building logic into its own function. Who knows one of your future project may want to import this one and extend its parser. (It actually hapened to me once.)



You also don't need the dest = 'graph_name' as it is already the default name assigned to (look at how --include-internal is stored into include_internal); I’d rather use metavar='NAME' here instead. And speaking about storage of values, I don't see any added value to using vars on the Namespace returned by parser.parse_args() as accessing the attributes directly would work the same.



I would also drop the __file__ from the ArgumentParser call as it is already pretty much what argparse is doing by default, but add a description to it. It is usually good practice to use the module docstring for that, so better add one too.



Finally your handling of sys.stdin compared to a path to a file seems off. Instead of checking it manually after parsing the arguments, you could pass default=sys.stdin to the database argument. And since you are dealing with files, rather than closing it after the fact, you should use a with statement. In the following code I won't make any special case for sys.stdin as closing it does not affect the rest of your program (which is empty).



Proposed improvements:



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





share|improve this answer





















  • Much appreciated - thanks. In particular I appreciate the stdin refactoring.
    – Caterpillar
    Feb 5 at 15:41












up vote
3
down vote



accepted







up vote
3
down vote



accepted






I’m only going to comment on the argparse part.



For starter, for function calls with long names and rather long value (esp. the help one), I mostly prefer the second indentation proposed by PEP8; but it's mostly a matter of taste. Speaking about PEP8, it also state that you should remove spaces around the = sign for named arguments.



Second, I like to make the parser building logic into its own function. Who knows one of your future project may want to import this one and extend its parser. (It actually hapened to me once.)



You also don't need the dest = 'graph_name' as it is already the default name assigned to (look at how --include-internal is stored into include_internal); I’d rather use metavar='NAME' here instead. And speaking about storage of values, I don't see any added value to using vars on the Namespace returned by parser.parse_args() as accessing the attributes directly would work the same.



I would also drop the __file__ from the ArgumentParser call as it is already pretty much what argparse is doing by default, but add a description to it. It is usually good practice to use the module docstring for that, so better add one too.



Finally your handling of sys.stdin compared to a path to a file seems off. Instead of checking it manually after parsing the arguments, you could pass default=sys.stdin to the database argument. And since you are dealing with files, rather than closing it after the fact, you should use a with statement. In the following code I won't make any special case for sys.stdin as closing it does not affect the rest of your program (which is empty).



Proposed improvements:



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





share|improve this answer













I’m only going to comment on the argparse part.



For starter, for function calls with long names and rather long value (esp. the help one), I mostly prefer the second indentation proposed by PEP8; but it's mostly a matter of taste. Speaking about PEP8, it also state that you should remove spaces around the = sign for named arguments.



Second, I like to make the parser building logic into its own function. Who knows one of your future project may want to import this one and extend its parser. (It actually hapened to me once.)



You also don't need the dest = 'graph_name' as it is already the default name assigned to (look at how --include-internal is stored into include_internal); I’d rather use metavar='NAME' here instead. And speaking about storage of values, I don't see any added value to using vars on the Namespace returned by parser.parse_args() as accessing the attributes directly would work the same.



I would also drop the __file__ from the ArgumentParser call as it is already pretty much what argparse is doing by default, but add a description to it. It is usually good practice to use the module docstring for that, so better add one too.



Finally your handling of sys.stdin compared to a path to a file seems off. Instead of checking it manually after parsing the arguments, you could pass default=sys.stdin to the database argument. And since you are dealing with files, rather than closing it after the fact, you should use a with statement. In the following code I won't make any special case for sys.stdin as closing it does not affect the rest of your program (which is empty).



Proposed improvements:



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






share|improve this answer













share|improve this answer



share|improve this answer











answered Feb 5 at 15:16









Mathias Ettinger

21.9k32876




21.9k32876











  • Much appreciated - thanks. In particular I appreciate the stdin refactoring.
    – Caterpillar
    Feb 5 at 15:41
















  • Much appreciated - thanks. In particular I appreciate the stdin refactoring.
    – Caterpillar
    Feb 5 at 15:41















Much appreciated - thanks. In particular I appreciate the stdin refactoring.
– Caterpillar
Feb 5 at 15:41




Much appreciated - thanks. In particular I appreciate the stdin refactoring.
– Caterpillar
Feb 5 at 15:41












 

draft saved


draft discarded


























 


draft saved


draft discarded














StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f186830%2fgnu-make-variable-relationship-graph-using-python%23new-answer', 'question_page');

);

Post as a guest













































































Popular posts from this blog

Chat program with C++ and SFML

Function to Return a JSON Like Objects Using VBA Collections and Arrays

Will my employers contract hold up in court?