Skip to content

Commit d04494c

Browse files
authored
Merge pull request #56 from openzim/better_error_handling
2 parents ae10de3 + ea307f5 commit d04494c

File tree

4 files changed

+125
-50
lines changed

4 files changed

+125
-50
lines changed

libzim/lib.cxx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
#include "wrapper_api.h"
2727

28+
#include <cstdlib>
2829
#include <iostream>
2930
#include <zim/writer/url.h>
3031
#include <zim/blob.h>
@@ -62,11 +63,11 @@ std::string ZimArticleWrapper::callCythonReturnString(std::string methodName) co
6263
if (!this->m_obj)
6364
throw std::runtime_error("Python object not set");
6465

65-
int error;
66+
std::string error;
6667

6768
std::string ret_val = string_cy_call_fct(this->m_obj, methodName, &error);
68-
if (error)
69-
throw std::runtime_error("The pure virtual function " + methodName + " must be override");
69+
if (!error.empty())
70+
throw std::runtime_error(error);
7071

7172
return ret_val;
7273
}
@@ -76,11 +77,11 @@ zim::Blob ZimArticleWrapper::callCythonReturnBlob(std::string methodName) const
7677
if (!this->m_obj)
7778
throw std::runtime_error("Python object not set");
7879

79-
int error;
80+
std::string error;
8081

8182
zim::Blob ret_val = blob_cy_call_fct(this->m_obj, methodName, &error);
82-
if (error)
83-
throw std::runtime_error("The pure virtual function " + methodName + " must be override");
83+
if (!error.empty())
84+
throw std::runtime_error(error);
8485

8586
return ret_val;
8687
}
@@ -90,11 +91,11 @@ bool ZimArticleWrapper::callCythonReturnBool(std::string methodName) const
9091
if (!this->m_obj)
9192
throw std::runtime_error("Python object not set");
9293

93-
int error;
94+
std::string error;
9495

9596
bool ret_val = bool_cy_call_fct(this->m_obj, methodName, &error);
96-
if (error)
97-
throw std::runtime_error("The pure virtual function " + methodName + " must be override");
97+
if (!error.empty())
98+
throw std::runtime_error(error);
9899

99100
return ret_val;
100101
}
@@ -104,11 +105,11 @@ uint64_t ZimArticleWrapper::callCythonReturnInt(std::string methodName) const
104105
if (!this->m_obj)
105106
throw std::runtime_error("Python object not set");
106107

107-
int error;
108+
std::string error;
108109

109-
int ret_val = int_cy_call_fct(this->m_obj, methodName, &error);
110-
if (error)
111-
throw std::runtime_error("The pure virtual function " + methodName + " must be override");
110+
int64_t ret_val = int_cy_call_fct(this->m_obj, methodName, &error);
111+
if (!error.empty())
112+
throw std::runtime_error(error);
112113

113114
return ret_val;
114115
}

libzim/wrapper.pyx

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ from libcpp.memory cimport shared_ptr, make_shared, unique_ptr
3232

3333
import pathlib
3434
import datetime
35+
import traceback
3536

3637

3738
#########################
@@ -98,44 +99,52 @@ cdef class ReadingBlob:
9899
self.view_count -= 1
99100

100101

101-
#------ Helper for pure virtual methods --------
102-
103-
cdef get_article_method_from_object(object obj, string method, int *error) with gil:
104-
try:
105-
func = getattr(obj, method.decode('UTF-8'))
106-
except AttributeError:
107-
error[0] = 1
108-
raise
109-
else:
110-
error[0] = 0
111-
return func
112-
113102
#------- ZimArticle pure virtual methods --------
114103

