Skip to content

Commit

Permalink
Log error messages with a callback instead of file (GenericMappingToo…
Browse files Browse the repository at this point in the history
…ls#188)

Use a callback function passed to `GMT_Create_Session` to log error
messages instead of redirecting them to a file using
`GMT_Handle_Messages`. It's a lot cleaner and the code is more legible.
Other functions don't need to do anything to have their errors logged,
it's automatic.
The logged messaged are also printed to stderr so they will show up in
the Jupyter notebook.
This is useful when using the verbose mode (`V="d"`) in modules.

Switch to the Fatiando a Terra CI scripts and enable OSX testing on 
Travis.
Fatiando provides scripts for handling all of the CI tasks we need: 
https://github.com/fatiando/continuous-integration 
Use them instead of rewriting everything.

Fixes GenericMappingTools#164 
The Segmentation fault on OSX was happening because of the log file
that we used to capture the GMT output and include in the exception.
I have no idea why this happens.
But removing that fixes the issue so I'm happy not knowing.
  • Loading branch information
leouieda committed Jun 16, 2018
1 parent edcfefb commit 3daf293
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 299 deletions.
1 change: 1 addition & 0 deletions .stickler.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
linters:
flake8:
python: 3
enable: true
ignore: E203, E266, E501, W503, F401, E741
max-line-length: 88
Expand Down
62 changes: 30 additions & 32 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ env:
- BUILD_DOCS=false
- DEPLOY_DOCS=false
- DEPLOY_PYPI=false
# Need the dev channel to get development builds of GMT 6
- CONDA_FLAGS="--yes --quiet -c conda-forge -c conda-forge/label/dev"
- CONDA_REQUIREMENTS="requirements.txt"

matrix:
# Build under the following configurations
Expand All @@ -40,25 +39,24 @@ matrix:
- BUILD_DOCS=true
- DEPLOY_DOCS=true
- DEPLOY_PYPI=true
#- os: osx
#env:
#- PYTHON=3.5
#- os: osx
#env:
#- PYTHON=3.6
#- COVERAGE=true
#- BUILD_DOCS=true
- os: osx
env:
- PYTHON=3.5
- BUILD_DOCS=true
- os: osx
env:
- PYTHON=3.6
- COVERAGE=true
- BUILD_DOCS=true

before_install:
# Get Miniconda from Continuum
# Need to source the script to set the PATH variable in this environment
# (not the scripts environment)
- source ci/install-miniconda.sh
- conda update conda $CONDA_FLAGS
- conda create --name testenv python=$PYTHON pip $CONDA_FLAGS
- source activate testenv
# Install dependencies
- conda install --file requirements.txt $CONDA_FLAGS
# Get the Fatiando CI scripts
- git clone https://github.com/fatiando/continuous-integration.git
# Download and install miniconda and setup dependencies
# Need to source the script to set the PATH variable globaly
- source continuous-integration/travis/setup-miniconda.sh
# Install GMT from the dev channel to get development builds of GMT 6
- conda install --yes --quiet -c conda-forge/label/dev gmt=6.0.0a*
- if [ "$COVERAGE" == "true" ]; then
pip install codecov codacy-coverage codeclimate-test-reporter;
fi
Expand All @@ -75,11 +73,11 @@ script:
- if [ "$CHECK" == "true" ]; then
make check;
fi
# Run the test suite
# Run the test suite. Make pytest report any captured output on stdout or stderr.
- if [ "$COVERAGE" == "true" ]; then
make coverage;
make coverage PYTEST_EXTRA="-r P";
else
make test;
make test PYTEST_EXTRA="-r P";
fi
# Build the documentation
- if [ "$BUILD_DOCS" == "true" ]; then
Expand All @@ -98,26 +96,26 @@ after_success:
fi

deploy:
# Push the built docs to Github pages
# Make a release on PyPI
- provider: script
script: ci/deploy-docs.sh
script: continuous-integration/travis/deploy-pypi.sh
on:
tags: true
condition: '$DEPLOY_PYPI == "true"'
# Push the built HTML in doc/_build/html to the gh-pages branch
- provider: script
script: continuous-integration/travis/deploy-gh-pages.sh
skip_cleanup: true
on:
branch: master
condition: '$DEPLOY_DOCS == "true"'
# Push docs when building tags as well
# Push HTML when building tags as well
- provider: script
script: ci/deploy-docs.sh
script: continuous-integration/travis/deploy-gh-pages.sh
skip_cleanup: true
on:
tags: true
condition: '$DEPLOY_DOCS == "true"'
# Make a release on PyPI
- provider: script
script: ci/deploy-pypi.sh
on:
tags: true
condition: '$DEPLOY_PYPI == "true"'

notifications:
email: false
Expand Down
55 changes: 0 additions & 55 deletions ci/deploy-docs.sh

This file was deleted.

27 changes: 0 additions & 27 deletions ci/deploy-pypi.sh

This file was deleted.

23 changes: 0 additions & 23 deletions ci/install-miniconda.sh

This file was deleted.

135 changes: 38 additions & 97 deletions gmt/clib/core.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"""
ctypes wrappers for core functions from the C API
"""
import os
import sys
import ctypes
from tempfile import NamedTemporaryFile
from contextlib import contextmanager

from packaging.version import Version
Expand Down Expand Up @@ -254,10 +253,26 @@ def create_session(self, session_name):
restype=ctypes.c_void_p,
)

