Skip to content

Commit

Permalink
Fix various problems with --import-graph filename parsing (#4259)
Browse files Browse the repository at this point in the history
Avoids backwards-incompatible changes.
 - Raise error if graphvis is not installed (instead of reporting success)
 - Fix tempfile creation bug when outputfile includes directories
 - Fix bug when using file extension that isn't 3 characters long
 - Fix confusing help text
 - Rename deprecated .dot extension to .gv
 - Default to .png if no extension is specified

* Add typing to modified functions (and ignore mypy thinking codecs.open()
returns an int)

Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
  • Loading branch information
howeaj and Pierre-Sassoulas committed Mar 29, 2021
1 parent 42bf3b0 commit a37c643
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 34 deletions.
2 changes: 2 additions & 0 deletions CONTRIBUTORS.txt
Expand Up @@ -462,3 +462,5 @@ contributors:
* Mark Byrne: contributor

* Konstantina Saketou: contributor

* Andrew Howe: contributor
2 changes: 2 additions & 0 deletions ChangeLog
Expand Up @@ -80,6 +80,8 @@ Release date: TBA

* Improve check if class is subscriptable PEP585

* Fix documentation and filename handling of --import-graph

* Fix false-positive for ``unused-import`` on class keyword arguments

Closes #3202
Expand Down
35 changes: 19 additions & 16 deletions pylint/checkers/imports.py
Expand Up @@ -44,6 +44,7 @@
import os
import sys
from distutils import sysconfig
from typing import Dict, List

import astroid

Expand All @@ -56,7 +57,7 @@
from pylint.exceptions import EmptyReportError
from pylint.graph import DotBackend, get_cycles
from pylint.interfaces import IAstroidChecker
from pylint.reporters.ureports.nodes import Paragraph, VerbatimText
from pylint.reporters.ureports.nodes import Paragraph, VerbatimText, VNode
from pylint.utils import IsortDriver, get_global_option


Expand Down Expand Up @@ -172,10 +173,10 @@ def _repr_tree_defs(data, indent_str=None):
return "\n".join(lines)


def _dependencies_graph(filename, dep_info):
def _dependencies_graph(filename: str, dep_info: Dict[str, List[str]]) -> str:
"""write dependencies as a dot (graphviz) file"""
done = {}
printer = DotBackend(filename[:-4], rankdir="LR")
printer = DotBackend(os.path.splitext(os.path.basename(filename))[0], rankdir="LR")
printer.emit('URL="." node[shape="box"]')
for modname, dependencies in sorted(dep_info.items()):
done[modname] = 1
Expand All @@ -187,15 +188,15 @@ def _dependencies_graph(filename, dep_info):
for depmodname, dependencies in sorted(dep_info.items()):
for modname in dependencies:
printer.emit_edge(modname, depmodname)
printer.generate(filename)
return printer.generate(filename)


def _make_graph(filename, dep_info, sect, gtype):
def _make_graph(filename: str, dep_info: Dict[str, List[str]], sect: VNode, gtype: str):
"""generate a dependencies graph and add some information about it in the
report's section
"""
_dependencies_graph(filename, dep_info)
sect.append(Paragraph(f"{gtype}imports graph has been written to {filename}"))
outputfile = _dependencies_graph(filename, dep_info)
sect.append(Paragraph(f"{gtype}imports graph has been written to {outputfile}"))


# the import checker itself ###################################################
Expand Down Expand Up @@ -332,9 +333,9 @@ class ImportsChecker(DeprecatedMixin, BaseChecker):
{
"default": "",
"type": "string",
"metavar": "<file.dot>",
"help": "Create a graph of every (i.e. internal and"
" external) dependencies in the given file"
"metavar": "<file.gv>",
"help": "Output a graph (.gv or any supported image format) of"
" all (i.e. internal and external) dependencies to the given file"
" (report RP0402 must not be disabled).",
},
),
Expand All @@ -343,19 +344,21 @@ class ImportsChecker(DeprecatedMixin, BaseChecker):
{
"default": "",
"type": "string",
"metavar": "<file.dot>",
"help": "Create a graph of external dependencies in the"
" given file (report RP0402 must not be disabled).",
"metavar": "<file.gv>",
"help": "Output a graph (.gv or any supported image format)"
" of external dependencies to the given file"
" (report RP0402 must not be disabled).",
},
),
(
"int-import-graph",
{
"default": "",
"type": "string",
"metavar": "<file.dot>",
"help": "Create a graph of internal dependencies in the"
" given file (report RP0402 must not be disabled).",
"metavar": "<file.gv>",
"help": "Output a graph (.gv or any supported image format)"
" of internal dependencies to the given file"
" (report RP0402 must not be disabled).",
},
),
(
Expand Down
42 changes: 27 additions & 15 deletions pylint/graph.py
Expand Up @@ -18,6 +18,7 @@
"""
import codecs
import os
import shutil
import subprocess
import sys
import tempfile
Expand All @@ -28,7 +29,7 @@ def target_info_from_filename(filename):
"""Transforms /some/path/foo.png into ('/some/path', 'foo.png', 'png')."""
basename = osp.basename(filename)
storedir = osp.dirname(osp.abspath(filename))
target = filename.split(".")[-1]
target = osp.splitext(filename)[-1][1:]
return storedir, basename, target


Expand Down Expand Up @@ -76,33 +77,44 @@ def get_source(self):

source = property(get_source)

def generate(self, outputfile=None, mapfile=None):
def generate(self, outputfile: str = None, mapfile: str = None) -> str:
"""Generates a graph file.
:param str outputfile: filename and path [defaults to graphname.png]
:param str mapfile: filename and path
:rtype: str
:return: a path to the generated file
:raises RuntimeError: if the executable for rendering was not found
"""
graphviz_extensions = ("dot", "gv")
name = self.graphname
if outputfile is not None:
_, _, target = target_info_from_filename(outputfile)
if target != "dot":
pdot, dot_sourcepath = tempfile.mkstemp(".dot", name)
os.close(pdot)
else:
dot_sourcepath = outputfile
else:
if outputfile is None:
target = "png"
pdot, dot_sourcepath = tempfile.mkstemp(".dot", name)
pdot, dot_sourcepath = tempfile.mkstemp(".gv", name)
ppng, outputfile = tempfile.mkstemp(".png", name)
os.close(pdot)
os.close(ppng)
pdot = codecs.open(dot_sourcepath, "w", encoding="utf8")
pdot.write(self.source)
pdot.close()
if target != "dot":
else:
_, _, target = target_info_from_filename(outputfile)
if not target:
target = "png"
outputfile = outputfile + "." + target
if target not in graphviz_extensions:
pdot, dot_sourcepath = tempfile.mkstemp(".gv", name)
os.close(pdot)
else:
dot_sourcepath = outputfile
pdot = codecs.open(dot_sourcepath, "w", encoding="utf8") # type: ignore
pdot.write(self.source) # type: ignore
pdot.close() # type: ignore
if target not in graphviz_extensions:
if shutil.which(self.renderer) is None:
raise RuntimeError(
f"Cannot generate `{outputfile}` because '{self.renderer}' "
"executable not found. Install graphviz, or specify a `.gv` "
"outputfile to produce the DOT source code."
)
use_shell = sys.platform == "win32"
if mapfile:
subprocess.call(
Expand Down
18 changes: 15 additions & 3 deletions tests/test_import_graph.py
Expand Up @@ -26,8 +26,8 @@


@pytest.fixture
def dest():
dest = "dependencies_graph.dot"
def dest(request):
dest = request.param
yield dest
try:
os.remove(dest)
Expand All @@ -36,13 +36,18 @@ def dest():
pass


POSSIBLE_DOT_FILENAMES = ["foo.dot", "foo.gv", "tests/regrtest_data/foo.dot"]


@pytest.mark.parametrize("dest", POSSIBLE_DOT_FILENAMES, indirect=True)
def test_dependencies_graph(dest):
"""DOC files are correctly generated, and the graphname is the basename"""
imports._dependencies_graph(dest, {"labas": ["hoho", "yep"], "hoho": ["yep"]})
with open(dest) as stream:
assert (
stream.read().strip()
== """
digraph "dependencies_graph" {
digraph "foo" {
rankdir=LR
charset="utf-8"
URL="." node[shape="box"]
Expand All @@ -57,6 +62,13 @@ def test_dependencies_graph(dest):
)


@pytest.mark.parametrize("filename", ["graph.png", "graph"])
def test_missing_graphviz(filename):
"""Raises if graphviz is not installed, and defaults to png if no extension given"""
with pytest.raises(RuntimeError, match=r"Cannot generate `graph\.png`.*"):
imports._dependencies_graph(filename, {"a": ["b", "c"], "b": ["c"]})


@pytest.fixture
def linter():
pylinter = PyLinter(reporter=testutils.GenericTestReporter())
Expand Down

0 comments on commit a37c643

Please sign in to comment.