# -*- coding: utf-8 -*-
#
# Copyright 2009-2011 CEA
# Pierre Raybaut
# Licensed under the terms of the CECILL License
# (see guidata/__init__.py for details)
# pylint: disable=W0613
"""
disthelpers
-----------
The ``guidata.disthelpers`` module provides helper functions for Python
package distribution on Microsoft Windows platforms with ``py2exe`` or on
all platforms thanks to ``cx_Freeze``.
"""
import sys
import os
from pathlib import Path
import shutil
import traceback
import atexit
import imp
from subprocess import Popen, PIPE
import warnings
from distutils.version import LooseVersion, StrictVersion
# ==============================================================================
# Module, scripts, programs
# ==============================================================================
def get_module_path(modname):
"""Return module *modname* base path"""
module = sys.modules.get(modname, __import__(modname))
return str(Path(module.__file__).parent.resolve())
# ==============================================================================
# Dependency management
# ==============================================================================
def get_changeset(path, rev=None):
"""Return Mercurial repository *path* revision number"""
args = ['hg', 'parent']
if rev is not None:
args += ['--rev', str(rev)]
process = Popen(
args, stdout=PIPE, stderr=PIPE, cwd=path, shell=True
)
try:
return (
process.stdout.read().splitlines()[0].split()[1]
)
except IndexError:
raise RuntimeError(process.stderr.read())
def prepend_module_to_path(module_path):
"""
Prepend to sys.path module located in *module_path*
Return string with module infos: name, revision, changeset
Use this function:
1) In your application to import local frozen copies of internal libraries
2) In your py2exe distributed package to add a text file containing the returned string
"""
if not Path(module_path).is_dir():
# Assuming py2exe distribution
return
sys.path.insert(0, str(Path(module_path).resolve()))
changeset = get_changeset(module_path)
name = Path(module_path).name
prefix = "Prepending module to sys.path"
message = prefix + (
f"{name} [revision {changeset}]"
).rjust(80 - len(prefix), ".")
print(message, file=sys.stderr)
if name in sys.modules:
sys.modules.pop(name)
nbsp = 0
for modname in sys.modules.keys():
if modname.startswith(name + '.'):
sys.modules.pop(modname)
nbsp += 1
warning = f'(removed {name} from sys.modules'
if nbsp:
warning += f' and {nbsp} subpackages'
warning += ')'
print(warning.rjust(80), file=sys.stderr)
return message
def prepend_modules_to_path(module_base_path):
"""Prepend to sys.path all modules located in *module_base_path*"""
if not Path(module_base_path).is_dir():
# Assuming py2exe distribution
return
fnames = [
str(Path(module_base_path) / name)
for name in os.listdir(module_base_path)
]
messages = [
prepend_module_to_path(dirname)
for dirname in fnames
if Path(dirname).is_dir()
]
return os.linesep.join(messages)
# ==============================================================================
# Distribution helpers
# ==============================================================================
def _remove_later(fname):
"""Try to remove file later (at exit)"""
def try_to_remove(fname):
if Path(fname).exists():
os.remove(fname)
atexit.register(try_to_remove, str(Path(fname).resolve()))
def to_include_files(data_files):
"""Convert data_files list to include_files list
data_files:
* this is the ``py2exe`` data files format
* list of tuples (dest_dirname, (src_fname1, src_fname2, ...))
include_files:
* this is the ``cx_Freeze`` data files format
* list of tuples ((src_fname1, dst_fname1),
(src_fname2, dst_fname2), ...))
"""
include_files = []
for dest_dir, fnames in data_files:
for source_fname in fnames:
dest_fname = str(Path(dest_dir) /
Path(source_fname).name)
include_files.append((source_fname, dest_fname))
return include_files
def strip_version(version):
"""Return version number with digits only
(Windows does not support strings in version numbers)"""
return (
version.split('beta')[0]
.split('alpha')[0]
.split('rc')[0]
.split('dev')[0]
)
def remove_dir(dirname):
"""Remove directory *dirname* and all its contents
Print details about the operation (progress, success/failure)"""
print(f"Removing directory '{dirname}'...", end=' ')
try:
shutil.rmtree(dirname, ignore_errors=True)
print("OK")
except Exception:
print("Failed!")
traceback.print_exc()
class Distribution(object):
"""Distribution object
Help creating an executable using ``py2exe`` or ``cx_Freeze``
"""
DEFAULT_EXCLUDES = [
'Tkconstants',
'Tkinter',
'tcl',
'tk',
'wx',
'_imagingtk',
'curses',
'PIL._imagingtk',
'ImageTk',
'PIL.ImageTk',
'FixTk',
'bsddb',
'email',
'pywin.debugger',
'pywin.debugger.dbgcon',
'matplotlib',
]
DEFAULT_INCLUDES = []
DEFAULT_BIN_EXCLUDES = [
'MSVCP100.dll',
'MSVCP90.dll',
'w9xpopen.exe',
'MSVCP80.dll',
'MSVCR80.dll',
]
DEFAULT_BIN_INCLUDES = []
DEFAULT_BIN_PATH_INCLUDES = []
DEFAULT_BIN_PATH_EXCLUDES = []
def __init__(self):
self.name = None
self.version = None
self.description = None
self.target_name = None
self._target_dir = None
self.icon = None
self.data_files = []
self.includes = self.DEFAULT_INCLUDES
self.excludes = self.DEFAULT_EXCLUDES
self.bin_includes = self.DEFAULT_BIN_INCLUDES
self.bin_excludes = self.DEFAULT_BIN_EXCLUDES
self.bin_path_includes = (
self.DEFAULT_BIN_PATH_INCLUDES
)
self.bin_path_excludes = (
self.DEFAULT_BIN_PATH_EXCLUDES
)
self.msvc = os.name == 'nt'
self._py2exe_is_loaded = False
self._pyqt4_added = False
self._pyside_added = False
# Attributes relative to cx_Freeze:
self.executables = []
@property
def target_dir(self):
"""Return target directory (default: 'dist')"""
dirname = self._target_dir
if dirname is None:
return 'dist'
else:
return dirname
@target_dir.setter # analysis:ignore
def target_dir(self, value):
self._target_dir = value
def setup(
self,
name,
version,
description,
script,
target_name=None,
target_dir=None,
icon=None,
data_files=None,
includes=None,
excludes=None,
bin_includes=None,
bin_excludes=None,
bin_path_includes=None,
bin_path_excludes=None,
msvc=None,
):
"""Setup distribution object
Notes:
* bin_path_excludes is specific to cx_Freeze (ignored if it's None)
* if msvc is None, it's set to True by default on Windows
platforms, False on non-Windows platforms
"""
self.name = name
self.version = (
strip_version(version)
if os.name == 'nt'
else version
)
self.description = description
assert Path(script).is_file()
self.script = script
self.target_name = target_name
self.target_dir = target_dir
self.icon = icon
if data_files is not None:
self.data_files += data_files
if includes is not None:
self.includes += includes
if excludes is not None:
self.excludes += excludes
if bin_includes is not None:
self.bin_includes += bin_includes
if bin_excludes is not None:
self.bin_excludes += bin_excludes
if bin_path_includes is not None:
self.bin_path_includes += bin_path_includes
if bin_path_excludes is not None:
self.bin_path_excludes += bin_path_excludes
if msvc is not None:
self.msvc = msvc
if self.msvc:
try:
pass # manage via msvc_runtime wheel (or give up anyway)
# self.data_files += create_msvc_data_files()
except IOError:
print(
"Setting the msvc option to False "
"will avoid this error",
file=sys.stderr,
)
raise
# cx_Freeze:
self.add_executable(
self.script, self.target_name, icon=self.icon
)
def add_text_data_file(self, filename, contents):
"""Create temporary data file *filename* with *contents*
and add it to *data_files*"""
open(filename, 'wb').write(contents)
self.data_files += [("", (filename,))]
_remove_later(filename)
def add_data_file(self, filename, destdir=''):
self.data_files += [(destdir, (filename,))]
# ------ Adding packages
def add_pyqt4(self):
"""Include module PyQt4 to the distribution"""
if self._pyqt4_added:
return
self._pyqt4_added = True
self.includes += [
'sip',
'PyQt4.Qt',
'PyQt4.QtSvg',
'PyQt4.QtNetwork',
]
import PyQt4
pyqt_path = str(Path(PyQt4.__file__).parent)
# Configuring PyQt4
conf = os.linesep.join(
["[Paths]", "Prefix = .", "Binaries = ."]
)
self.add_text_data_file('qt.conf', conf)
# Including plugins (.svg icons support, QtDesigner support, ...)
if self.msvc:
vc90man = "Microsoft.VC90.CRT.manifest"
pyqt_tmp = 'pyqt_tmp'
if Path(pyqt_tmp).is_dir():
shutil.rmtree(pyqt_tmp)
os.mkdir(pyqt_tmp)
vc90man_pyqt = str(Path(pyqt_tmp) / vc90man)
man = (
open(vc90man, "r")
.read()
.replace(
'