Skip to content

Commit c1131f8

Browse files
committed
Improve error messages by including more detailed information.
1 parent 76f8ebb commit c1131f8

File tree

3 files changed

+175
-39
lines changed

3 files changed

+175
-39
lines changed

CHANGES.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Changelog
44
1.3 (unreleased)
55
----------------
66

7+
- Improve error messages by including more detailed information.
8+
[thet]
9+
710
- Do not wrap resource ``__repr__`` output in ``<>`` to render tracebacks
811
properly in browser.
912
[lenadax]
@@ -68,7 +71,7 @@ Changelog
6871
Modernize setup.[py|cfg].
6972
[jensens]
7073

71-
- Added ``GracefulResourceRenderer``.
74+
- Added ``GracefulResourceRenderer``.
7275
Fixes #1.
7376
[jensens]
7477

webresource/_api.py

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,36 @@ def include(self, include):
8585
def remove(self):
8686
"""Remove resource or resource group from parent group."""
8787
if not self.parent:
88-
raise ResourceError('Object is no member of a resource group')
88+
raise ResourceError(
89+
'Cannot remove resource or resource group {}.'
90+
' It is no member of a resource group.'.format(self)
91+
)
8992
self.parent.members.remove(self)
9093
self.parent = None
9194

9295
def copy(self):
9396
"""Return a deep copy of this object."""
9497
return copy.deepcopy(self)
9598

99+
def __repr__(self):
100+
"""Return a string representation of the resource or resource group."""
101+
102+
reprs = []
103+
if getattr(self, "name", None):
104+
reprs.append('name="{}"'.format(self.name))
105+
elif getattr(self, "path", None):
106+
reprs.append('path="{}"'.format(self.path))
107+
elif getattr(self, "url", None):
108+
reprs.append('url="{}"'.format(self.url))
109+
110+
if getattr(self, "depends", None):
111+
reprs.append('depends="{}"'.format(self.depends))
112+
113+
return '<{} {}>'.format(
114+
self.__class__.__name__,
115+
", ".join(reprs) if reprs else "unnamed"
116+
)
117+
96118

