Skip to content

Commit

Permalink
Merge pull request #6442 from matthew-brett/dynamic-tkagg
Browse files Browse the repository at this point in the history
MRG: loading TCL / Tk symbols dynamically

cherry-pick of commit f3e5576

Conflicts resolved:
    .travis.yml
    appveyor.yml
    setupext.py
  • Loading branch information
efiring committed May 24, 2016
1 parent cab8cf7 commit b80e0f1
Show file tree
Hide file tree
Showing 4 changed files with 390 additions and 319 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ script:
# multiple processes
- python -c "from matplotlib import font_manager"
- |
echo Testing import of tkagg backend
MPLBACKEND="tkagg" python -c 'import matplotlib.pyplot as plt; print(plt.get_backend())'
echo Testing using $NPROC processes
echo The following args are passed to nose $NOSE_ARGS
if [[ $BUILD_DOCS == false ]]; then
export MPL_REPO_DIR=$PWD # needed for pep8-conformance test of the examples
gdb -return-child-result -batch -ex r -ex bt --args python tests.py -s --processes=$NPROC --process-timeout=300 $TEST_ARGS
Expand Down
290 changes: 8 additions & 282 deletions setupext.py
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,7 @@ def get_install_requires(self):

class BackendAgg(OptionalBackendPackage):
name = "agg"
force = True

