Skip to content

Commit

Permalink
[feature] User can override templates for table and graph (conan-io#7176
Browse files Browse the repository at this point in the history
)

* let the user override templates in '<cache>/templates' folder

* value explicit

* pass 'base_template_path'

* test 'base_template_path'

* expose raw data in graph

* test documented fields of grapher

* topics without quotes
  • Loading branch information
jgsogo authored Jun 18, 2020
1 parent 444d445 commit 51bf334
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 23 deletions.
6 changes: 6 additions & 0 deletions conans/assets/templates/info_graph_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@
' <li><b>id</b>: {{ node.package_id }}</li>' +
{%- for key, value in node.data().items() %}
{%- if value %}
{%- if key in ['url', 'homepage'] %}
' <li><b>{{ key }}</b>: <a href="{{ value }}">{{ value }}</a></li>' +
{%- elif key in ['topics'] %}
' <li><b>{{ key }}</b>: {{ value|join(", ") }}</li>' +
{%- else %}
' <li><b>{{ key }}</b>: {{ value }}</li>' +
{%- endif %}
{%- endif %}
{%- endfor %}
'</ul>'
Expand Down
11 changes: 7 additions & 4 deletions conans/client/cache/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from collections import OrderedDict
from os.path import join

from jinja2 import Environment, select_autoescape
from jinja2 import Environment, select_autoescape, FileSystemLoader, ChoiceLoader

from conans.assets.templates import dict_loader
from conans.client.cache.editable import EditablePackages
Expand Down Expand Up @@ -226,10 +226,13 @@ def remove_locks(self):
Lock.clean(conan_folder)
shutil.rmtree(os.path.join(conan_folder, "locks"), ignore_errors=True)

@classmethod
def get_template(cls, template_name):
def get_template(self, template_name, user_overrides=False):
# TODO: It can be initialized only once together with the Conan app
env = Environment(loader=dict_loader, autoescape=select_autoescape(['html', 'xml']))
loaders = [dict_loader]
if user_overrides:
loaders.insert(0, FileSystemLoader(os.path.join(self.cache_folder, 'templates')))
env = Environment(loader=ChoiceLoader(loaders),
autoescape=select_autoescape(['html', 'xml']))
return env.get_template(template_name)

def initialize_config(self):
Expand Down
9 changes: 6 additions & 3 deletions conans/client/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,9 +737,11 @@ def info(self, *args):

if args.graph:
if args.graph.endswith(".html"):
template = self._conan.app.cache.get_template(templates.INFO_GRAPH_HTML)
template = self._conan.app.cache.get_template(templates.INFO_GRAPH_HTML,
user_overrides=True)
else:
template = self._conan.app.cache.get_template(templates.INFO_GRAPH_DOT)
template = self._conan.app.cache.get_template(templates.INFO_GRAPH_DOT,
user_overrides=True)
self._outputer.info_graph(args.graph, deps_graph, get_cwd(), template=template)
if args.json:
json_arg = True if args.json == "1" else args.json
Expand Down Expand Up @@ -1325,7 +1327,8 @@ def search(self, *args):
remote_name=args.remote,
outdated=args.outdated)
# search is done for one reference
template = self._conan.app.cache.get_template(templates.SEARCH_TABLE_HTML)
template = self._conan.app.cache.get_template(templates.SEARCH_TABLE_HTML,
user_overrides=True)
self._outputer.print_search_packages(info["results"], ref, args.query,
args.table, args.raw, outdated=args.outdated,
template=template)
Expand Down
4 changes: 3 additions & 1 deletion conans/client/conan_command_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@ def info_graph(self, graph_filename, deps_graph, cwd, template):
if os.path.exists(vis_css):
assets['vis_css'] = vis_css

save(graph_filename, template.render(graph=graph, assets=assets))
template_folder = os.path.dirname(template.filename)
save(graph_filename,
template.render(graph=graph, assets=assets, base_template_path=template_folder))

def json_info(self, deps_graph, json_output, cwd, show_paths):
data = self._grab_info_data(deps_graph, grab_paths=show_paths)
Expand Down
16 changes: 6 additions & 10 deletions conans/client/graph/grapher.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from jinja2 import Markup