97119
class Resource(ResourceMixin):
98120
"""A web resource."""
@@ -135,12 +157,16 @@ def __init__(
135157
additional attributes on resource tag.
136158
:raise ResourceError: No resource and no url given.
137159
"""
138-
if resource is None and url is None:
139-
raise ResourceError('Either resource or url must be given')
140160
super(Resource, self).__init__(
141161
name=name, directory=directory, path=path,
142162
include=include, group=group
143163
)
164+
if resource is None and url is None:
165+
raise ResourceError(
166+
'Either resource or url must be given for resource {}'.format(
167+
self
168+
)
169+
)
144170
self.depends = (
145171
(depends if isinstance(depends, (list, tuple)) else [depends])
146172
if depends else None
@@ -169,7 +195,9 @@ def file_path(self):
169195
"""Absolute resource file path depending on operation mode."""
170196
directory = self.directory
171197
if not directory:
172-
raise ResourceError('No directory set on resource.')
198+
raise ResourceError(
199+
'No directory set on resource {}'.format(self)
200+
)
173201
return os.path.join(directory, self.file_name)
174202

175203
@property
@@ -235,15 +263,6 @@ def _render_tag(self, tag, closing_tag, **attrs):
235263
return u'<{tag}{attrs} />'.format(tag=tag, attrs=attrs_)
236264
return u'<{tag}{attrs}></{tag}>'.format(tag=tag, attrs=attrs_)
237265

238-
def __repr__(self):
239-
return (
240-
'{} name="{}", depends="{}"'
241-
).format(
242-
self.__class__.__name__,
243-
self.name,
244-
self.depends
245-
)
246-
247266

248267
class ScriptResource(Resource):
249268
"""A Javascript resource."""
@@ -323,8 +342,10 @@ def integrity(self):
323342
def integrity(self, integrity):
324343
if integrity is True:
325344
if self.url is not None:
326-
msg = 'Cannot calculate integrity hash from external resource'
327-
raise ResourceError(msg)
345+
raise ResourceError(
346+
'Cannot calculate integrity hash from external resource '
347+
'{}'.format(self)
348+
)
328349
self._integrity_hash = None
329350
else:
330351
self._integrity_hash = integrity
@@ -564,8 +585,8 @@ def add(self, member):
564585
"""
565586
if not isinstance(member, (ResourceGroup, Resource)):
566587
raise ResourceError(
567-
'Resource group can only contain instances '
568-
'of ``ResourceGroup`` or ``Resource``'
588+
'Resource group {} can only contain instances '
589+
'of ``ResourceGroup`` or ``Resource``'.format(self)
569590
)
570591
member.parent = self
571592
self._members.append(member)
@@ -584,12 +605,6 @@ def _filtered_resources(self, type_, members=None):
584605
resources.append(member)
585606
return resources
586607

587-
def __repr__(self):
588-
return '{} name="{}"'.format(
589-
self.__class__.__name__,
590-
self.name
591-
)
592-
593608

594609
class ResourceConflictError(ResourceError):
595610
"""Multiple resources declared with the same name."""
@@ -634,7 +649,7 @@ def __init__(self, members):
634649
for member in members:
635650
if not isinstance(member, (Resource, ResourceGroup)):
636651
raise ResourceError(
637-
'members can only contain instances '
652+
'ResourceResolver members can only contain instances '
638653
'of ``ResourceGroup`` or ``Resource``'
639654
)
640655
self.members = members
@@ -726,10 +741,19 @@ class GracefulResourceRenderer(ResourceRenderer):
726741
def render(self):
727742
lines = []
728743
for resource in self.resolver.resolve():
744+
error_message = None
729745
try:
730746
lines.append(resource.render(self.base_url))
731-
except (ResourceError, FileNotFoundError):
732-
msg = u'Failure to render resource "{}"'.format(resource.name)
733-
lines.append(u'<!-- {} - details in logs -->'.format(msg))
734-
logger.exception(msg)
747+
except FileNotFoundError:
748+
error_message = u'File not found for resource {}'.format(
749+
resource
750+
)
751+
except ResourceError as e:
752+
error_message = str(e)
753+
finally:
754+
if error_message:
755+
lines.append(u'<!-- {} - details in logs -->'.format(
756+
error_message
757+
))
758+
logger.exception(error_message)
735759
return u'\n'.join(lines)

webresource/tests.py

Lines changed: 119 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def test_Resource(self, tempdir):
111111
self.assertEqual(resource.type_, None)
112112
self.assertEqual(
113113
repr(resource),
114-
'Resource name="res", depends="None"'
114+
'<Resource name="res">'
115115
)
116116

117117
resource = Resource(name='res', resource='res.ext')
@@ -239,7 +239,7 @@ def test_ScriptResource(self, tempdir):
239239
self.assertEqual(script.nomodule, None)
240240
self.assertEqual(
241241
repr(script),
242-
'ScriptResource name="js_res", depends="None"'
242+
"""<ScriptResource name="js_res">"""
243243
)
244244
self.assertEqual(
245245
script.render('https://tld.org'),
@@ -301,7 +301,7 @@ def test_LinkMixin(self):
301301
self.assertEqual(link.title, None)
302302
self.assertEqual(
303303
repr(link),
304-
'LinkMixin name="link_res", depends="None"'
304+
'<LinkMixin name="link_res">'
305305
)
306306
link.hreflang = 'en'
307307
link.media = 'screen'
@@ -327,7 +327,7 @@ def test_LinkResource(self):
327327
self.assertIsInstance(link, LinkMixin)
328328
self.assertEqual(
329329
repr(link),
330-
'LinkResource name="icon_res", depends="None"'
330+
'<LinkResource name="icon_res">'
331331
)
332332
link.rel = 'icon'
333333
link.type_ = 'image/png'
@@ -355,7 +355,7 @@ def test_StyleResource(self):
355355
self.assertEqual(style.rel, 'stylesheet')
356356
self.assertEqual(
357357
repr(style),
358-
'StyleResource name="css_res", depends="None"'
358+
"""<StyleResource name="css_res">"""
359359
)
360360
self.assertEqual(style.render('https://tld.org'), (
361361
'<link href="https://tld.org/res.css" media="all" '
@@ -377,7 +377,7 @@ def test_ResourceGroup(self):
377377
self.assertIsInstance(group, ResourceMixin)
378378
self.assertEqual(group.name, 'groupname')
379379
self.assertEqual(group.members, [])
380-
self.assertEqual(repr(group), 'ResourceGroup name="groupname"')
380+
self.assertEqual(repr(group), '<ResourceGroup name="groupname">')
381381

382382
res = wr.ScriptResource(name='name', resource='name.js')
383383
group.add(res)
@@ -442,6 +442,115 @@ def test_ResourceGroup(self):
442442
self.assertEqual(group.members, [])
443443
self.assertEqual(resource.parent, None)
444444

445+
def test_resource_repr(self):
446+
res = ResourceMixin(name='res')
447+
self.assertEqual(
448+
repr(res),
449+
'<ResourceMixin name="res">'
450+
)
451+
452+
# Fallback, if nothing is set.
453+
res = ResourceMixin()
454+
self.assertEqual(
455+
repr(res),
456+
'<ResourceMixin unnamed>'
457+
)
458+
459+
res = ResourceMixin(path="/a/b/c")
460+
self.assertEqual(
461+
repr(res),
462+
'<ResourceMixin path="/a/b/c">'
463+
)
464+
465+
res = Resource(url="https://example.com")
466+
self.assertEqual(
467+
repr(res),
468+
'<Resource url="https://example.com">'
469+
)
470+
471+
# Name takes precendence over all other attributes.
472+
res = Resource(name='res', path="/a/b/c", url="https://example.com")
473+
self.assertEqual(
474+
repr(res),
475+
'<Resource name="res">'
476+
)
477+
478+
# Path takes precendence over url.
479+
res = Resource(path="/a/b/c", url="https://example.com")
480+
self.assertEqual(
481+
repr(res),
482+
'<Resource path="/a/b/c">'
483+
)
484+
485+
# Example with depends
486+
res = Resource(
487+
name='res',
488+
url="https://example.com",
489+
depends="other.ext",
490+
)
491+
self.assertEqual(
492+
repr(res),
493+
"""<Resource name="res", depends="['other.ext']">"""
494+
)
495+
496+
def test_ResourceError(self):
497+
with self.assertRaises(wr.ResourceError) as cm:
498+
Resource()
499+
500+
self.assertEqual(
501+
str(cm.exception),
502+
'Either resource or url must be given for resource '
503+
'<Resource unnamed>'
504+
)
505+
506+
with self.assertRaises(wr.ResourceError) as cm:
507+
res = ResourceMixin(name="res")
508+
res.remove()
509+
510+
self.assertEqual(
511+
str(cm.exception),
512+
'Cannot remove resource or resource group '
513+
'<ResourceMixin name="res">. It is no member of a resource group.'
514+
)
515+
516+
with self.assertRaises(wr.ResourceError) as cm:
517+
res = Resource(url='https://example.com')
518+
res.file_path
519+
520+
self.assertEqual(
521+
str(cm.exception),
522+
'No directory set on resource <Resource url="https://example.com">'
523+
)
524+
525+
with self.assertRaises(wr.ResourceError) as cm:
526+
res = wr.ScriptResource(url='https://example.com')
527+
res.integrity = True
528+
529+
self.assertEqual(
530+
str(cm.exception),
531+
'Cannot calculate integrity hash from external resource '
532+
'<ScriptResource url="https://example.com">'
533+
)
534+
535+
with self.assertRaises(wr.ResourceError) as cm:
536+
res = wr.ResourceGroup(name='okay')
537+
res.add(object())
538+
539+
self.assertEqual(
540+
str(cm.exception),
541+
'Resource group <ResourceGroup name="okay"> can only contain '
542+
'instances of ``ResourceGroup`` or ``Resource``'
543+
)
544+
545+
with self.assertRaises(wr.ResourceError) as cm:
546+
res = wr.ResourceResolver(None)
547+
548+
self.assertEqual(
549+
str(cm.exception),
550+
'ResourceResolver members can only contain instances of '
551+
'``ResourceGroup`` or ``Resource``'
552+
)
553+
445554
def test_ResourceConflictError(self):
446555
counter = Counter(['a', 'b', 'b', 'c', 'c'])
447556
err = wr.ResourceConflictError(counter)
@@ -452,15 +561,15 @@ def test_ResourceCircularDependencyError(self):
452561
err = wr.ResourceCircularDependencyError([resource])
453562
self.assertEqual(str(err), (
454563
'Resources define circular dependencies: '
455-
'[Resource name="res1", depends="[\'res2\']"]'
564+
"""[<Resource name="res1", depends="['res2']">]"""
456565
))
457566

458567
def test_ResourceMissingDependencyError(self):
459568
resource = Resource(name='res', resource='res.ext', depends='missing')
460569
err = wr.ResourceMissingDependencyError(resource)
461570
self.assertEqual(str(err), (
462571
'Resource defines missing dependency: '
463-
'Resource name="res", depends="[\'missing\']"'
572+
"""<Resource name="res", depends="['missing']">"""
464573
))
465574

466575
def test_ResourceResolver__flat_resources(self):
@@ -687,7 +796,7 @@ def test_GracefulResourceRenderer(self):
687796
# check if its ours
688797
self.assertEqual(
689798
captured.records[0].getMessage().split('\n')[0],
690-
'Failure to render resource "js2"',
799+
"""File not found for resource <ScriptResource name="js2", depends="['js']">"""
691800
)
692801
else: # pragma: nocover
693802
rendered = renderer.render()
@@ -699,7 +808,7 @@ def test_GracefulResourceRenderer(self):
699808
'<link href="https://ext.org/styles.css" media="all" '
700809
'rel="stylesheet" type="text/css" />\n'
701810
'<script src="https://tld.org/res/script.js"></script>\n'
702-
'<!-- Failure to render resource "js2" - details in logs -->'
811+
"""<!-- File not found for resource <ScriptResource name="js2", depends="['js']"> - details in logs -->"""
703812
))
704813

705814

0 commit comments

Comments
 (0)