import sys, os, warnings, email
from types import ClassType,ListType
from distutils import command,filelist,version
from distutils.cmd import Command
from distutils.core import Distribution,gen_usage,DEBUG
from distutils.errors import *
from distutils.fancy_getopt import FancyGetopt,wrap_text
try:
from distutils import log
except ImportError:
# Python 2.2; create an instance that has the module interface but acts
# like the announce() methods in 2.2.
class Log:
verbose = 1
def log(self, level, msg):
if self.verbose >= level:
print msg
sys.stdout.flush()
def set_verbosity(self, verbose):
self.verbose = verbose
log = Log()
else:
if sys.version < '2.5':
def _log(self, level, msg, args):
if level >= self.threshold:
if args:
msg %= args
print msg
sys.stdout.flush()
return
log.Log._log = _log
del _log
from Ft.Lib import Terminfo
from Ft.Lib.DistExt import Version
# Our new Distribution class
class Dist(Distribution):
"""
An enhanced version of core Distutils' Distribution class.
Currently supported features, for *all* Python (2.2+) versions:
(from Python 2.3+)
download_url, classifiers - PEP 314 metadata fields
(from Python 2.5+)
install_egg_info command - for setuptools
requires, provides, obsoletes - PEP 314 metadata fields
(only available in 4Suite)
requires_python - [PEP 345] a list of version restrictions for Python
requires_external - [PEP 345] a list of external requirements
command_mapping - maps command names to a module/class name that differs
from the actual command name
"""
# 'command_mapping' maps command names to the module/class names
command_mapping = {
'config' : 'Config',
'build' : 'Build',
'build_py' : 'BuildPy',
'build_ext' : 'BuildExt',
'build_clib' : None,
'build_scripts' : 'BuildScripts',
'build_docs' : 'BuildDocs', # only in 4Suite
'build_l10n' : 'BuildL10n', # only in 4Suite
'clean' : None,
'install' : 'Install',
'install_lib' : 'InstallLib',
'install_headers' : None,
'install_scripts' : 'InstallScripts',
'install_data' : 'InstallData',
'install_egg_info' : 'InstallEggInfo', # new in 2.5+
'install_sysconf' : 'InstallSysconf', # only in 4Suite
'install_localstate' : 'InstallLocalState', # only in 4Suite
'install_devel' : 'InstallDevel', # only in 4Suite
#'install_man' : 'InstallMan', # only in 4Suite
'install_text' : 'InstallText', # only in 4Suite
'install_html' : 'InstallHtml', # only in 4Suite
#'install_info' : 'InstallInfo', # only in 4Suite
'install_l10n' : 'InstallL10n', # only in 4Suite
'install_config' : 'InstallConfig', # only in 4Suite
'sdist' : 'SDist',
'register' : None, # new in 2.3+
'bdist' : 'BDist',
'bdist_dumb' : None,
'bdist_rpm' : 'BDistRpm',
'bdist_inno' : 'BDistInno', # only in 4Suite
'bdist_msi' : None, # new in 2.5+
'bdist_egg' : 'BDistEgg',
'upload' : None, # new in 2.5+
'generate' : 'Generate', # only in 4Suite
'generate_bgen' : 'GenerateBisonGen', # only in 4Suite
'generate_l10n' : 'GenerateL10n', # only in 4Suite
}
command_aliases = {
'install_docs' : 'install_html',
'bdist_wininst' : 'bdist_inno',
}
standard_commands = ['config', 'build', 'clean', 'install', 'sdist',
'register', 'bdist', 'upload', 'generate']
if sys.version < '2.5':
standard_commands.remove('upload')
if sys.version < '2.3':
standard_commands.remove('register')
# 'toplevel_options' desribes the command-line options that may be
# supplied to the setup script prior to any actual command.
toplevel_options = []
# PKG-INFO is created for source distributions, so allow "developer"
# friendly features to be enabled/disabled (i.e., install_docs)
source_package = os.path.exists('PKG-INFO')
if not source_package:
toplevel_options.extend([
('source-package', 's',
'run as if from a source dist (developer testing)'),
])
def __init__(self, attrs):
# Add our placeholders for arguments from setup()
self.l10n = []
self.doc_files = []
self.bgen_files = []
self.sysconf_files = []
self.localstate_files = []
self.devel_files = []
# The module where configuration variables are written.
# Used by the 'install_config' command.
self.config_module = None
# File in source tree that represents the software copyright.
# Currently, only used by the 'bdist_inno' command.
self.license_file = None
# 'package' is the name of the subpackage.
self.package = None
# File in source tree that contains the setup attributes for the
# subpackage.
self.package_file = None
self.main_distribution = None
# Used for gathering and validating the files included in a source
# distribution. Used by the 'sdist' command.
self.manifest_templates = []
self.validate_templates = []
# Add support for build_py's 'package_data'. New in Python 2.4+
self.package_data = {}
# 'namespace_packages' is a list of package names whose contents are
# split across multiple distributions.
self.namespace_packages = None
Distribution.__init__(self, attrs)
return
def get_allfiles(self):
if self._allfiles is None:
# If a "main" distribution exists, use its files to prevent
# unnecessary additional searches.
if self.main_distribution:
self._allfiles = self.main_distribution.get_allfiles()
else:
source_list = filelist.FileList()
source_list.extend(filelist.findall())
# Remove files that don't really belong in the file list.
# Note the leading slash (\) before os.sep substitutions. It is
# needed to prevent regex-escaping when os.sep is '\' (Windows).
exclude_patterns = (
# revision control (CVS client) files
r'\%s?CVS(\.sandboxinfo)?\%s' % (os.sep, os.sep),
r'\.cvsignore$',
r'\.#[^\%s]+$' % os.sep,
# (X)Emacs temporary files
r'\.?#[^\%s]+#$' % os.sep,
# common editor backup files
r'[^\%s]+~$' % os.sep,
# python bytecode files
r'\.py[co]$',
)
for pattern in exclude_patterns:
source_list.exclude_pattern(pattern, is_regex=True)
self._allfiles = source_list.files
return self._allfiles
def get_source_files(self):
source_list = filelist.FileList()
source_list.set_allfiles(self.get_allfiles())
# Add the files used to create the Distribution
source_list.append(self.script_name)
if os.path.exists('setup.cfg'):
source_list.append('setup.cfg')
if self.package_file:
source_list.append(self.package_file)
# Get the source files from the command groupds
for cmd_name in ('generate', 'build', 'install'):
cmd = self.get_command_obj(cmd_name)
cmd.ensure_finalized()
source_list.extend(cmd.get_source_files())
# 'license_file' is used by bdist_inno
if self.license_file:
source_list.append(self.license_file)
# Add the files not included by the commands
for line in self.manifest_templates:
try:
source_list.process_template_line(line)
except DistutilsTemplateError, msg:
self.warn(str(msg))
# File list now complete -- sort it so that higher-level files
# come first
source_list.sort()
# Remove duplicates from the file list
source_list.remove_duplicates()
return source_list.files
# -- Config file finding/parsing methods ---------------------------
if sys.version < '2.4':
def parse_config_files(self, filenames=None):
Distribution.parse_config_files(self, filenames)
if 'global' in self.command_options:
global_options = self.command_options['global']
boolean_options = {'verbose':1, 'dry_run':1}
boolean_options.update(self.negative_opt)
for opt in global_options:
if opt not in boolean_options:
setattr(self, opt, global_options[opt][1])
return
# -- Command-line parsing methods ----------------------------------
if sys.version < '2.4':
def parse_command_line(self):
"""Parse the setup script's command line, taken from the
'script_args' instance attribute (which defaults to 'sys.argv[1:]'
-- see 'setup()' in core.py). This list is first processed for
"global options" -- options that set attributes of the Distribution
instance. Then, it is alternately scanned for Distutils commands
and options for that command. Each new command terminates the
options for the previous command. The allowed options for a
command are determined by the 'user_options' attribute of the
command class -- thus, we have to be able to load command classes
in order to parse the command line. Any error in that 'options'
attribute raises DistutilsGetoptError; any error on the
command-line raises DistutilsArgError. If no Distutils commands
were found on the command line, raises DistutilsArgError. Return
true if command-line was successfully parsed and we should carry
on with executing commands; false if no errors but we shouldn't
execute commands (currently, this only happens if user asks for
help).
"""
#
# We now have enough information to show the Macintosh dialog
# that allows the user to interactively specify the "command line".
#
toplevel_options = self._get_toplevel_options()
if sys.platform == 'mac':
import EasyDialogs
cmdlist = self.get_command_list()
self.script_args = EasyDialogs.GetArgv(
toplevel_options + self.display_options, cmdlist)
# We have to parse the command line a bit at a time -- global
# options, then the first command, then its options, and so on --
# because each command will be handled by a different class, and
# the options that are valid for a particular class aren't known
# until we have loaded the command class, which doesn't happen
# until we know what the command is.
self.commands = []
parser = FancyGetopt(toplevel_options + self.display_options)
parser.set_negative_aliases(self.negative_opt)
parser.set_aliases({'licence': 'license'})
args = parser.getopt(args=self.script_args, object=self)
option_order = parser.get_option_order()
log.set_verbosity(self.verbose)
# for display options we return immediately
if self.handle_display_options(option_order):
return
while args:
args = self._parse_command_opts(parser, args)
if args is None: # user asked for help (and got it)
return
# Handle the cases of --help as a "global" option, ie.
# "setup.py --help" and "setup.py --help command ...". For the
# former, we show global options (--verbose, --dry-run, etc.)
# and display-only options (--name, --version, etc.); for the
# latter, we omit the display-only options and show help for
# each command listed on the command line.
if self.help:
self._show_help(parser,
display_options=len(self.commands) == 0,
commands=self.commands)
return
# Oops, no commands found -- an end-user error
if not self.commands:
raise DistutilsArgError, "no commands supplied"
# All is well: return true
return 1
def _get_toplevel_options(self):
"""Return the non-display options recognized at the top level.
This includes options that are recognized *only* at the top
level as well as options recognized for commands.
"""
if sys.version < '2.4':
toplevel_options = self.global_options
else:
toplevel_options = Distribution._get_toplevel_options(self)
return toplevel_options + self.toplevel_options
def finalize_options(self):
if sys.version < '2.5':
# Run the setter functions for the metadata fields that have them.
# Only those fields that have a supplied value (not None) will
# be considered.
for name, value in vars(self.metadata).items():
if value is not None:
try:
setter = getattr(self.metadata, 'set_' + name)
except AttributeError:
pass
else:
setter(value)
requires_python = self.get_requires_python()
if requires_python:
requires_python = 'Python (%s)' % ', '.join(requires_python)
requires_python = Version.VersionPredicate(requires_python)
python_version = version.StrictVersion()
python_version.version = sys.version_info[:3]
python_version.prerelease = sys.version_info[3:]
if not requires_python.satisfied_by(python_version):
raise DistutilsSetupError(
"%s requires %s" % (self.metadata.name, requires_python))
# Initialize the containter type data variables before dealing
# with the information from the package defintions.
if self.packages is None:
self.packages = []
if self.package_dir is None:
self.package_dir = {}
if self.py_modules is None:
self.py_modules = []
if self.libraries is None:
self.libraries = []
if self.headers is None:
self.headers = []
if self.ext_modules is None:
self.ext_modules = []
if self.include_dirs is None:
self.include_dirs = []
if self.scripts is None:
self.scripts = []
if self.data_files is None:
self.data_files = []
if self.package_file is None:
self.package_file = self.script_name
if self.namespace_packages is None:
self.namespace_packages = []
# Per PEP 314, only use License and Platform if they can't be
# handled by an appropriate classifier. Or, in our case, aren't
# being handled by a classifier entry.
has_platform = has_license = False
for classifier in self.get_classifiers():
category = classifier.split('::', 1)[0]
category = category.strip().title()
if category == 'Operating System':
has_platform = True
elif category == 'License':
has_license = True
if self.metadata.license and has_license:
raise DistutilsSetupError("license keyword conflicts with"
" classifiers list")
if self.metadata.platforms and has_platform:
raise DistutilsSetupError("platforms keyword conflicts with"
" classifiers list")
# Finalize "private" variables; those that are not part of the
# setup arguments.
self._allfiles = None
Distribution.finalize_options(self)
def print_commands(self):
"""
Overridden to add the commands defined by 'command_mapping' to the
list of "standard commands".
"""
std_commands = []
is_std = {}
for command in self.standard_commands:
std_commands.append(command)
is_std[command] = True
klass = self.get_command_class(command)
for command, method in klass.sub_commands:
std_commands.append(command)
is_std[command] = True
extra_commands = []
for command in self.cmdclass:
if command not in is_std:
extra_commands.append(command)
max_length = max(map(len, (std_commands + extra_commands)))
self.print_command_list(std_commands, "Standard commands", max_length)
if extra_commands:
print
self.print_command_list(extra_commands, "Extra commands",
max_length)
return
def get_command_list(self):
"""
Overridden to add the commands defined by 'command_mapping' to the
list of (command, description) tuples.
"""
for command in self.command_mapping:
self.get_command_class(command)
return Distribution.get_command_list(self)
def print_option_list(self, options, header, max_length):
# Generate lines of help text.
line_width = Terminfo.GetColumns()
opt_width = max_length + 2 + 2 + 2 # room for indent + dashes + gutter
text_width = line_width - opt_width
big_indent = ' ' * opt_width
print header
for option in options:
long, short, help = option[:3]
if long[-1] == '=':
long = long[0:-1]
# Case 1: no short option at all
if short is None:
opt_names = long
# Case 2: we have a short option, so we have to include it
# just after the long option
else:
opt_names = "%s (-%s)" % (long, short)
text = wrap_text(help, text_width)
if text:
print " --%-*s %s" % (max_length, opt_names, text[0])
for line in text[1:]:
print big_indent + line
else:
print " --%-*s" % (max_length, opt_names)
print
return
def _show_help (self, parser, global_options=1, display_options=1,
commands=[]):
# Gather the options for the distribution
options = []
if global_options:
if display_options:
global_options = self._get_toplevel_options()
else:
global_options = self.global_options
options.extend(global_options)
if display_options:
display_options = self.display_options
options.extend(display_options)
# Gather the options for the requested commands
commands = []
for command in self.commands:
klass = self.get_command_class(command)
command_name = getattr(klass, 'command_name', klass.__name__)
command_options = klass.user_options
if hasattr(klass, 'help_options'):
command_options = command_options + klass.help_options
commands.append((command_name, command_options))
options.extend(command_options)
# Determine maximum length of option names
max_length = 0
for option in options:
long = option[0]
short = option[1]
l = len(long)
if long[-1] == '=':
l = l - 1
if short is not None:
l = l + 5 # " (-x)" where short == 'x'
if l > max_length:
max_length = l
# Now print the option tables
if global_options:
self.print_option_list(global_options, "Global options:",
max_length)
if display_options:
self.print_option_list(display_options,
"Information display options (just display"
" information, ignore any commands):",
max_length)
for name, options in commands:
self.print_option_list(options,
"Options for '%s' command:" % name,
max_length)
print gen_usage(self.script_name)
return
# -- Command class/object methods ----------------------------------
def get_command_class(self, command):
"""
Extends Distribution.get_command_class() to search 'command_mapping'
for modules that implement that requested command.
"""
# Try user defined classes first (and already loaded classes)
klass = self.cmdclass.get(command)
if klass:
return klass
if command in self.command_aliases:
command = self.command_aliases[command]
base_name = self.command_mapping.get(command)
if base_name is None:
return Distribution.get_command_class(self, command)
command_package = 'Ft.Lib.DistExt'
module_name = command_package + '.' + base_name
klass_name = base_name
try:
module = __import__(module_name, {}, {}, [klass_name])
except ImportError:
# If the module exists but is just broken, re-raise the existing
# exception as this is (most likely) a developer error.
if sys.exc_info()[-1].tb_next is not None:
raise
raise DistutilsModuleError(
"invalid command '%s' (no module named '%s')" %
(command, module_name))
try:
klass = getattr(module, klass_name)
except AttributeError:
raise DistutilsModuleError(
"invalid command '%s' (no class '%s' in module '%s')" %
(command, klass_name, module_name))
# Make sure that the command provides the proper command name
try:
if command != klass.command_name:
raise AttributeError('command_name')
except AttributeError:
raise DistutilsClassError(
"command class %s must define 'command_name' as %r" %
(klass, command))
self.cmdclass[command] = klass
return klass
# -- Methods that operate on the Distribution ----------------------
def announce (self, msg, level=1):
"""If the current verbosity level is of greater than or equal to
'level' print 'msg' to stdout.
"""
log.log(level, msg)
# -- Distribution query methods ------------------------------------
def has_l10n(self):
# Used for both build and generate
return len(self.l10n) > 0
def has_sysconf(self):
# Used for install
return len(self.sysconf_files) > 0
def has_localstate(self):
# Used for install
return len(self.localstate_files) > 0
def has_docs(self):
# Used for both build and install
# Both scripts and modules have generated documentation
return (len(self.doc_files) > 0 or
self.has_modules() or self.has_scripts())
def has_text(self):
return self.license_file is not None or len(self.doc_files) > 0
def has_devel(self):
# Used for install
return len(self.devel_files) > 0
def has_bgen(self):
# Used for both sdist and generate
return self.bgen_files and len(self.bgen_files) > 0
# ----------------------------------------------------------------------
# Upgade distutils core support to 2.5+ features
import re, operator
from distutils import dist
from distutils.util import rfc822_escape
class DistributionMetadata(dist.DistributionMetadata):
_METHOD_BASENAMES = dist.DistributionMetadata._METHOD_BASENAMES + (
'requires_python', 'requires_external')
requires_python = None
requires_external = None
copyright = None
def get_requires_python(self):
return self.requires_python or []
def set_requires_python(self, value):
if not isinstance(value, list):
value = [ v.strip() for v in value.split(',') ]
for v in value:
Version.SplitComparison(v)
self.requires_python = value
def get_requires_external(self):
return self.requires_external or []
def set_requires_external(self, value):
for v in value:
Version.SplitComparison(v)
self.requires_external = value
if sys.version < '2.5':
requires = None
provides = None
obsoletes = None
_METHOD_BASENAMES += ('requires', 'provides', 'obsoletes')
def get_requires(self):
return self.requires or []
def set_requires(self, value):
for v in value:
Version.VersionPredicate(v)
self.requires = value
def get_provides(self):
return self.provides or []
def set_provides(self, value):
for v in value:
Version.SplitProvision(v)
self.provides = value
def get_obsoletes(self):
return self.obsoletes or []
def set_obsoletes(self, value):
for v in value:
Version.VersionPredicate(v)
self.obsoletes = value
def write_pkg_info(self, base_dir):
"""
Write the PKG-INFO file into the release tree.
"""
pkg_info = open(os.path.join(base_dir, 'PKG-INFO'), 'w')
self.write_pkg_file(pkg_info)
pkg_info.close()
if sys.version < '2.3':
classifiers = None
download_url = None
_METHOD_BASENAMES += ('classifiers', 'download_url')
def get_classifiers(self):
return self.classifiers or []
def get_download_url(self):
return self.download_url or "UNKNOWN"
# -- PKG-INFO and .egg-info utility methods --------------------
def from_stream(cls, stream):
headers = email.message_from_file(stream)
fields = {}
for header, value in headers.items():
# clean up the keys and values, normalise "-" to "_"
field = header.lower().replace('-', '_')
value = value.strip()
# Platform, Classifiers, Requires, Provides, Obsoletes,
# Requires-External
if field in fields:
old = fields[field]
if isinstance(old, list):
old.append(value)
else:
fields[field] = [old, value]
else:
fields[field] = value
# remove fields only needed for the Python Package Index
for field in ('metadata_version', 'target_version'):
if field in fields:
del fields[field]
# convert comma-separated items into lists
for field in ('keywords', 'requires_python'):
if field in fields:
values = fields[field].split(',')
values = [ value.strip() for value in values ]
fields[field] = [ value for value in values if value ]
# ensure multiple-use fields are lists
for field in ('platform', 'classifier', 'requires', 'provides',
'obsoletes', 'requires_external'):
if field in fields:
value = fields[field]
if not isinstance(value, list):
fields[field] = [value]
# convert PKG-INFO field names to the corresponding metadata names
for field, attr in (('platform', 'platforms'),
('classifier', 'classifiers'),
('home_page', 'url'),
('summary', 'description'),
):
if field in fields:
fields[attr] = fields[field]
del fields[field]
self = cls()
for name, value in fields.items():
if hasattr(self, 'set_' + name):
getattr(self, 'set_' + name)(value)
elif hasattr(self, name):
setattr(self, name, value)
else:
warnings.warn("unknown metadata attribute: %s" % name)
return self
from_stream = classmethod(from_stream)
def from_string(cls, string):
from cStringIO import StringIO
return cls.from_stream(StringIO(string))
from_string = classmethod(from_string)
def from_filename(cls, filename):
fp = open(filename)
try:
return cls.from_stream(fp)
finally:
fp.close()
from_filename = classmethod(from_filename)
def write_pkg_file(self, file):
"""
Write the PKG-INFO format data to a file object.
Supports metadata version 1.2 (PEP 345), 1.1 (PEP 314) and
1.0 (PEP 241) in a lowest common denominator fashion.
"""
# Use lowest possible version for metadata version
if self.requires_python or self.requires_external:
# PEP 345 fields
version = '1.2'
elif (self.download_url or self.classifiers or
self.provides or self.requires or self.obsoletes):
# PEP 314 fields
version = '1.1'
else:
# PEP 241 fields
version = '1.0'
file.write('Metadata-Version: %s\n' % version)
# PEP 241 (http://python.org/dev/peps/pep-0241.html) fields:
# Name
# Version
# Platform (multiple use) [obsolete]
# Summary
# Description (optional)
# Keywords (comma-separated list, optional)
# Home-page (optional)
# Author (optional)
# Author-email
# License [obsolete]
file.write('Name: %s\n' % self.get_name() )
file.write('Version: %s\n' % self.get_version() )
file.write('Summary: %s\n' % self.get_description() )
file.write('Home-page: %s\n' % self.get_url() )
file.write('Author: %s\n' % self.get_contact() )
file.write('Author-email: %s\n' % self.get_contact_email() )
if self.long_description:
description = rfc822_escape(self.long_description)
file.write('Description: %s\n' % description)
if self.keywords:
keywords = ','.join(self.keywords)
file.write('Keywords: %s\n' % keywords)
# Per PEP 314, only use License and Platform if they can't be
# handled by an appropriate classifier. Or, in our case, aren't
# being handled by a classifier entry.
has_platform = has_license = False
for classifier in self.get_classifiers():
category = classifier.split('::', 1)[0]
category = category.strip().title()
if category == 'Operating System':
has_platform = True
elif category == 'License':
has_license = True
if self.license:
if has_license:
raise DistutilsSetupError("license keyword conflicts with"
" classifiers list")
file.write('License: %s\n' % self.license)
if self.platforms:
if has_platform:
raise DistutilsSetupError("platforms keyword conflicts with"
" classifiers list")
for platform in self.platforms:
file.write('Platform: %s\n' % platform)
# PEP 314 (http://python.org/dev/peps/pep-0314.html) fields:
# Supported-Platform (multiple use) [binary dists only; unused]
# Download-URL
# Classifier (multiple use)
# Requires (multiple use)
# Provides (multiple use)
# Obsoletes (multiple use)
if self.download_url:
file.write('Download-URL: %s\n' % self.get_download_url())
for value in self.get_classifiers():
file.write('Classifier: %s\n' % value)
for value in self.get_requires():
file.write('Requires: %s\n' % value)
for value in self.get_provides():
file.write('Provides: %s\n' % value)
for value in self.get_obsoletes():
file.write('Obsoletes: %s\n' % value)
# PEP 345 (http://python.org/dev/peps/pep-0345.html) fields:
# Requires-Python (comma separated list)
# Requires-External (multiple use)
# Copyright
if self.requires_python:
value = ','.join(self.get_requires_python())
file.write('Requires-Python: %s\n' % value)
for value in self.get_requires_external():
file.write('Requires-External: %s\n' % value)
if self.copyright:
file.write('Copyright: %s\n' % self.copyright)
return
# Unfortunately, this is the only way to supply a different metadata class
dist.DistributionMetadata = DistributionMetadata
|