115104
cdef public api:
116-
string string_cy_call_fct(object obj, string method, int *error) with gil:
105+
string string_cy_call_fct(object obj, string method, string *error) with gil:
117106
"""Lookup and execute a pure virtual method on ZimArticle returning a string"""
118-
func = get_article_method_from_object(obj, method, error)
119-
ret_str = func()
120-
return ret_str.encode('UTF-8')
121-
122-
wrapper.Blob blob_cy_call_fct(object obj, string method, int *error) with gil:
107+
try:
108+
func = getattr(obj, method.decode('UTF-8'))
109+
ret_str = func()
110+
return ret_str.encode('UTF-8')
111+
except Exception as e:
112+
error[0] = traceback.format_exc().encode('UTF-8')
113+
return b""
114+
115+
wrapper.Blob blob_cy_call_fct(object obj, string method, string *error) with gil:
123116
"""Lookup and execute a pure virtual method on ZimArticle returning a Blob"""
124117
cdef WritingBlob blob
125118

126-
func = get_article_method_from_object(obj, method, error)
127-
blob = func()
128-
return dereference(blob.c_blob)
119+
try:
120+
func = getattr(obj, method.decode('UTF-8'))
121+
blob = func()
122+
if blob is None:
123+
raise RuntimeError("Blob is none")
124+
return dereference(blob.c_blob)
125+
except Exception as e:
126+
error[0] = traceback.format_exc().encode('UTF-8')
129127

130-
bool bool_cy_call_fct(object obj, string method, int *error) with gil:
128+
return Blob()
129+
130+
bool bool_cy_call_fct(object obj, string method, string *error) with gil:
131131
"""Lookup and execute a pure virtual method on ZimArticle returning a bool"""
132-
func = get_article_method_from_object(obj, method, error)
133-
return func()
132+
try:
133+
func = getattr(obj, method.decode('UTF-8'))
134+
return func()
135+
except Exception as e:
136+
error[0] = traceback.format_exc().encode('UTF-8')
137+
return False
134138

135-
uint64_t int_cy_call_fct(object obj, string method, int *error) with gil:
139+
uint64_t int_cy_call_fct(object obj, string method, string *error) with gil:
136140
"""Lookup and execute a pure virtual method on ZimArticle returning an int"""
137-
func = get_article_method_from_object(obj, method, error)
138-
return <uint64_t> func()
141+
try:
142+
func = getattr(obj, method.decode('UTF-8'))
143+
return <uint64_t>func()
144+
except Exception as e:
145+
error[0] = traceback.format_exc().encode('UTF-8')
146+
147+
return 0
139148

140149
cdef class Creator:
141150
""" Zim Creator

libzim/writer.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,35 +50,35 @@ def __init__(self):
5050

5151
def get_url(self) -> str:
5252
""" Full URL of article including namespace """
53-
raise NotImplementedError
53+
raise NotImplementedError("get_url must be implemented.")
5454

5555
def get_title(self) -> str:
5656
""" Article title. Might be indexed and used in suggestions """
57-
raise NotImplementedError
57+
raise NotImplementedError("get_title must be implemented.")
5858

5959
def is_redirect(self) -> bool:
6060
""" Whether this redirects to another article (cf. redirec_url) """
61-
raise NotImplementedError
61+
raise NotImplementedError("get_redirect must be implemented.")
6262

6363
def get_mime_type(self) -> str:
6464
""" MIME-type of the article's content. A/ namespace reserved to text/html """
65-
raise NotImplementedError
65+
raise NotImplementedError("get_mime_type must be implemented.")
6666

6767
def get_filename(self) -> str:
6868
""" Filename to get content from. Blank string "" if not used """
69-
raise NotImplementedError
69+
raise NotImplementedError("get_filename must be implemented.")
7070

7171
def should_compress(self) -> bool:
7272
""" Whether the article's content should be compressed or not """
73-
raise NotImplementedError
73+
raise NotImplementedError("should_compress must be implemented.")
7474

7575
def should_index(self) -> bool:
7676
""" Whether the article's content should be indexed or not """
77-
raise NotImplementedError
77+
raise NotImplementedError("should_index must be implemented.")
7878