def get_extension(self):
sources = [
Expand All @@ -1300,36 +1301,10 @@ def get_extension(self):

class BackendTkAgg(OptionalBackendPackage):
name = "tkagg"
force = True

def __init__(self):
self.tcl_tk_cache = None

def check_requirements(self):
try:
if PY3min:
import tkinter as Tkinter
else:
import Tkinter
except ImportError:
raise CheckFailed('TKAgg requires Tkinter.')
except RuntimeError:
raise CheckFailed('Tkinter present but import failed.')
else:
if Tkinter.TkVersion < 8.3:
raise CheckFailed("Tcl/Tk v8.3 or later required.")

ext = self.get_extension()
check_include_file(ext.include_dirs, "tk.h", "Tk")

try:
tk_v = Tkinter.__version__.split()[-2]
except (AttributeError, IndexError):
# Tkinter.__version__ has been removed in python 3
tk_v = 'not identified'

BackendAgg.force = True

return "version %s" % tk_v
def check(self):
return "installing; run-time loading from Python Tcl / Tk"

def get_extension(self):
sources = [
Expand All @@ -1343,251 +1318,11 @@ def get_extension(self):
LibAgg().add_flags(ext, add_sources=False)
return ext

def query_tcltk(self):
"""
Tries to open a Tk window in order to query the Tk object
about its library paths. This should never be called more
than once by the same process, as Tk intricacies may cause the
Python interpreter to hang. The function also has a workaround
if no X server is running (useful for autobuild systems).
"""
# Use cached values if they exist, which ensures this function
# only executes once
if self.tcl_tk_cache is not None:
return self.tcl_tk_cache

# By this point, we already know that Tkinter imports correctly
if PY3min:
import tkinter as Tkinter
else:
import Tkinter
tcl_lib_dir = ''
tk_lib_dir = ''
# First try to open a Tk window (requires a running X server)
try:
tk = Tkinter.Tk()
except Tkinter.TclError:
# Next, start Tcl interpreter without opening a Tk window
# (no need for X server) This feature is available in
# python version 2.4 and up
try:
tcl = Tkinter.Tcl()
except AttributeError: # Python version not high enough
pass
except Tkinter.TclError: # Something went wrong while opening Tcl
pass
else:
tcl_lib_dir = str(tcl.getvar('tcl_library'))
# Guess Tk location based on Tcl location
(head, tail) = os.path.split(tcl_lib_dir)
tail = tail.replace('Tcl', 'Tk').replace('tcl', 'tk')
tk_lib_dir = os.path.join(head, tail)
if not os.path.exists(tk_lib_dir):
tk_lib_dir = tcl_lib_dir.replace(
'Tcl', 'Tk').replace('tcl', 'tk')
else:
# Obtain Tcl and Tk locations from Tk widget
tk.withdraw()
tcl_lib_dir = str(tk.getvar('tcl_library'))
tk_lib_dir = str(tk.getvar('tk_library'))
tk.destroy()

# Save directories and version string to cache
self.tcl_tk_cache = tcl_lib_dir, tk_lib_dir, str(Tkinter.TkVersion)[:3]
return self.tcl_tk_cache

def parse_tcl_config(self, tcl_lib_dir, tk_lib_dir):
try:
if PY3min:
import tkinter as Tkinter
else:
import Tkinter
except ImportError:
return None

tcl_poss = [tcl_lib_dir,
os.path.normpath(os.path.join(tcl_lib_dir, '..')),
"/usr/lib/tcl" + str(Tkinter.TclVersion),
"/usr/lib"]
tk_poss = [tk_lib_dir,
os.path.normpath(os.path.join(tk_lib_dir, '..')),
"/usr/lib/tk" + str(Tkinter.TkVersion),
"/usr/lib"]
for ptcl, ptk in zip(tcl_poss, tk_poss):
tcl_config = os.path.join(ptcl, "tclConfig.sh")
tk_config = os.path.join(ptk, "tkConfig.sh")
if (os.path.exists(tcl_config) and os.path.exists(tk_config)):
break
if not (os.path.exists(tcl_config) and os.path.exists(tk_config)):
return None

def get_var(file, varname):
p = subprocess.Popen(
'. %s ; eval echo ${%s}' % (file, varname),
shell=True,
executable="/bin/sh",
stdout=subprocess.PIPE)
result = p.communicate()[0]
return result.decode('ascii')

tcl_lib_dir = get_var(
tcl_config, 'TCL_LIB_SPEC').split()[0][2:].strip()
tcl_inc_dir = get_var(
tcl_config, 'TCL_INCLUDE_SPEC')[2:].strip()
tcl_lib = get_var(tcl_config, 'TCL_LIB_FLAG')[2:].strip()

tk_lib_dir = get_var(tk_config, 'TK_LIB_SPEC').split()[0][2:].strip()
tk_inc_dir = get_var(tk_config, 'TK_INCLUDE_SPEC').strip()
if tk_inc_dir == '':
tk_inc_dir = tcl_inc_dir
else:
tk_inc_dir = tk_inc_dir[2:]
tk_lib = get_var(tk_config, 'TK_LIB_FLAG')[2:].strip()

if not os.path.exists(os.path.join(tk_inc_dir, 'tk.h')):
return None

return (tcl_lib_dir, tcl_inc_dir, tcl_lib,
tk_lib_dir, tk_inc_dir, tk_lib)

def guess_tcl_config(self, tcl_lib_dir, tk_lib_dir, tk_ver):
if not (os.path.exists(tcl_lib_dir) and os.path.exists(tk_lib_dir)):
return None

tcl_lib = os.path.normpath(os.path.join(tcl_lib_dir, '../'))
tk_lib = os.path.normpath(os.path.join(tk_lib_dir, '../'))

tcl_inc = os.path.normpath(
os.path.join(tcl_lib_dir,
'../../include/tcl' + tk_ver))
if not os.path.exists(tcl_inc):
tcl_inc = os.path.normpath(
os.path.join(tcl_lib_dir,
'../../include'))

tk_inc = os.path.normpath(os.path.join(
tk_lib_dir,
'../../include/tk' + tk_ver))
if not os.path.exists(tk_inc):
tk_inc = os.path.normpath(os.path.join(
tk_lib_dir,
'../../include'))

if not os.path.exists(os.path.join(tk_inc, 'tk.h')):
tk_inc = tcl_inc

if not os.path.exists(tcl_inc):
# this is a hack for suse linux, which is broken
if (sys.platform.startswith('linux') and
os.path.exists('/usr/include/tcl.h') and
os.path.exists('/usr/include/tk.h')):
tcl_inc = '/usr/include'
tk_inc = '/usr/include'

if not os.path.exists(os.path.join(tk_inc, 'tk.h')):
return None

return tcl_lib, tcl_inc, 'tcl' + tk_ver, tk_lib, tk_inc, 'tk' + tk_ver

def hardcoded_tcl_config(self):
tcl_inc = "/usr/local/include"
tk_inc = "/usr/local/include"
tcl_lib = "/usr/local/lib"
tk_lib = "/usr/local/lib"
return tcl_lib, tcl_inc, 'tcl', tk_lib, tk_inc, 'tk'

def add_flags(self, ext):
ext.include_dirs.extend(['src'])
if sys.platform == 'win32':
major, minor1, minor2, s, tmp = sys.version_info
if sys.version_info[0:2] < (3, 4):
ext.include_dirs.extend(['win32_static/include/tcl85'])
ext.libraries.extend(['tk85', 'tcl85'])
else:
ext.include_dirs.extend(['win32_static/include/tcl86'])
ext.libraries.extend(['tk86t', 'tcl86t'])
ext.library_dirs.extend([os.path.join(sys.prefix, 'dlls')])

elif sys.platform == 'darwin':
# this config section lifted directly from Imaging - thanks to
# the effbot!

# First test for a MacOSX/darwin framework install
from os.path import join, exists
framework_dirs = [
join(os.getenv('HOME'), '/Library/Frameworks'),
'/Library/Frameworks',
'/System/Library/Frameworks/',
]

# Find the directory that contains the Tcl.framework and
# Tk.framework bundles.
tk_framework_found = 0
for F in framework_dirs:
# both Tcl.framework and Tk.framework should be present
for fw in 'Tcl', 'Tk':
if not exists(join(F, fw + '.framework')):
break
else:
# ok, F is now directory with both frameworks. Continure
# building
tk_framework_found = 1
break
if tk_framework_found:
# For 8.4a2, we must add -I options that point inside
# the Tcl and Tk frameworks. In later release we
# should hopefully be able to pass the -F option to
# gcc, which specifies a framework lookup path.

tk_include_dirs = [
join(F, fw + '.framework', H)
for fw in ('Tcl', 'Tk')
for H in ('Headers', 'Versions/Current/PrivateHeaders')
]

# For 8.4a2, the X11 headers are not included. Rather
# than include a complicated search, this is a
# hard-coded path. It could bail out if X11 libs are
# not found...

# tk_include_dirs.append('/usr/X11R6/include')
frameworks = ['-framework', 'Tcl', '-framework', 'Tk']
ext.include_dirs.extend(tk_include_dirs)
ext.extra_link_args.extend(frameworks)
ext.extra_compile_args.extend(frameworks)

# you're still here? ok we'll try it this way...
else:
# There are 3 methods to try, in decreasing order of "smartness"
#
# 1. Parse the tclConfig.sh and tkConfig.sh files that have
# all the information we need
#
# 2. Guess the include and lib dirs based on the location of
# Tkinter's 'tcl_library' and 'tk_library' variables.
#
# 3. Use some hardcoded locations that seem to work on a lot
# of distros.

# Query Tcl/Tk system for library paths and version string
try:
tcl_lib_dir, tk_lib_dir, tk_ver = self.query_tcltk()
except:
tk_ver = ''
result = self.hardcoded_tcl_config()
else:
result = self.parse_tcl_config(tcl_lib_dir, tk_lib_dir)
if result is None:
result = self.guess_tcl_config(
tcl_lib_dir, tk_lib_dir, tk_ver)
if result is None:
result = self.hardcoded_tcl_config()

# Add final versions of directories and libraries to ext lists
(tcl_lib_dir, tcl_inc_dir, tcl_lib,
tk_lib_dir, tk_inc_dir, tk_lib) = result
ext.include_dirs.extend([tcl_inc_dir, tk_inc_dir])
ext.library_dirs.extend([tcl_lib_dir, tk_lib_dir])
ext.libraries.extend([tcl_lib, tk_lib])
# PSAPI library needed for finding Tcl / Tk at run time
ext.libraries.extend(['psapi'])


class BackendGtk(OptionalBackendPackage):
Expand Down Expand Up @@ -1701,8 +1436,6 @@ def check(self):
return super(BackendGtkAgg, self).check()
except:
raise
else:
BackendAgg.force = True

def get_package_data(self):
return {'matplotlib': ['mpl-data/*.glade']}
Expand Down Expand Up @@ -1778,7 +1511,6 @@ def check_requirements(self):
p.join()

if success:
BackendAgg.force = True
return msg
else:
raise CheckFailed(msg)
Expand Down Expand Up @@ -1851,7 +1583,6 @@ def check_requirements(self):
p.join()

if success:
BackendAgg.force = True
return msg
else:
raise CheckFailed(msg)
Expand Down Expand Up @@ -1895,8 +1626,6 @@ def check_requirements(self):
raise CheckFailed(
"Requires wxPython 2.8, found %s" % backend_version)

BackendAgg.force = True

return "version %s" % backend_version


Expand Down Expand Up @@ -1935,7 +1664,7 @@ def check_requirements(self):
config = self.get_config()
if config is False:
raise CheckFailed("skipping due to configuration")
return "installing"
return ""

def get_extension(self):
sources = [
Expand Down Expand Up @@ -2010,7 +1739,6 @@ def backend_pyside_internal_check(self):
except ImportError:
raise CheckFailed("PySide not found")
else:
BackendAgg.force = True
return ("Qt: %s, PySide: %s" %
(QtCore.__version__, __version__))

Expand All @@ -2027,7 +1755,6 @@ def backend_pyqt4_internal_check(self):
except AttributeError:
raise CheckFailed('PyQt4 not correctly imported')
else:
BackendAgg.force = True
return ("Qt: %s, PyQt: %s" % (self.convert_qt_version(qt_version), pyqt_version_str))


Expand Down Expand Up @@ -2069,7 +1796,6 @@ def backend_qt5_internal_check(self):
except AttributeError:
raise CheckFailed('PyQt5 not correctly imported')
else:
BackendAgg.force = True
return ("Qt: %s, PyQt: %s" % (self.convert_qt_version(qt_version), pyqt_version_str))


Expand Down
Loading

0 comments on commit b80e0f1

Please sign in to comment.