# None is passed in place of the print function pointer. It becomes the
# NULL pointer when passed to C, prompting the C API to use the default
# print function.
print_func = None
# Capture the output printed by GMT into this list. Will use it later to
# generate error messages for the exceptions raised by API calls.
self._log = []

@ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_char_p)
def print_func(file_pointer, message): # pylint: disable=unused-argument
"""
Callback function that GMT uses to print log and error messages.
We'll capture the message and print it to stderr so that it will show up on
the Jupyter notebook.
"""
message = message.decode().strip()
self._log.append(message)
print(message, file=sys.stderr)
return 0

# Need to store a copy of the function because ctypes doesn't and it will be
# garbage collected otherwise
self._print_callback = print_func

padding = self.get_constant("GMT_PAD_DEFAULT")
session_type = self.get_constant("GMT_SESSION_EXTERNAL")
session = c_create_session(
Expand All @@ -269,6 +284,16 @@ def create_session(self, session_name):

return session

def _get_error_message(self):
"""
Return a string with error messages emitted by GMT.
Only includes messages with the string "[ERROR]" in them.
"""
msg = ""
if hasattr(self, "_log"):
msg = "\n".join(line for line in self._log if "[ERROR]" in line)
return msg

def destroy_session(self, session):
"""
Terminate and free the memory of a registered ``GMTAPI_CTRL`` session.
Expand Down Expand Up @@ -382,73 +407,6 @@ def get_default(self, name):

return value.value.decode()

@contextmanager
def log_to_file(self, logfile=None):
"""
Set the next API call in this session to log to a file.
Use it as a context manager (in a ``with`` block) to capture the error
messages from GMT API calls. Mostly useful with ``GMT_Call_Module``
because most others don't print error messages.
The log file will be deleted when exiting the ``with`` block.
Calls the GMT API function ``GMT_Handle_Messages`` using
``GMT_LOG_ONCE`` mode (to only log errors from the next API call).
Only works for a **single API call**, so make calls like
``get_constant`` outside of the ``with`` block.
Parameters
----------
* logfile : str or None
The name of the logfile. If ``None`` (default),
the file name is automatically generated by the tempfile module.
Examples
--------
>>> with LibGMT() as lib:
... mode = lib.get_constant('GMT_MODULE_CMD')
... with lib.log_to_file() as logfile:
... call_module = lib.get_libgmt_func('GMT_Call_Module')
... status = call_module(lib.current_session, 'info'.encode(),
... mode, 'bogus-file.bla'.encode())
... with open(logfile) as flog:
... print(flog.read().strip())
gmtinfo [ERROR]: Error for input file: No such file (bogus-file.bla)
"""
c_handle_messages = self.get_libgmt_func(
"GMT_Handle_Messages",
argtypes=[ctypes.c_void_p, ctypes.c_uint, ctypes.c_uint, ctypes.c_char_p],
restype=ctypes.c_int,
)

if logfile is None:
tmp_file = NamedTemporaryFile(
prefix="gmt-python-", suffix=".log", delete=False
)
logfile = tmp_file.name
tmp_file.close()

status = c_handle_messages(
self.current_session,
self.get_constant("GMT_LOG_ONCE"),
self.get_constant("GMT_IS_FILE"),
logfile.encode(),
)
if status != 0:
msg = "Failed to set logging to file '{}' (error: {}).".format(
logfile, status
)
raise GMTCLibError(msg)

# The above is called when entering a 'with' statement
yield logfile

# Clean up when exiting the 'with' statement
os.remove(logfile)

def call_module(self, module, args):
"""
Call a GMT module with the given arguments.
Expand Down Expand Up @@ -479,30 +437,15 @@ def call_module(self, module, args):
)

mode = self.get_constant("GMT_MODULE_CMD")
# If there is no open session, this will raise an exception. Can' let
# it happen inside the 'with' otherwise the logfile won't be deleted.
session = self.current_session
with self.log_to_file() as logfile:
status = c_call_module(session, module.encode(), mode, args.encode())
# Get the error message inside the with block before the log file
# is deleted
with open(logfile) as flog:
log = flog.read().strip()
# Raise the exception outside the log 'with' to make sure the logfile
# is cleaned.
status = c_call_module(
self.current_session, module.encode(), mode, args.encode()
)
if status != 0:
if log == "":
msg = "Invalid GMT module name '{}'.".format(module)
else:
msg = "\n".join(
[
"Command '{}' failed:".format(module),
"---------- Error log ----------",
log,
"-------------------------------",
]
raise GMTCLibError(
"Module '{}' failed with status code {}:\n{}".format(
module, status, self._get_error_message()
)
raise GMTCLibError(msg)
)

def create_data(self, family, geometry, mode, **kwargs):
"""
Expand Down Expand Up @@ -1324,8 +1267,6 @@ def extract_region(self):
)

wesn = np.empty(4, dtype=np.float64)
# Use NaNs so that we can know if GMT didn't change the array
wesn[:] = np.nan
wesn_pointer = wesn.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
# The second argument to GMT_Extract_Region is a file pointer to a
# PostScript file. It's only valid in classic mode. Use None to get a
Expand Down
Loading

0 comments on commit 3daf293

Please sign in to comment.