7979
def redirect_url(self) -> str:
8080
""" Full URL including namespace of another article """
81-
raise NotImplementedError
81+
raise NotImplementedError("redirect_url must be implemented.")
8282

8383
def _get_data(self) -> Blob:
8484
""" Internal data-retrieval with a cache to the content's pointer
@@ -90,7 +90,7 @@ def _get_data(self) -> Blob:
9090

9191
def get_data(self) -> Blob:
9292
""" Blob containing the complete content of the article """
93-
raise NotImplementedError
93+
raise NotImplementedError("get_data must be implemented.")
9494

9595
def __repr__(self) -> str:
9696
return f"{self.__class__.__name__}(url={self.get_url()}, title={self.get_title()})"

tests/test_libzim.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616
# You should have received a copy of the GNU General Public License
1717
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1818

19-
import pytest
19+
import sys
20+
import subprocess
2021
import pathlib
2122

23+
import pytest
24+
2225
from libzim.writer import Article, Blob, Creator
2326
from libzim.reader import File
2427

@@ -188,3 +191,65 @@ def test_filename_param_types(tmpdir):
188191
with Creator(str(path), "welcome") as creator:
189192
assert creator.filename == path
190193
assert isinstance(creator.filename, pathlib.Path)
194+
195+
196+
def test_in_article_exceptions(tmpdir):
197+
""" make sure we raise RuntimeError from article's virtual methods """
198+
199+
class BoolErrorArticle(SimpleArticle):
200+
def is_redirect(self):
201+
raise RuntimeError("OUPS Redirect")
202+
203+
class StringErrorArticle(SimpleArticle):
204+
def get_url(self):
205+
raise IOError
206+
207+
class BlobErrorArticle(SimpleArticle):
208+
def get_data(self):
209+
raise IOError
210+
211+
path, main_page = tmpdir / "test.zim", "welcome"
212+
args = {"title": "Hello", "mime_type": "text/html", "content": "", "url": "welcome"}
213+
214+
with Creator(path, main_page) as zim_creator:
215+
# make sure we can can exception of all types (except int, not used)
216+
with pytest.raises(RuntimeError, match="OUPS Redirect"):
217+
zim_creator.add_article(BoolErrorArticle(**args))
218+
with pytest.raises(RuntimeError, match="in get_url"):
219+
zim_creator.add_article(StringErrorArticle(**args))
220+
with pytest.raises(RuntimeError, match="IOError"):
221+
zim_creator.add_article(BlobErrorArticle(**args))
222+
with pytest.raises(RuntimeError, match="NotImplementedError"):
223+
zim_creator.add_article(Article())
224+
225+
# make sure we can catch it from outside creator
226+
with pytest.raises(RuntimeError):
227+
with Creator(path, main_page) as zim_creator:
228+
zim_creator.add_article(BlobErrorArticle(**args))
229+
230+
231+
def test_dontcreatezim_onexception(tmpdir):
232+
""" make sure we can prevent ZIM file creation (workaround missing cancel())
233+
234+
A new interpreter is instanciated to get a different memory space.
235+
This workaround is not safe and may segfault at GC under some circumstances
236+
237+
Unless we get a proper cancel() on libzim, that's the only way to not create
238+
a ZIM file on error """
239+
path, main_page = tmpdir / "test.zim", "welcome"
240+
pycode = f"""
241+
from libzim.writer import Creator
242+
from libzim.writer import Article
243+
class BlobErrorArticle(Article):
244+
def get_data(self):
245+
raise ValueError
246+
zim_creator = Creator("{path}", "{main_page}")
247+
try:
248+
zim_creator.add_article(BlobErrorArticle(**args))
249+
except Exception:
250+
zim_creator._closed = True
251+
"""
252+
253+
py = subprocess.run([sys.executable, "-c", pycode])
254+
assert py.returncode == 0
255+
assert not path.exists()

0 commit comments

Comments
 (0)