from conans.client.graph.graph import BINARY_BUILD, BINARY_CACHE, BINARY_DOWNLOAD, BINARY_MISSING, \
BINARY_UPDATE
from conans.client.installer import build_id
Expand Down Expand Up @@ -30,21 +28,19 @@ def is_build_requires(self):
return self._is_build_time_node

def data(self):
def format_url(url):
return Markup('<a href="{url}">{url}</a>'.format(url=url))

def join_if_iterable(value):
def ensure_iterable(value):
if isinstance(value, (list, tuple)):
return '("{}")'.format('", "'.join(value))
return value
return value
return value,

return {
'build_id': build_id(self._conanfile),
'url': format_url(self._conanfile.url),
'homepage': format_url(self._conanfile.homepage),
'url': self._conanfile.url,
'homepage': self._conanfile.homepage,
'license': self._conanfile.license,
'author': self._conanfile.author,
'topics': join_if_iterable(self._conanfile.topics)
'topics': ensure_iterable(self._conanfile.topics) if self._conanfile.topics else None
}


Expand Down
7 changes: 3 additions & 4 deletions conans/search/binary_html_table.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import os
from collections import OrderedDict, defaultdict

from jinja2 import Template

from conans.assets.templates import search_table_html
from conans.model.ref import PackageReference
from conans.util.files import save

Expand Down Expand Up @@ -152,5 +150,6 @@ def html_binary_graph(search_info, reference, table_filename, template):
results = Results(search_info)

# Render and save
content = template.render(search=search, results=results)
template_folder = os.path.dirname(template.filename)
content = template.render(search=search, results=results, base_template_path=template_folder)
save(table_filename, content)
2 changes: 1 addition & 1 deletion conans/test/functional/command/info/info_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ class MyTest(ConanFile):
client.run("info Pkg/0.2@lasote/testing --graph file.html")
html_content = client.load("file.html")
self.assertIn("<h3>Pkg/0.2@lasote/testing</h3>", html_content)
self.assertIn("<li><b>topics</b>: (&#34;foo&#34;, &#34;bar&#34;, &#34;qux&#34;)</li>", html_content)
self.assertIn("<li><b>topics</b>: foo, bar, qux</li>", html_content)

# Topics as a string
conanfile = conanfile.replace("(\"foo\", \"bar\", \"qux\")", "\"foo\"")
Expand Down
Empty file.
43 changes: 43 additions & 0 deletions conans/test/functional/templates/test_user_overrides.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import os
import unittest

from conans.assets.templates import SEARCH_TABLE_HTML, INFO_GRAPH_DOT, INFO_GRAPH_HTML
from conans.client.tools import save
from conans.model.ref import ConanFileReference
from conans.test.utils.tools import TestClient, GenConanfile


class UserOverridesTemplatesTestCase(unittest.TestCase):
lib_ref = ConanFileReference.loads("lib/version")
app_ref = ConanFileReference.loads("app/version")

@classmethod
def setUpClass(cls):
cls.t = TestClient()
cls.t.save({'lib.py': GenConanfile().with_setting("os"),
'app.py': GenConanfile().with_setting("os").with_require(cls.lib_ref)})
cls.t.run("create lib.py {}@ -s os=Windows".format(cls.lib_ref))
cls.t.run("create lib.py {}@ -s os=Linux".format(cls.lib_ref))
cls.t.run("create app.py {}@ -s os=Windows".format(cls.app_ref))
cls.t.run("create app.py {}@ -s os=Linux".format(cls.app_ref))

def test_table_html(self):
table_template_path = os.path.join(self.t.cache_folder, 'templates', SEARCH_TABLE_HTML)
save(table_template_path, content='{{ base_template_path }}')
self.t.run("search {}@ --table=output.html".format(self.lib_ref))
content = self.t.load("output.html")
self.assertEqual(os.path.join(self.t.cache_folder, 'templates', 'output'), content)

def test_graph_html(self):
table_template_path = os.path.join(self.t.cache_folder, 'templates', INFO_GRAPH_HTML)
save(table_template_path, content='{{ base_template_path }}')
self.t.run("info {}@ --graph=output.html".format(self.app_ref))
content = self.t.load("output.html")
self.assertEqual(os.path.join(self.t.cache_folder, 'templates', 'output'), content)

