diff options
Diffstat (limited to 'development/python3-matplotlib/setupext.py')
-rw-r--r-- | development/python3-matplotlib/setupext.py | 837 |
1 files changed, 537 insertions, 300 deletions
diff --git a/development/python3-matplotlib/setupext.py b/development/python3-matplotlib/setupext.py index 5a1939fa125e9..68ab3684433eb 100644 --- a/development/python3-matplotlib/setupext.py +++ b/development/python3-matplotlib/setupext.py @@ -1,50 +1,60 @@ from __future__ import print_function, absolute_import +from importlib import import_module + from distutils import sysconfig from distutils import version from distutils.core import Extension +import distutils.command.build_ext import glob -import io import multiprocessing import os +import platform import re import subprocess +from subprocess import check_output import sys import warnings from textwrap import fill - +import shutil import versioneer PY3min = (sys.version_info[0] >= 3) -PY32min = (PY3min and sys.version_info[1] >= 2 or sys.version_info[0] > 3) - -try: - from subprocess import check_output -except ImportError: - # check_output is not available in Python 2.6 - def check_output(*popenargs, **kwargs): - """ - Run command with arguments and return its output as a byte - string. - Backported from Python 2.7 as it's implemented as pure python - on stdlib. - """ - process = subprocess.Popen( - stdout=subprocess.PIPE, *popenargs, **kwargs) - output, unused_err = process.communicate() - retcode = process.poll() - if retcode: - cmd = kwargs.get("args") - if cmd is None: - cmd = popenargs[0] - error = subprocess.CalledProcessError(retcode, cmd) - error.output = output - raise error - return output +def _get_xdg_cache_dir(): + """ + Return the XDG cache directory. + See https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + """ + cache_dir = os.environ.get('XDG_CACHE_HOME') + if not cache_dir: + cache_dir = os.path.expanduser('~/.cache') + if cache_dir.startswith('~/'): # Expansion failed. + return None + return os.path.join(cache_dir, 'matplotlib') + + +# SHA256 hashes of the FreeType tarballs +_freetype_hashes = { + '2.6.1': '0a3c7dfbda6da1e8fce29232e8e96d987ababbbf71ebc8c75659e4132c367014', + '2.6.2': '8da42fc4904e600be4b692555ae1dcbf532897da9c5b9fb5ebd3758c77e5c2d4', + '2.6.3': '7942096c40ee6fea882bd4207667ad3f24bff568b96b10fd3885e11a7baad9a3', + '2.6.4': '27f0e38347a1850ad57f84fc4dfed68ba0bc30c96a6fa6138ef84d485dd9a8d7', + '2.6.5': '3bb24add9b9ec53636a63ea8e867ed978c4f8fdd8f1fa5ccfd41171163d4249a', + '2.7': '7b657d5f872b0ab56461f3bd310bd1c5ec64619bd15f0d8e08282d494d9cfea4', + '2.7.1': '162ef25aa64480b1189cdb261228e6c5c44f212aac4b4621e28cf2157efb59f5', + '2.8': '33a28fabac471891d0523033e99c0005b95e5618dc8ffa7fa47f9dadcacb1c9b', + '2.8.1': '876711d064a6a1bd74beb18dd37f219af26100f72daaebd2d86cb493d7cd7ec6', +} +# This is the version of FreeType to use when building a local +# version. It must match the value in +# lib/matplotlib.__init__.py and also needs to be changed below in the +# embedded windows build script (grep for "REMINDER" in this file) +LOCAL_FREETYPE_VERSION = '2.6.1' +LOCAL_FREETYPE_HASH = _freetype_hashes.get(LOCAL_FREETYPE_VERSION, 'unknown') if sys.platform != 'win32': if not PY3min: @@ -70,31 +80,31 @@ options = { setup_cfg = os.environ.get('MPLSETUPCFG', 'setup.cfg') if os.path.exists(setup_cfg): - if PY32min: + if PY3min: config = configparser.ConfigParser() else: config = configparser.SafeConfigParser() config.read(setup_cfg) - try: + if config.has_option('status', 'suppress'): options['display_status'] = not config.getboolean("status", "suppress") - except: - pass - try: + if config.has_option('rc_options', 'backend'): options['backend'] = config.get("rc_options", "backend") - except: - pass - try: + if config.has_option('directories', 'basedirlist'): options['basedirlist'] = [ x.strip() for x in config.get("directories", "basedirlist").split(',')] - except: - pass + + if config.has_option('test', 'local_freetype'): + options['local_freetype'] = config.getboolean("test", "local_freetype") else: config = None +lft = bool(os.environ.get('MPLLOCALFREETYPE', False)) +options['local_freetype'] = lft or options.get('local_freetype', False) + def get_win32_compiler(): """ @@ -127,7 +137,8 @@ def has_include_file(include_dirs, filename): directories in `include_dirs`. """ if sys.platform == 'win32': - include_dirs += os.environ.get('INCLUDE', '.').split(';') + include_dirs = list(include_dirs) # copy before modify + include_dirs += os.environ.get('INCLUDE', '.').split(os.pathsep) for dir in include_dirs: if os.path.exists(os.path.join(dir, filename)): return True @@ -152,8 +163,21 @@ def get_base_dirs(): if options['basedirlist']: return options['basedirlist'] + if os.environ.get('MPLBASEDIRLIST'): + return os.environ.get('MPLBASEDIRLIST').split(os.pathsep) + + win_bases = ['win32_static', ] + # on conda windows, we also add the <conda_env_dir>\Library, + # as conda installs libs/includes there + # env var names mess: https://github.com/conda/conda/issues/2312 + conda_env_path = os.getenv('CONDA_PREFIX') # conda >= 4.1 + if not conda_env_path: + conda_env_path = os.getenv('CONDA_DEFAULT_ENV') # conda < 4.1 + if conda_env_path and os.path.isdir(conda_env_path): + win_bases.append(os.path.join(conda_env_path, "Library")) + basedir_map = { - 'win32': ['win32_static', ], + 'win32': win_bases, 'darwin': ['/usr/local/', '/usr', '/usr/X11', '/opt/X11', '/opt/local'], 'sunos5': [os.getenv('MPLIB_BASE') or '/usr/local', ], @@ -168,8 +192,11 @@ def get_include_dirs(): Returns a list of standard include directories on this platform. """ include_dirs = [os.path.join(d, 'include') for d in get_base_dirs()] - include_dirs.extend( - os.environ.get('CPLUS_INCLUDE_PATH', '').split(os.pathsep)) + if sys.platform != 'win32': + # gcc includes this dir automatically, so also look for headers in + # these dirs + include_dirs.extend( + os.environ.get('CPLUS_INCLUDE_PATH', '').split(os.pathsep)) return include_dirs @@ -209,8 +236,8 @@ else: print_status = print_message = print_raw = print_line -# Remove the -Wstrict-prototypesoption, is it's not valid for C++ -customize_compiler = sysconfig.customize_compiler +# Remove the -Wstrict-prototypes option, is it's not valid for C++ +customize_compiler = distutils.command.build_ext.customize_compiler def my_customize_compiler(compiler): @@ -221,7 +248,7 @@ def my_customize_compiler(compiler): pass return retval -sysconfig.customize_compiler = my_customize_compiler +distutils.command.build_ext.customize_compiler = my_customize_compiler def make_extension(name, files, *args, **kwargs): @@ -251,6 +278,21 @@ def make_extension(name, files, *args, **kwargs): return ext +def get_file_hash(filename): + """ + Get the SHA256 hash of a given filename. + """ + import hashlib + BLOCKSIZE = 1 << 16 + hasher = hashlib.sha256() + with open(filename, 'rb') as fd: + buf = fd.read(BLOCKSIZE) + while len(buf) > 0: + hasher.update(buf) + buf = fd.read(BLOCKSIZE) + return hasher.hexdigest() + + class PkgConfig(object): """ This is a class for communicating with pkg-config. @@ -364,16 +406,31 @@ class CheckFailed(Exception): class SetupPackage(object): optional = False + pkg_names = { + "apt-get": None, + "yum": None, + "dnf": None, + "brew": None, + "port": None, + "windows_url": None + } def check(self): """ - Checks whether the dependencies are met. Should raise a - `CheckFailed` exception if the dependency could not be met, - otherwise return a string indicating a version number or some - other message indicating what was found. + Checks whether the build dependencies are met. Should raise a + `CheckFailed` exception if the dependency could not be met, otherwise + return a string indicating a version number or some other message + indicating what was found. """ pass + def runtime_check(self): + """ + True if the runtime dependencies of the backend are met. Assumes that + the build-time dependencies are met. + """ + return True + def get_packages(self): """ Get a list of package names to add to the configuration. @@ -475,11 +532,76 @@ class SetupPackage(object): return 'version %s' % version + def do_custom_build(self): + """ + If a package needs to do extra custom things, such as building a + third-party library, before building an extension, it should + override this method. + """ + pass + + def install_help_msg(self): + """ + Do not override this method ! + + Generate the help message to show if the package is not installed. + To use this in subclasses, simply add the dictionary `pkg_names` as + a class variable: + + pkg_names = { + "apt-get": <Name of the apt-get package>, + "yum": <Name of the yum package>, + "dnf": <Name of the dnf package>, + "brew": <Name of the brew package>, + "port": <Name of the port package>, + "windows_url": <The url which has installation instructions> + } + + All the dictionary keys are optional. If a key is not present or has + the value `None` no message is provided for that platform. + """ + def _try_managers(*managers): + for manager in managers: + pkg_name = self.pkg_names.get(manager, None) + if pkg_name: + try: + # `shutil.which()` can be used when Python 2.7 support + # is dropped. It is available in Python 3.3+ + _ = check_output(["which", manager], + stderr=subprocess.STDOUT) + if manager == 'port': + pkgconfig = 'pkgconfig' + else: + pkgconfig = 'pkg-config' + return ('Try installing {0} with `{1} install {2}` ' + 'and pkg-config with `{1} install {3}`' + .format(self.name, manager, pkg_name, + pkgconfig)) + except subprocess.CalledProcessError: + pass + + message = None + if sys.platform == "win32": + url = self.pkg_names.get("windows_url", None) + if url: + message = ('Please check {0} for instructions to install {1}' + .format(url, self.name)) + elif sys.platform == "darwin": + message = _try_managers("brew", "port") + elif sys.platform.startswith("linux"): + release = platform.linux_distribution()[0].lower() + if release in ('debian', 'ubuntu'): + message = _try_managers('apt-get') + elif release in ('centos', 'redhat', 'fedora'): + message = _try_managers('dnf', 'yum') + return message + class OptionalPackage(SetupPackage): optional = True force = False config_category = "packages" + default_config = "auto" @classmethod def get_config(cls): @@ -489,7 +611,7 @@ class OptionalPackage(SetupPackage): insensitively defined as 1, true, yes, on for True) or opted-out (case insensitively defined as 0, false, no, off for False). """ - conf = "auto" + conf = cls.default_config if config is not None and config.has_option(cls.config_category, cls.name): try: conf = config.getboolean(cls.config_category, cls.name) @@ -560,13 +682,13 @@ class Python(SetupPackage): if major < 2: raise CheckFailed( - "Requires Python 2.6 or later") - elif major == 2 and minor1 < 6: + "Requires Python 2.7 or later") + elif major == 2 and minor1 < 7: raise CheckFailed( - "Requires Python 2.6 or later (in the 2.x series)") - elif major == 3 and minor1 < 1: + "Requires Python 2.7 or later (in the 2.x series)") + elif major == 3 and minor1 < 4: raise CheckFailed( - "Requires Python 3.1 or later (in the 3.x series)") + "Requires Python 3.4 or later (in the 3.x series)") return sys.version @@ -588,8 +710,11 @@ class Matplotlib(SetupPackage): 'matplotlib.sphinxext', 'matplotlib.style', 'matplotlib.testing', + 'matplotlib.testing._nose', + 'matplotlib.testing._nose.plugins', 'matplotlib.testing.jpl_units', 'matplotlib.tri', + 'matplotlib.cbook' ] def get_py_modules(self): @@ -616,6 +741,7 @@ class Matplotlib(SetupPackage): 'mpl-data/example/*.npy', 'mpl-data/matplotlibrc', 'backends/web_backend/*.*', + 'backends/web_backend/js/*.*', 'backends/web_backend/jquery/js/*.min.js', 'backends/web_backend/jquery/css/themes/base/*.min.css', 'backends/web_backend/jquery/css/themes/base/images/*', @@ -659,29 +785,30 @@ class Toolkits(OptionalPackage): class Tests(OptionalPackage): name = "tests" - nose_min_version = '0.11.1' + pytest_min_version = '3.1' + default_config = False def check(self): super(Tests, self).check() msgs = [] - msg_template = ('{package} is required to run the matplotlib test ' - 'suite. Please install it with pip or your preferred' - ' tool to run the test suite') + msg_template = ('{package} is required to run the Matplotlib test ' + 'suite. Please install it with pip or your preferred ' + 'tool to run the test suite') - bad_nose = msg_template.format( - package='nose %s or later' % self.nose_min_version + bad_pytest = msg_template.format( + package='pytest %s or later' % self.pytest_min_version ) try: - import nose - if is_min_version(nose.__version__, self.nose_min_version): - msgs += ['using nose version %s' % nose.__version__] + import pytest + if is_min_version(pytest.__version__, self.pytest_min_version): + msgs += ['using pytest version %s' % pytest.__version__] else: - msgs += [bad_nose] + msgs += [bad_pytest] except ImportError: - msgs += [bad_nose] + msgs += [bad_pytest] - if sys.version_info >= (3, 3): + if PY3min: msgs += ['using unittest.mock'] else: try: @@ -707,6 +834,7 @@ class Tests(OptionalPackage): 'matplotlib': baseline_images + [ + 'tests/cmr10.pfb', 'tests/mpltest.ttf', 'tests/test_rcparams.rc', 'tests/test_utf32_be_rcparams.rc', @@ -805,7 +933,7 @@ class Numpy(SetupPackage): @staticmethod def include_dirs_hook(): - if sys.version_info[0] >= 3: + if PY3min: import builtins if hasattr(builtins, '__NUMPY_SETUP__'): del builtins.__NUMPY_SETUP__ @@ -834,11 +962,11 @@ class Numpy(SetupPackage): try: import numpy except ImportError: - raise CheckFailed( - "could not be found" ) + return 'not found. pip may install it below.' + if not is_min_version(numpy.__version__, min_version): - raise CheckFailed( - "requires numpy %s or later to build. (Found %s)" % + raise SystemExit( + "Requires numpy %s or later to build. (Found %s)" % (min_version, numpy.__version__)) return 'version %s' % numpy.__version__ @@ -854,11 +982,14 @@ class Numpy(SetupPackage): ext.define_macros.append(('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION')) + # Allow NumPy's printf format specifiers in C++. + ext.define_macros.append(('__STDC_FORMAT_MACROS', 1)) + def get_setup_requires(self): - return ['numpy>=1.6'] + return ['numpy>=1.7.1'] def get_install_requires(self): - return ['numpy>=1.6'] + return ['numpy>=1.7.1'] class LibAgg(SetupPackage): @@ -877,7 +1008,7 @@ class LibAgg(SetupPackage): if self.found_external: pkg_config.setup_extension(ext, 'libagg') else: - ext.include_dirs.append('extern/agg24-svn/include') + ext.include_dirs.insert(0, 'extern/agg24-svn/include') if add_sources: agg_sources = [ 'agg_bezier_arc.cpp', @@ -895,10 +1026,24 @@ class LibAgg(SetupPackage): class FreeType(SetupPackage): name = "freetype" + pkg_names = { + "apt-get": "libfreetype6-dev", + "yum": "freetype-devel", + "dnf": "freetype-devel", + "brew": "freetype", + "port": "freetype", + "windows_url": "http://gnuwin32.sourceforge.net/packages/freetype.htm" + } def check(self): + if options.get('local_freetype'): + return "Using local version for testing" + if sys.platform == 'win32': - check_include_file(get_include_dirs(), 'ft2build.h', 'freetype') + try: + check_include_file(get_include_dirs(), 'ft2build.h', 'freetype') + except CheckFailed: + check_include_file(get_include_dirs(), 'freetype2\\ft2build.h', 'freetype') return 'Using unknown version found on system.' status, output = getstatusoutput("freetype-config --ftversion") @@ -942,15 +1087,171 @@ class FreeType(SetupPackage): return '.'.join([major, minor, patch]) def add_flags(self, ext): - pkg_config.setup_extension( - ext, 'freetype2', - default_include_dirs=[ - 'include/freetype2', 'freetype2', - 'lib/freetype2/include', - 'lib/freetype2/include/freetype2'], - default_library_dirs=[ - 'freetype2/lib'], - default_libraries=['freetype', 'z']) + if options.get('local_freetype'): + src_path = os.path.join( + 'build', 'freetype-{0}'.format(LOCAL_FREETYPE_VERSION)) + # Statically link to the locally-built freetype. + # This is certainly broken on Windows. + ext.include_dirs.insert(0, os.path.join(src_path, 'include')) + if sys.platform == 'win32': + libfreetype = 'libfreetype.lib' + else: + libfreetype = 'libfreetype.a' + ext.extra_objects.insert( + 0, os.path.join(src_path, 'objs', '.libs', libfreetype)) + ext.define_macros.append(('FREETYPE_BUILD_TYPE', 'local')) + else: + pkg_config.setup_extension( + ext, 'freetype2', + default_include_dirs=[ + 'include/freetype2', 'freetype2', + 'lib/freetype2/include', + 'lib/freetype2/include/freetype2'], + default_library_dirs=[ + 'freetype2/lib'], + default_libraries=['freetype', 'z']) + ext.define_macros.append(('FREETYPE_BUILD_TYPE', 'system')) + + def do_custom_build(self): + # We're using a system freetype + if not options.get('local_freetype'): + return + + src_path = os.path.join( + 'build', 'freetype-{0}'.format(LOCAL_FREETYPE_VERSION)) + + # We've already built freetype + if sys.platform == 'win32': + libfreetype = 'libfreetype.lib' + else: + libfreetype = 'libfreetype.a' + + if os.path.isfile(os.path.join(src_path, 'objs', '.libs', libfreetype)): + return + + tarball = 'freetype-{0}.tar.gz'.format(LOCAL_FREETYPE_VERSION) + tarball_path = os.path.join('build', tarball) + try: + tarball_cache_dir = _get_xdg_cache_dir() + tarball_cache_path = os.path.join(tarball_cache_dir, tarball) + except: + # again, do not really care if this fails + tarball_cache_dir = None + tarball_cache_path = None + if not os.path.isfile(tarball_path): + if (tarball_cache_path is not None and + os.path.isfile(tarball_cache_path)): + if get_file_hash(tarball_cache_path) == LOCAL_FREETYPE_HASH: + try: + os.makedirs('build') + except OSError: + # Don't care if it exists. + pass + try: + shutil.copy(tarball_cache_path, tarball_path) + print('Using cached tarball: {}' + .format(tarball_cache_path)) + except OSError: + # If this fails, oh well just re-download + pass + + if not os.path.isfile(tarball_path): + if PY3min: + from urllib.request import urlretrieve + else: + from urllib import urlretrieve + + if not os.path.exists('build'): + os.makedirs('build') + + url_fmts = [ + 'https://downloads.sourceforge.net/project/freetype' + '/freetype2/{version}/{tarball}', + 'https://download.savannah.gnu.org/releases/freetype' + '/{tarball}' + ] + for url_fmt in url_fmts: + tarball_url = url_fmt.format( + version=LOCAL_FREETYPE_VERSION, tarball=tarball) + + print("Downloading {0}".format(tarball_url)) + try: + urlretrieve(tarball_url, tarball_path) + except IOError: # URLError (a subclass) on Py3. + print("Failed to download {0}".format(tarball_url)) + else: + if get_file_hash(tarball_path) != LOCAL_FREETYPE_HASH: + print("Invalid hash.") + else: + break + else: + raise IOError("Failed to download freetype. " + "You can download the file by " + "alternative means and copy it " + " to '{0}'".format(tarball_path)) + try: + os.makedirs(tarball_cache_dir) + except OSError: + # Don't care if it exists. + pass + try: + shutil.copy(tarball_path, tarball_cache_path) + print('Cached tarball at: {}'.format(tarball_cache_path)) + except OSError: + # If this fails, we can always re-download. + pass + + if get_file_hash(tarball_path) != LOCAL_FREETYPE_HASH: + raise IOError( + "{0} does not match expected hash.".format(tarball)) + + print("Building {0}".format(tarball)) + if sys.platform != 'win32': + # compilation on all other platforms than windows + cflags = 'CFLAGS="{0} -fPIC" '.format(os.environ.get('CFLAGS', '')) + + subprocess.check_call( + ['tar', 'zxf', tarball], cwd='build') + subprocess.check_call( + [cflags + './configure --with-zlib=no --with-bzip2=no ' + '--with-png=no --with-harfbuzz=no'], shell=True, cwd=src_path) + subprocess.check_call( + [cflags + 'make'], shell=True, cwd=src_path) + else: + # compilation on windows + FREETYPE_BUILD_CMD = """\ +call "%ProgramFiles%\\Microsoft SDKs\\Windows\\v7.0\\Bin\\SetEnv.Cmd" /Release /{xXX} /xp +call "{vcvarsall}" {xXX} +set MSBUILD=C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\MSBuild.exe +rd /S /Q %FREETYPE%\\objs +%MSBUILD% %FREETYPE%\\builds\\windows\\{vc20xx}\\freetype.sln /t:Clean;Build /p:Configuration="{config}";Platform={WinXX} +echo Build completed, moving result" +:: move to the "normal" path for the unix builds... +mkdir %FREETYPE%\\objs\\.libs +:: REMINDER: fix when changing the version +copy %FREETYPE%\\objs\\{vc20xx}\\{xXX}\\freetype261.lib %FREETYPE%\\objs\\.libs\\libfreetype.lib +if errorlevel 1 ( + rem This is a py27 version, which has a different location for the lib file :-/ + copy %FREETYPE%\\objs\\win32\\{vc20xx}\\freetype261.lib %FREETYPE%\\objs\\.libs\\libfreetype.lib +) +""" + from setup_external_compile import fixproj, prepare_build_cmd, VS2010, X64, tar_extract + # Note: freetype has no build profile for 2014, so we don't bother... + vc = 'vc2010' if VS2010 else 'vc2008' + WinXX = 'x64' if X64 else 'Win32' + tar_extract(tarball_path, "build") + # This is only false for py2.7, even on py3.5... + if not VS2010: + fixproj(os.path.join(src_path, 'builds', 'windows', vc, 'freetype.sln'), WinXX) + fixproj(os.path.join(src_path, 'builds', 'windows', vc, 'freetype.vcproj'), WinXX) + + cmdfile = os.path.join("build", 'build_freetype.cmd') + with open(cmdfile, 'w') as cmd: + cmd.write(prepare_build_cmd(FREETYPE_BUILD_CMD, vc20xx=vc, WinXX=WinXX, + config='Release' if VS2010 else 'LIB Release')) + + os.environ['FREETYPE'] = src_path + subprocess.check_call([cmdfile], shell=True) class FT2Font(SetupPackage): @@ -970,6 +1271,14 @@ class FT2Font(SetupPackage): class Png(SetupPackage): name = "png" + pkg_names = { + "apt-get": "libpng12-dev", + "yum": "libpng-devel", + "dnf": "libpng-devel", + "brew": "libpng", + "port": "libpng", + "windows_url": "http://gnuwin32.sourceforge.net/packages/libpng.htm" + } def check(self): if sys.platform == 'win32': @@ -1011,27 +1320,21 @@ class Qhull(SetupPackage): self.__class__.found_external = True try: return self._check_for_pkg_config( - 'qhull', 'qhull/qhull_a.h', min_version='2003.1') + 'libqhull', 'libqhull/qhull_a.h', min_version='2015.2') except CheckFailed as e: self.__class__.found_pkgconfig = False - # Qhull may not be in the pkg-config system but may still be - # present on this system, so check if the header files can be - # found. - include_dirs = [ - os.path.join(x, 'qhull') for x in get_include_dirs()] - if has_include_file(include_dirs, 'qhull_a.h'): - return 'Using system Qhull (version unknown, no pkg-config info)' - else: - self.__class__.found_external = False - return str(e) + ' Using local copy.' + self.__class__.found_external = False + return str(e) + ' Using local copy.' def add_flags(self, ext): if self.found_external: pkg_config.setup_extension(ext, 'qhull', default_libraries=['qhull']) else: - ext.include_dirs.append('extern') - ext.sources.extend(glob.glob('extern/qhull/*.c')) + ext.include_dirs.insert(0, 'extern') + ext.sources.extend(sorted(glob.glob('extern/libqhull/*.c'))) + if sysconfig.get_config_var('LIBM') == '-lm': + ext.libraries.extend('m') class TTConv(SetupPackage): @@ -1046,7 +1349,7 @@ class TTConv(SetupPackage): ] ext = make_extension('matplotlib.ttconv', sources) Numpy().add_flags(ext) - ext.include_dirs.append('extern') + ext.include_dirs.insert(0, 'extern') return ext @@ -1072,23 +1375,13 @@ class Image(SetupPackage): sources = [ 'src/_image.cpp', 'src/mplutils.cpp', - 'src/_image_wrapper.cpp' + 'src/_image_wrapper.cpp', + 'src/py_converters.cpp' ] ext = make_extension('matplotlib._image', sources) Numpy().add_flags(ext) LibAgg().add_flags(ext) - return ext - -class ContourLegacy(SetupPackage): - name = "contour_legacy" - - def get_extension(self): - sources = [ - "src/cntr.c" - ] - ext = make_extension('matplotlib._cntr', sources) - Numpy().add_flags(ext) return ext @@ -1105,21 +1398,6 @@ class Contour(SetupPackage): return ext -class Delaunay(SetupPackage): - name = "delaunay" - - def get_packages(self): - return ['matplotlib.delaunay'] - - def get_extension(self): - sources = ["_delaunay.cpp", "VoronoiDiagramGenerator.cpp", - "delaunay_utils.cpp", "natneighbors.cpp"] - sources = [os.path.join('lib/matplotlib/delaunay', s) for s in sources] - ext = make_extension('matplotlib._delaunay', sources) - Numpy().add_flags(ext) - return ext - - class QhullWrap(SetupPackage): name = "qhull_wrap" @@ -1146,134 +1424,77 @@ class Tri(SetupPackage): return ext -class Externals(SetupPackage): - name = "externals" - - def get_packages(self): - return ['matplotlib.externals'] - - -class Pytz(SetupPackage): - name = "pytz" +class InstallRequires(SetupPackage): + name = "install_requires" def check(self): - try: - import pytz - except ImportError: - raise CheckFailed( - "could not be found") - - return "using pytz version %s" % pytz.__version__ - - def get_install_requires(self): - return ['pytz'] - - -class Cycler(SetupPackage): - name = "cycler" - - def check(self): - try: - import cycler - except ImportError: - raise CheckFailed( - "could not be found") - - return "using cycler version %s" % cycler.__version__ - - def get_install_requires(self): - return ['cycler'] - - -class Dateutil(SetupPackage): - name = "dateutil" - - def __init__(self, version=None): - self.version = version - - def check(self): - try: - import dateutil - except ImportError: - # dateutil 2.1 has a file encoding bug that breaks installation on - # python 3.3 - # https://github.com/matplotlib/matplotlib/issues/2373 - # hack around the problem by installing the (working) v2.0 - #major, minor1, _, _, _ = sys.version_info - #if self.version is None and (major, minor1) == (3, 3): - #self.version = '!=2.1' - - raise CheckFailed ( - "could not be found") - - major, minor1, _, _, _ = sys.version_info - if dateutil.__version__ == '2.1' and (major, minor1) == (3, 3): - raise CheckFailed ( - "dateutil v. 2.1 has a bug that breaks installation" - "on python 3.3.x, use another dateutil version") - return "using dateutil version %s" % dateutil.__version__ - - def get_install_requires(self): - dateutil = 'python-dateutil' - if self.version is not None: - dateutil += self.version - return [dateutil] - - -class Tornado(SetupPackage): - name = "tornado" - - def check(self): - try: - import tornado - except ImportError: - raise CheckFailed ( - "could not be found") - - return "using tornado version %s" % tornado.version - - -class Pyparsing(SetupPackage): - name = "pyparsing" - # pyparsing 2.0.4 has broken python 3 support. - # pyparsing 2.1.2 is broken in python3.4/3.3. - def is_ok(self): - # pyparsing 2.0.0 bug, but it may be patched in distributions - try: - import pyparsing - f = pyparsing.Forward() - f <<= pyparsing.Literal('a') - return f is not None - except (ImportError, TypeError): - return False - - def check(self): - try: - import pyparsing - except ImportError: - raise CheckFailed( - "could not be found") - - required = [1, 5, 6] - if [int(x) for x in pyparsing.__version__.split('.')] < required: - raise CheckFailed( - "matplotlib requires pyparsing >= {0}".format( - '.'.join(str(x) for x in required))) - - if not self.is_ok(): - return ( - "Your pyparsing contains a bug that will be monkey-patched by " - "matplotlib. For best results, upgrade to pyparsing 2.0.1 or " - "later.") - - return "using pyparsing version %s" % pyparsing.__version__ + not_available = [] + wrong_version = [] + inst_req = self.get_install_requires() + for pack_inf in inst_req: + pack_inf_disp = pack_inf.split('>=') + if 'dateutil' in pack_inf_disp[0]: + pack_inf_disp[0] = 'dateutil' + pack_name = pack_inf_disp[0] + try: + import_module(pack_name) + if pack_name != pack_inf_disp[-1]: + # This means that we have to check for the version + pack_ver = sys.modules[pack_name].__version__ + pack_ver = [int(ele) for ele in pack_ver.split('.')] + ver_cond = pack_inf_disp[1].split(',!=') + # Check for minimum version + if pack_ver < [int(ele) for ele in ver_cond[0].split('.')]: + if len(ver_cond[1:]) > 0: + wrong_version.append(pack_name + +" is not at least at version " + +ver_cond[0]+os.linesep + +"Please upgrade!"+os.linesep + +"WARNING: Version(s) " + +", ".join(ver_cond[1:]) + +" have issues and must be " + +"avoided.") + else: + wrong_version.append(pack_name + +" is not at least at version " + +ver_cond[0]+os.linesep + +"Please upgrade!") + continue + # Check for forbidden versions if any + for ver in ver_cond[1:]: + if pack_ver == [int(ele) for ele in ver.split('.')]: + wrong_version.append(pack_name+" is at version " + +ver+" which is not allowed."+os.linesep + +"Please use a version newer than "+ver_cond[0] + +" but different from "+", ".join(ver_cond[1:])) + break + except ImportError: + not_available.append(pack_name+" could not be found") + if not_available or wrong_version: + sp_mult = min(1, len(wrong_version)) + req_fail_msg = "ERROR: At least one third-party python package " + \ + "is missing or has the wrong version:" + os.linesep + req_fail_msg += (os.linesep.join(not_available) + + os.linesep*(2*sp_mult)) * min(1, len(not_available)) + req_fail_msg += (os.linesep*2).join(wrong_version) + print_status(package.name, req_fail_msg) + raise CheckFailed("missing or faulty third-party python packages") + return "all third-party python packages are present" def get_install_requires(self): - versionstring = 'pyparsing>=1.5.6,!=2.0.4,!=2.1.2' - if self.is_ok(): - return [versionstring] - else: - return [versionstring + ',!=2.0.0'] + install_requires = [ + "cycler>=0.10", + "pyparsing>=2.0.1,!=2.0.4,!=2.1.2,!=2.1.6", + "python-dateutil>=2.1", + "pytz", + "six>=1.10", + "kiwisolver>=1.0.1", + ] + if sys.version_info < (3,): + install_requires += ["backports.functools_lru_cache"] + if sys.version_info < (3,) and os.name == "posix": + install_requires += ["subprocess32"] + return install_requires class BackendAgg(OptionalBackendPackage): @@ -1301,23 +1522,33 @@ class BackendTkAgg(OptionalBackendPackage): def check(self): return "installing; run-time loading from Python Tcl / Tk" + def runtime_check(self): + """ Checks whether TkAgg runtime dependencies are met + """ + pkg_name = 'tkinter' if PY3min else 'Tkinter' + try: + import_module(pkg_name) + except ImportError: + return False + return True + def get_extension(self): sources = [ - 'src/py_converters.cpp', 'src/_tkagg.cpp' ] ext = make_extension('matplotlib.backends._tkagg', sources) self.add_flags(ext) - Numpy().add_flags(ext) LibAgg().add_flags(ext, add_sources=False) return ext def add_flags(self, ext): - ext.include_dirs.extend(['src']) + ext.include_dirs.insert(0, 'src') if sys.platform == 'win32': # PSAPI library needed for finding Tcl / Tk at run time ext.libraries.extend(['psapi']) + elif sys.platform.startswith('linux'): + ext.libraries.extend(['dl']) class BackendGtk(OptionalBackendPackage): @@ -1372,7 +1603,7 @@ class BackendGtk(OptionalBackendPackage): # If Gtk+ is installed, pkg-config is required to be installed os.environ['PKG_CONFIG_PATH'] = 'C:\\GTK\\lib\\pkgconfig' - # popen broken on my win32 plaform so I can't use pkgconfig + # popen broken on my win32 platform so I can't use pkgconfig ext.library_dirs.extend( ['C:/GTK/bin', 'C:/GTK/lib']) @@ -1426,12 +1657,6 @@ class BackendGtk(OptionalBackendPackage): class BackendGtkAgg(BackendGtk): name = "gtkagg" - def check(self): - try: - return super(BackendGtkAgg, self).check() - except: - raise - def get_package_data(self): return {'matplotlib': ['mpl-data/*.glade']} @@ -1491,7 +1716,7 @@ class BackendGtk3Agg(OptionalBackendPackage): success, msg = res.get(timeout=10)[0] except multiprocessing.TimeoutError: p.terminate() - # No result returned. Probaly hanging, terminate the process. + # No result returned. Probably hanging, terminate the process. success = False raise CheckFailed("Check timed out") except: @@ -1565,7 +1790,7 @@ class BackendGtk3Cairo(OptionalBackendPackage): success, msg = res.get(timeout=10)[0] except multiprocessing.TimeoutError: p.terminate() - # No result returned. Probaly hanging, terminate the process. + # No result returned. Probably hanging, terminate the process. success = False raise CheckFailed("Check timed out") except: @@ -1603,7 +1828,7 @@ class BackendWxAgg(OptionalBackendPackage): _wx_ensure_failed = wxversion.VersionError try: - wxversion.ensureMinimal('2.8') + wxversion.ensureMinimal('2.9') except _wx_ensure_failed: pass @@ -1613,13 +1838,9 @@ class BackendWxAgg(OptionalBackendPackage): except ImportError: raise CheckFailed("requires wxPython") - # Extra version check in case wxversion lacks AlreadyImportedError; - # then VersionError might have been raised and ignored when - # there really *is* a problem with the version. - major, minor = [int(n) for n in backend_version.split('.')[:2]] - if major < 2 or (major < 3 and minor < 8): + if not is_min_version(backend_version, "2.9"): raise CheckFailed( - "Requires wxPython 2.8, found %s" % backend_version) + "Requires wxPython 2.9, found %s" % backend_version) return "version %s" % backend_version @@ -1635,14 +1856,10 @@ class BackendMacOSX(OptionalBackendPackage): def get_extension(self): sources = [ - 'src/_macosx.m', - 'src/py_converters.cpp', - 'src/path_cleanup.cpp' + 'src/_macosx.m' ] ext = make_extension('matplotlib.backends._macosx', sources) - Numpy().add_flags(ext) - LibAgg().add_flags(ext) ext.extra_link_args.extend(['-framework', 'Cocoa']) return ext @@ -1692,17 +1909,14 @@ class BackendQtBase(OptionalBackendPackage): p = multiprocessing.Pool() except: - # Can't do multiprocessing, fall back to normal approach ( this will fail if importing both PyQt4 and PyQt5 ) + # Can't do multiprocessing, fall back to normal approach + # (this will fail if importing both PyQt4 and PyQt5). try: # Try in-process msg = self.callback(self) - except RuntimeError: - raise CheckFailed("Could not import: are PyQt4 & PyQt5 both installed?") - - except: - # Raise any other exceptions - raise + raise CheckFailed( + "Could not import: are PyQt4 & PyQt5 both installed?") else: # Multiprocessing OK @@ -1711,7 +1925,7 @@ class BackendQtBase(OptionalBackendPackage): msg = res.get(timeout=10)[0] except multiprocessing.TimeoutError: p.terminate() - # No result returned. Probaly hanging, terminate the process. + # No result returned. Probably hanging, terminate the process. raise CheckFailed("Check timed out") except: # Some other error. @@ -1746,7 +1960,7 @@ def backend_pyqt4_internal_check(self): try: qt_version = QtCore.QT_VERSION - pyqt_version_str = QtCore.QT_VERSION_STR + pyqt_version_str = QtCore.PYQT_VERSION_STR except AttributeError: raise CheckFailed('PyQt4 not correctly imported') else: @@ -1778,8 +1992,17 @@ class BackendQt4(BackendQtBase): BackendQtBase.__init__(self, *args, **kwargs) self.callback = backend_qt4_internal_check +def backend_pyside2_internal_check(self): + try: + from PySide2 import __version__ + from PySide2 import QtCore + except ImportError: + raise CheckFailed("PySide2 not found") + else: + return ("Qt: %s, PySide2: %s" % + (QtCore.__version__, __version__)) -def backend_qt5_internal_check(self): +def backend_pyqt5_internal_check(self): try: from PyQt5 import QtCore except ImportError: @@ -1787,12 +2010,28 @@ def backend_qt5_internal_check(self): try: qt_version = QtCore.QT_VERSION - pyqt_version_str = QtCore.QT_VERSION_STR + pyqt_version_str = QtCore.PYQT_VERSION_STR except AttributeError: raise CheckFailed('PyQt5 not correctly imported') else: return ("Qt: %s, PyQt: %s" % (self.convert_qt_version(qt_version), pyqt_version_str)) +def backend_qt5_internal_check(self): + successes = [] + failures = [] + try: + successes.append(backend_pyside2_internal_check(self)) + except CheckFailed as e: + failures.append(str(e)) + + try: + successes.append(backend_pyqt5_internal_check(self)) + except CheckFailed as e: + failures.append(str(e)) + + if len(successes) == 0: + raise CheckFailed('; '.join(failures)) + return '; '.join(successes + failures) class BackendQt5(BackendQtBase): name = "qt5agg" @@ -1837,23 +2076,21 @@ class Ghostscript(SetupPackage): optional = True def check(self): - try: - if sys.platform == 'win32': - command = 'gswin32c --version' - try: - output = check_output(command, shell=True, - stderr=subprocess.STDOUT) - except subprocess.CalledProcessError: - command = 'gswin64c --version' - output = check_output(command, shell=True, - stderr=subprocess.STDOUT) - else: - command = 'gs --version' + if sys.platform == 'win32': + # mgs is the name in miktex + gs_execs = ['gswin32c', 'gswin64c', 'mgs', 'gs'] + else: + gs_execs = ['gs'] + for gs_exec in gs_execs: + try: + command = gs_exec + ' --version' output = check_output(command, shell=True, stderr=subprocess.STDOUT) - return "version %s" % output.decode()[:-1] - except (IndexError, ValueError, subprocess.CalledProcessError): - raise CheckFailed() + return "version %s" % output.decode()[:-1] + except (IndexError, ValueError, subprocess.CalledProcessError): + pass + + raise CheckFailed() class LaTeX(SetupPackage): @@ -1865,7 +2102,7 @@ class LaTeX(SetupPackage): output = check_output('latex -version', shell=True, stderr=subprocess.STDOUT) line = output.splitlines()[0].decode() - pattern = '(3\.1\d+)|(MiKTeX \d+.\d+)' + pattern = r'(3\.1\d+)|(MiKTeX \d+.\d+)' match = re.search(pattern, line) return "version %s" % match.group(0) except (IndexError, ValueError, AttributeError, subprocess.CalledProcessError): |