def test_graph_dot(self):
table_template_path = os.path.join(self.t.cache_folder, 'templates', INFO_GRAPH_DOT)
save(table_template_path, content='{{ base_template_path }}')
self.t.run("info {}@ --graph=output.dot".format(self.app_ref))
content = self.t.load("output.dot")
self.assertEqual(os.path.join(self.t.cache_folder, 'templates', 'output'), content)
91 changes: 91 additions & 0 deletions conans/test/unittests/client/graph/test_grapher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import textwrap

from conans.client.graph.grapher import Grapher, Node
from conans.model.profile import Profile
from conans.test.functional.cross_building.graph._base_test_case import CrossBuildingBaseTestCase


class GrapherTestCase(CrossBuildingBaseTestCase):
""" Written on top of the cross-building tests, so I can get a more interesting graph that
serves for the future when users start to need information related to cross-building
scenarios.
"""

application = textwrap.dedent("""
from conans import ConanFile
class Protoc(ConanFile):
name = "application"
version = "testing"
url = "http://myurl.com"
topics = "conan", "center"
settings = "os"
def requirements(self):
self.requires("protobuf/testing@user/channel")
def build_requirements(self):
self.build_requires("protoc/testing@user/channel", force_host_context=False)
# Make it explicit that these should be for host_machine
self.build_requires("protoc/testing@user/channel", force_host_context=True)
self.build_requires("gtest/testing@user/channel", force_host_context=True)
""")

def setUp(self):
super(GrapherTestCase, self).setUp()
self._cache_recipe(self.protobuf_ref, self.protobuf)
self._cache_recipe(self.protoc_ref, self.protoc)
self._cache_recipe(self.gtest_ref, self.gtest)
self._cache_recipe(self.app_ref, self.application)

profile_host = Profile()
profile_host.settings["os"] = "Host"
profile_host.process_settings(self.cache)

profile_build = Profile()
profile_build.settings["os"] = "Build"
profile_build.process_settings(self.cache)

deps_graph = self._build_graph(profile_host=profile_host, profile_build=profile_build)

self.grapher = Grapher(deps_graph)

def test_node_colors(self):
# Every node gets one color
for n in self.grapher.nodes:
color = self.grapher.binary_color(node=n)
self.assertIsInstance(color, str)

def test_nodes(self):
self.assertEqual(len(self.grapher.nodes), 7)
sorted_nodes = sorted(list(self.grapher.nodes), key=lambda it: (it.label, it.package_id))

protobuf = sorted_nodes[4]
self.assertEqual(protobuf.label, "protobuf/testing@user/channel")
self.assertEqual(protobuf.short_label, "protobuf/testing")
self.assertEqual(protobuf.package_id, "c31c69c9792316eb5e1a5641419abe169b44f775")
self.assertEqual(protobuf.is_build_requires, True)
self.assertEqual(protobuf.binary, "Build")
self.assertDictEqual(protobuf.data(), {'author': None, 'build_id': None, 'homepage': None,
'license': None, 'topics': None, 'url': None})

app = sorted_nodes[0]
self.assertEqual(app.label, "app/testing@user/channel")
self.assertEqual(app.short_label, "app/testing")
self.assertEqual(app.package_id, "28220efa62679ebe67eb9e4792449f5e03ef9f8c")
self.assertEqual(app.is_build_requires, False)
self.assertEqual(app.binary, "Build")
self.assertDictEqual(app.data(), {'author': None, 'build_id': None, 'homepage': None,
'license': None, 'topics': ('conan', 'center'),
'url': 'http://myurl.com'})

def test_edges(self):
self.assertEqual(len(self.grapher.edges), 7)
sorted_edges = sorted(list(self.grapher.edges),
key=lambda it: (it[0].label, it[0].package_id, it[1].label))

app_node, gtest_node = sorted_edges[0]
self.assertIsInstance(app_node, Node)
self.assertIsInstance(gtest_node, Node)

0 comments on commit 51bf334

Please sign in to comment.