diff options
| author | Erik Rose <ErikRose@psu.edu> | 2010-02-26 15:46:59 -0500 |
|---|---|---|
| committer | Erik Rose <ErikRose@psu.edu> | 2010-02-26 15:46:59 -0500 |
| commit | 4eda36d32612b87a6da4a7e69064ccd13298a94b (patch) | |
| tree | 4ed6d90c1d6fa5378d79f9be14f9b430092b5a3a /src/distutils2 | |
| parent | 6d666c89d771c0907bf5148743c901f0331b59c8 (diff) | |
| parent | 15b4a8d408b09a0cae3a39ce0430d94155b82a1c (diff) | |
| download | disutils2-4eda36d32612b87a6da4a7e69064ccd13298a94b.tar.gz | |
Merged Tarek's commits in. test_check_document was failing in upstream before merge. Not my fault. :-)
Diffstat (limited to 'src/distutils2')
32 files changed, 1343 insertions, 1108 deletions
diff --git a/src/distutils2/__init__.py b/src/distutils2/__init__.py index ecc163d..b77c534 100644 --- a/src/distutils2/__init__.py +++ b/src/distutils2/__init__.py @@ -7,13 +7,10 @@ used from a setup script as setup (...) """ +__all__ = ['__version__', 'setup'] __revision__ = "$Id: __init__.py 78020 2010-02-06 16:37:32Z benjamin.peterson $" +__version__ = "1.0a1" + +from distutils2.core import setup -# Distutils version -# -# Updated automatically by the Python release process. -# -#--start constants-- -__version__ = "2.7a3" -#--end constants-- diff --git a/src/distutils2/cmd.py b/src/distutils2/cmd.py index d75c880..6dcc998 100644 --- a/src/distutils2/cmd.py +++ b/src/distutils2/cmd.py @@ -189,16 +189,6 @@ class Command: """ log.log(level, msg) - def debug_print(self, msg): - """Print 'msg' to stdout if the global DEBUG (taken from the - DISTUTILS_DEBUG environment variable) flag is true. - """ - from distutils2.debug import DEBUG - if DEBUG: - print msg - sys.stdout.flush() - - # -- Option validation methods ------------------------------------- # (these are very handy in writing the 'finalize_options()' method) # diff --git a/src/distutils2/command/bdist_rpm.py b/src/distutils2/command/bdist_rpm.py index ea7fd87..31f9711 100644 --- a/src/distutils2/command/bdist_rpm.py +++ b/src/distutils2/command/bdist_rpm.py @@ -10,7 +10,6 @@ import os import string from distutils2.core import Command -from distutils2.debug import DEBUG from distutils2.util import write_file from distutils2.errors import (DistutilsOptionError, DistutilsPlatformError, DistutilsFileError, DistutilsExecError) @@ -268,13 +267,6 @@ class bdist_rpm (Command): def run (self): - if DEBUG: - print "before _get_package_data():" - print "vendor =", self.vendor - print "packager =", self.packager - print "doc_files =", self.doc_files - print "changelog =", self.changelog - # make directories if self.spec_only: spec_dir = self.dist_dir diff --git a/src/distutils2/command/check.py b/src/distutils2/command/check.py index ac2fd93..bd31c14 100644 --- a/src/distutils2/command/check.py +++ b/src/distutils2/command/check.py @@ -7,30 +7,6 @@ __revision__ = "$Id: check.py 75266 2009-10-05 22:32:48Z andrew.kuchling $" from distutils2.core import Command from distutils2.errors import DistutilsSetupError -try: - # docutils is installed - from docutils.utils import Reporter - from docutils.parsers.rst import Parser - from docutils import frontend - from docutils import nodes - from StringIO import StringIO - - class SilentReporter(Reporter): - - def __init__(self, source, report_level, halt_level, stream=None, - debug=0, encoding='ascii', error_handler='replace'): - self.messages = [] - Reporter.__init__(self, source, report_level, halt_level, stream, - debug, encoding, error_handler) - - def system_message(self, level, message, *children, **kwargs): - self.messages.append((level, message, children, kwargs)) - - HAS_DOCUTILS = True -except ImportError: - # docutils is not installed - HAS_DOCUTILS = False - class check(Command): """This command checks the meta-data of the package. """ @@ -65,7 +41,7 @@ class check(Command): if self.metadata: self.check_metadata() if self.restructuredtext: - if HAS_DOCUTILS: + if self.distribution.metadata.docutils_support: self.check_restructuredtext() elif self.strict: raise DistutilsSetupError('The docutils package is needed.') @@ -83,32 +59,14 @@ class check(Command): Warns if any are missing. """ - metadata = self.distribution.metadata - - missing = [] - for attr in ('name', 'version', 'url'): - if not (hasattr(metadata, attr) and getattr(metadata, attr)): - missing.append(attr) - - if missing: + missing, __ = self.distribution.metadata.check() + if missing != []: self.warn("missing required meta-data: %s" % ', '.join(missing)) - if metadata.author: - if not metadata.author_email: - self.warn("missing meta-data: if 'author' supplied, " + - "'author_email' must be supplied too") - elif metadata.maintainer: - if not metadata.maintainer_email: - self.warn("missing meta-data: if 'maintainer' supplied, " + - "'maintainer_email' must be supplied too") - else: - self.warn("missing meta-data: either (author and author_email) " + - "or (maintainer and maintainer_email) " + - "must be supplied") def check_restructuredtext(self): """Checks if the long string fields are reST-compliant.""" - data = self.distribution.get_long_description() - for warning in self._check_rst_data(data): + missing, warnings = self.distribution.metadata.check() + for warning in warnings: line = warning[-1].get('line') if line is None: warning = warning[1] @@ -116,28 +74,3 @@ class check(Command): warning = '%s (line %s)' % (warning[1], line) self.warn(warning) - def _check_rst_data(self, data): - """Returns warnings when the provided data doesn't compile.""" - source_path = StringIO() - parser = Parser() - settings = frontend.OptionParser().get_default_values() - settings.tab_width = 4 - settings.pep_references = None - settings.rfc_references = None - reporter = SilentReporter(source_path, - settings.report_level, - settings.halt_level, - stream=settings.warning_stream, - debug=settings.debug, - encoding=settings.error_encoding, - error_handler=settings.error_encoding_error_handler) - - document = nodes.document(settings, reporter, source=source_path) - document.note_source(source_path, -1) - try: - parser.parse(data, document) - except AttributeError: - reporter.messages.append((-1, 'Could not finish the parsing.', - '', {})) - - return reporter.messages diff --git a/src/distutils2/command/install.py b/src/distutils2/command/install.py index 004dca8..588786f 100644 --- a/src/distutils2/command/install.py +++ b/src/distutils2/command/install.py @@ -13,7 +13,6 @@ from distutils2._backport.sysconfig import (get_config_vars, get_paths, from distutils2 import log from distutils2.core import Command -from distutils2.debug import DEBUG from distutils2.errors import DistutilsPlatformError from distutils2.util import write_file from distutils2.util import convert_path, change_root, get_platform @@ -231,9 +230,10 @@ class install(Command): prefix, exec_prefix, srcdir = get_config_vars('prefix', 'exec_prefix', 'srcdir') - self.config_vars = {'dist_name': self.distribution.get_name(), - 'dist_version': self.distribution.get_version(), - 'dist_fullname': self.distribution.get_fullname(), + metadata = self.distribution.metadata + self.config_vars = {'dist_name': metadata['Name'], + 'dist_version': metadata['Version'], + 'dist_fullname': metadata.get_fullname(), 'py_version': py_version, 'py_version_short': py_version[0:3], 'py_version_nodot': py_version[0] + py_version[2], @@ -255,11 +255,6 @@ class install(Command): self.config_vars['base'] = self.install_base self.config_vars['platbase'] = self.install_platbase - if DEBUG: - from pprint import pprint - print "config vars:" - pprint(self.config_vars) - # Expand "~" and configuration variables in the installation # directories. self.expand_dirs() @@ -313,8 +308,6 @@ class install(Command): def dump_dirs(self, msg): """Dumps the list of user options.""" - if not DEBUG: - return from distutils2.fancy_getopt import longopt_xlate log.debug(msg + ":") for opt in self.user_options: @@ -399,7 +392,7 @@ class install(Command): for key, value in scheme.items(): if key == 'platinclude': key = 'headers' - value = os.path.join(value, self.distribution.get_name()) + value = os.path.join(value, self.distribution.metadata['Name']) attrname = 'install_' + key if hasattr(self, attrname): if getattr(self, attrname) is None: @@ -475,7 +468,6 @@ class install(Command): home = convert_path(os.path.expanduser("~")) for name, path in self.config_vars.iteritems(): if path.startswith(home) and not os.path.isdir(path): - self.debug_print("os.makedirs('%s', 0700)" % path) os.makedirs(path, 0700) # -- Command execution methods ------------------------------------- diff --git a/src/distutils2/command/install_egg_info.py b/src/distutils2/command/install_egg_info.py index 79abe2f..765c69f 100644 --- a/src/distutils2/command/install_egg_info.py +++ b/src/distutils2/command/install_egg_info.py @@ -21,10 +21,11 @@ class install_egg_info(Command): self.install_dir = None def finalize_options(self): + metadata = self.distribution.metadata self.set_undefined_options('install_lib',('install_dir','install_dir')) basename = "%s-%s-py%s.egg-info" % ( - to_filename(safe_name(self.distribution.get_name())), - to_filename(safe_version(self.distribution.get_version())), + to_filename(safe_name(metadata['Name'])), + to_filename(safe_version(metadata['Version'])), sys.version[:3] ) self.target = os.path.join(self.install_dir, basename) @@ -45,7 +46,7 @@ class install_egg_info(Command): log.info("Writing %s", target) if not self.dry_run: f = open(target, 'w') - self.distribution.metadata.write_pkg_file(f) + self.distribution.metadata.write_file(f) f.close() def get_outputs(self): diff --git a/src/distutils2/command/register.py b/src/distutils2/command/register.py index 50c4b67..4cd878e 100644 --- a/src/distutils2/command/register.py +++ b/src/distutils2/command/register.py @@ -227,26 +227,26 @@ Your selection [default 1]: ''', log.INFO) meta = self.distribution.metadata data = { ':action': action, + # XXX implement 1.1 'metadata_version' : '1.0', - 'name': meta.get_name(), - 'version': meta.get_version(), - 'summary': meta.get_description(), - 'home_page': meta.get_url(), - 'author': meta.get_contact(), - 'author_email': meta.get_contact_email(), - 'license': meta.get_licence(), - 'description': meta.get_long_description(), - 'keywords': meta.get_keywords(), - 'platform': meta.get_platforms(), - 'classifiers': meta.get_classifiers(), - 'download_url': meta.get_download_url(), - # PEP 314 - 'provides': meta.get_provides(), - 'requires': meta.get_requires(), - 'obsoletes': meta.get_obsoletes(), + 'name': meta['Name'], + 'version': meta['Version'], + 'summary': meta['Summary'], + 'home_page': meta['Home-page'], + 'author': meta['Author'], + 'author_email': meta['Author-email'], + 'license': meta['License'], + 'description': meta['Description'], + 'keywords': meta['Keywords'], + 'platform': meta['Platform'], + 'classifiers': meta['Classifier'], + 'download_url': meta['Download-URL'], + #'provides': meta['Provides'], + #'requires': meta['Requires'], + #'obsoletes': meta['Obsoletes'], } - if data['provides'] or data['requires'] or data['obsoletes']: - data['metadata_version'] = '1.1' + #if data['provides'] or data['requires'] or data['obsoletes']: + # data['metadata_version'] = '1.1' return data def post_to_server(self, data, auth=None): diff --git a/src/distutils2/command/sdist.py b/src/distutils2/command/sdist.py index 70b888f..2757299 100644 --- a/src/distutils2/command/sdist.py +++ b/src/distutils2/command/sdist.py @@ -204,8 +204,6 @@ class sdist(Command): # manifest, but there's no template -- which will happen if the # developer elects to generate a manifest some other way -- then we # can't regenerate the manifest, so we don't.) - self.debug_print("checking if %s newer than %s" % - (self.distribution.script_name, self.manifest)) setup_newer = newer(self.distribution.script_name, self.manifest) @@ -450,7 +448,7 @@ class sdist(Command): dest = os.path.join(base_dir, file) self.copy_file(file, dest, link=link) - self.distribution.metadata.write_pkg_info(base_dir) + self.distribution.metadata.write(os.path.join(base_dir, 'PKG-INFO')) def make_distribution(self): """Create the source distribution(s). First, we create the release diff --git a/src/distutils2/command/upload.py b/src/distutils2/command/upload.py index 742a4e5..ab97b97 100644 --- a/src/distutils2/command/upload.py +++ b/src/distutils2/command/upload.py @@ -84,14 +84,15 @@ class upload(PyPIRCCommand): # register a new release content = open(filename,'rb').read() meta = self.distribution.metadata + data = { # action ':action': 'file_upload', 'protcol_version': '1', # identify release - 'name': meta.get_name(), - 'version': meta.get_version(), + 'name': meta['Name'], + 'version': meta['Version'], # file content 'content': (os.path.basename(filename),content), @@ -100,21 +101,23 @@ class upload(PyPIRCCommand): 'md5_digest': md5(content).hexdigest(), # additional meta-data + # XXX Implement 1.1 'metadata_version' : '1.0', - 'summary': meta.get_description(), - 'home_page': meta.get_url(), - 'author': meta.get_contact(), - 'author_email': meta.get_contact_email(), - 'license': meta.get_licence(), - 'description': meta.get_long_description(), - 'keywords': meta.get_keywords(), - 'platform': meta.get_platforms(), - 'classifiers': meta.get_classifiers(), - 'download_url': meta.get_download_url(), - # PEP 314 - 'provides': meta.get_provides(), - 'requires': meta.get_requires(), - 'obsoletes': meta.get_obsoletes(), + 'name': meta['Name'], + 'version': meta['Version'], + 'summary': meta['Summary'], + 'home_page': meta['Home-page'], + 'author': meta['Author'], + 'author_email': meta['Author-email'], + 'license': meta['License'], + 'description': meta['Description'], + 'keywords': meta['Keywords'], + 'platform': meta['Platform'], + 'classifiers': meta['Classifier'], + 'download_url': meta['Download-URL'], + #'provides': meta['Provides'], + #'requires': meta['Requires'], + #'obsoletes': meta['Obsoletes'], } comment = '' if command == 'bdist_rpm': diff --git a/src/distutils2/core.py b/src/distutils2/core.py index c5813a2..40b905c 100644 --- a/src/distutils2/core.py +++ b/src/distutils2/core.py @@ -11,7 +11,6 @@ __revision__ = "$Id: core.py 77704 2010-01-23 09:23:15Z tarek.ziade $" import sys import os -from distutils2.debug import DEBUG from distutils2.errors import (DistutilsSetupError, DistutilsArgError, DistutilsError, CCompilerError) from distutils2.util import grok_environment_error @@ -124,10 +123,6 @@ def setup(**attrs): # the setup script, but be overridden by the command line. dist.parse_config_files() - if DEBUG: - print "options (after parsing config files):" - dist.dump_option_dicts() - if _setup_stop_after == "config": return dist @@ -139,10 +134,6 @@ def setup(**attrs): except DistutilsArgError, msg: raise SystemExit, gen_usage(dist.script_name) + "\nerror: %s" % msg - if DEBUG: - print "options (after parsing command line):" - dist.dump_option_dicts() - if _setup_stop_after == "commandline": return dist @@ -154,19 +145,11 @@ def setup(**attrs): raise SystemExit, "interrupted" except (IOError, os.error), exc: error = grok_environment_error(exc) - - if DEBUG: - sys.stderr.write(error + "\n") - raise - else: - raise SystemExit, error + raise SystemExit, error except (DistutilsError, CCompilerError), msg: - if DEBUG: - raise - else: - raise SystemExit, "error: " + str(msg) + raise SystemExit, "error: " + str(msg) return dist diff --git a/src/distutils2/debug.py b/src/distutils2/debug.py deleted file mode 100644 index e3c33ea..0000000 --- a/src/distutils2/debug.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - -__revision__ = "$Id: debug.py 68943 2009-01-25 22:09:10Z tarek.ziade $" - -# If DISTUTILS_DEBUG is anything other than the empty string, we run in -# debug mode. -DEBUG = os.environ.get('DISTUTILS_DEBUG') diff --git a/src/distutils2/dist.py b/src/distutils2/dist.py index 31b3d33..55737a3 100644 --- a/src/distutils2/dist.py +++ b/src/distutils2/dist.py @@ -18,7 +18,6 @@ from distutils2.errors import (DistutilsOptionError, DistutilsArgError, from distutils2.fancy_getopt import FancyGetopt, translate_longopt from distutils2.util import check_environ, strtobool from distutils2 import log -from distutils2.debug import DEBUG from distutils2.metadata import DistributionMetadata # Regex to define acceptable Distutils command names. This is not *quite* @@ -28,7 +27,7 @@ from distutils2.metadata import DistributionMetadata command_re = re.compile (r'^[a-zA-Z]([a-zA-Z0-9_]*)$') -class Distribution: +class Distribution(object): """The core of the Distutils. Most of the work hiding behind 'setup' is really done within a Distribution instance, which farms the work out to the Distutils commands specified on the command line. @@ -145,10 +144,11 @@ Common commands: (see '--help-commands' for more) # information here (and enough command-line options) that it's # worth it. Also delegate 'get_XXX()' methods to the 'metadata' # object in a sneaky and underhanded (but efficient!) way. + self.metadata = DistributionMetadata() - for basename in self.metadata._METHOD_BASENAMES: - method_name = "get_" + basename - setattr(self, method_name, getattr(self.metadata, method_name)) + #for basename in self.metadata._METHOD_BASENAMES: + # method_name = "get_" + basename + # setattr(self, method_name, getattr(self.metadata, method_name)) # 'cmdclass' maps command names to class objects, so we # can 1) quickly figure out which class to instantiate when @@ -227,7 +227,7 @@ Common commands: (see '--help-commands' for more) # the setup script) to possibly override any or all of these # distribution options. - if attrs: + if attrs is not None: # Pull out the set of command options and work on them # specifically. Note that this order guarantees that aliased # command options will override any supplied redundantly @@ -240,22 +240,11 @@ Common commands: (see '--help-commands' for more) for (opt, val) in cmd_options.items(): opt_dict[opt] = ("setup script", val) - if 'licence' in attrs: - attrs['license'] = attrs['licence'] - del attrs['licence'] - msg = "'licence' distribution option is deprecated; use 'license'" - if warnings is not None: - warnings.warn(msg) - else: - sys.stderr.write(msg + "\n") - # Now work on the rest of the attributes. Any attribute that's # not already defined is invalid! - for (key, val) in attrs.items(): - if hasattr(self.metadata, "set_" + key): - getattr(self.metadata, "set_" + key)(val) - elif hasattr(self.metadata, key): - setattr(self.metadata, key, val) + for key, val in attrs.items(): + if self.metadata.is_metadata_field(key): + self.metadata[key] = val elif hasattr(self, key): setattr(self, key, val) else: @@ -294,6 +283,9 @@ Common commands: (see '--help-commands' for more) dict = self.command_options[command] = {} return dict + def get_fullname(self): + return self.metadata.get_fullname() + def dump_option_dicts(self, header=None, commands=None, indent=""): from pprint import pformat @@ -366,9 +358,7 @@ Common commands: (see '--help-commands' for more) if os.path.isfile(local_file): files.append(local_file) - if DEBUG: - self.announce("using config files: %s" % ', '.join(files)) - + log.debug("using config files: %s" % ', '.join(files)) return files def parse_config_files(self, filenames=None): @@ -377,13 +367,11 @@ Common commands: (see '--help-commands' for more) if filenames is None: filenames = self.find_config_files() - if DEBUG: - self.announce("Distribution.parse_config_files():") + log.debug("Distribution.parse_config_files():") parser = ConfigParser() for filename in filenames: - if DEBUG: - self.announce(" reading %s" % filename) + log.debug(" reading %s" % filename) parser.read(filename) for section in parser.sections(): options = parser.options(section) @@ -593,13 +581,15 @@ Common commands: (see '--help-commands' for more) instance, analogous to the .finalize_options() method of Command objects. """ - for attr in ('keywords', 'platforms'): - value = getattr(self.metadata, attr) - if value is None: - continue - if isinstance(value, str): - value = [elm.strip() for elm in value.split(',')] - setattr(self.metadata, attr, value) + + # XXX conversion -- removed + #for attr in ('keywords', 'platforms'): + # value = self.metadata.get_field(attr) + # if value is None: + # continue + # if isinstance(value, str): + # value = [elm.strip() for elm in value.split(',')] + # setattr(self.metadata, attr, value) def _show_help(self, parser, global_options=1, display_options=1, commands=[]): @@ -676,11 +666,11 @@ Common commands: (see '--help-commands' for more) for option in self.display_options: is_display_option[option[0]] = 1 - for (opt, val) in option_order: + for opt, val in option_order: if val and is_display_option.get(opt): opt = translate_longopt(opt) - value = getattr(self.metadata, "get_"+opt)() - if opt in ['keywords', 'platforms']: + value = self.metadata[opt] + if opt in ['keywords', 'platform']: print(','.join(value)) elif opt in ('classifiers', 'provides', 'requires', 'obsoletes'): @@ -835,9 +825,8 @@ Common commands: (see '--help-commands' for more) """ cmd_obj = self.command_obj.get(command) if not cmd_obj and create: - if DEBUG: - self.announce("Distribution.get_command_obj(): " \ - "creating '%s' command object" % command) + log.debug("Distribution.get_command_obj(): " \ + "creating '%s' command object" % command) klass = self.get_command_class(command) cmd_obj = self.command_obj[command] = klass(self) @@ -867,11 +856,10 @@ Common commands: (see '--help-commands' for more) if option_dict is None: option_dict = self.get_option_dict(command_name) - if DEBUG: - self.announce(" setting options for '%s' command:" % command_name) + log.debug(" setting options for '%s' command:" % command_name) + for (option, (source, value)) in option_dict.items(): - if DEBUG: - self.announce(" %s = %s (from %s)" % (option, value, + log.debug(" %s = %s (from %s)" % (option, value, source)) try: bool_opts = map(translate_longopt, command_obj.boolean_options) diff --git a/src/distutils2/filelist.py b/src/distutils2/filelist.py index 711ec8d..08122a3 100644 --- a/src/distutils2/filelist.py +++ b/src/distutils2/filelist.py @@ -34,14 +34,6 @@ class FileList(object): def findall(self, dir=os.curdir): self.allfiles = findall(dir) - def debug_print(self, msg): - """Print 'msg' to stdout if the global DEBUG (taken from the - DISTUTILS_DEBUG environment variable) flag is true. - """ - from distutils2.debug import DEBUG - if DEBUG: - print msg - # -- List-like methods --------------------------------------------- def append(self, item): @@ -116,28 +108,24 @@ class FileList(object): # right number of words on the line for that action -- so we # can proceed with minimal error-checking. if action == 'include': - self.debug_print("include " + ' '.join(patterns)) for pattern in patterns: if not self.include_pattern(pattern, anchor=1): log.warn("warning: no files found matching '%s'", pattern) elif action == 'exclude': - self.debug_print("exclude " + ' '.join(patterns)) for pattern in patterns: if not self.exclude_pattern(pattern, anchor=1): log.warn(("warning: no previously-included files " "found matching '%s'"), pattern) elif action == 'global-include': - self.debug_print("global-include " + ' '.join(patterns)) for pattern in patterns: if not self.include_pattern(pattern, anchor=0): log.warn(("warning: no files found matching '%s' " + "anywhere in distribution"), pattern) elif action == 'global-exclude': - self.debug_print("global-exclude " + ' '.join(patterns)) for pattern in patterns: if not self.exclude_pattern(pattern, anchor=0): log.warn(("warning: no previously-included files matching " @@ -145,8 +133,6 @@ class FileList(object): pattern) elif action == 'recursive-include': - self.debug_print("recursive-include %s %s" % - (dir, ' '.join(patterns))) for pattern in patterns: if not self.include_pattern(pattern, prefix=dir): log.warn(("warning: no files found matching '%s' " + @@ -154,8 +140,6 @@ class FileList(object): pattern, dir) elif action == 'recursive-exclude': - self.debug_print("recursive-exclude %s %s" % - (dir, ' '.join(patterns))) for pattern in patterns: if not self.exclude_pattern(pattern, prefix=dir): log.warn(("warning: no previously-included files matching " @@ -163,13 +147,11 @@ class FileList(object): pattern, dir) elif action == 'graft': - self.debug_print("graft " + dir_pattern) if not self.include_pattern(None, prefix=dir_pattern): log.warn("warning: no directories found matching '%s'", dir_pattern) elif action == 'prune': - self.debug_print("prune " + dir_pattern) if not self.exclude_pattern(None, prefix=dir_pattern): log.warn(("no previously-included directories found " + "matching '%s'"), dir_pattern) @@ -207,16 +189,12 @@ class FileList(object): """ files_found = 0 pattern_re = translate_pattern(pattern, anchor, prefix, is_regex) - self.debug_print("include_pattern: applying regex r'%s'" % - pattern_re.pattern) - # delayed loading of allfiles list if self.allfiles is None: self.findall() for name in self.allfiles: if pattern_re.search(name): - self.debug_print(" adding " + name) self.files.append(name) files_found = 1 @@ -233,11 +211,8 @@ class FileList(object): """ files_found = 0 pattern_re = translate_pattern(pattern, anchor, prefix, is_regex) - self.debug_print("exclude_pattern: applying regex r'%s'" % - pattern_re.pattern) for i in range(len(self.files)-1, -1, -1): if pattern_re.search(self.files[i]): - self.debug_print(" removing " + self.files[i]) del self.files[i] files_found = 1 diff --git a/src/distutils2/metadata.py b/src/distutils2/metadata.py index 5ec7fdf..499c2a6 100644 --- a/src/distutils2/metadata.py +++ b/src/distutils2/metadata.py @@ -1,3 +1,56 @@ +""" +================================================== +Implementation of the Metadata for Python packages +================================================== + +The file format is RFC 822 and there are currently three implementations. +We only support reading/writing Metadata v1.0 or v1.2. If 1.1 is encountered +1.1 extra fields will be ignored. + +PEP 241 - Metadata v1.0 +======================= + +- Metadata-Version +- Name +- Version +- Platform (multiple) +- Summary +- Description (optional) +- Keywords (optional) +- Home-page (optional) +- Author (optional) +- Author-email (optional) +- License (optional) + +PEP 345 - Metadata v1.2 +======================= + +# XXX adding codename ? multiple email rfc232 ? + +- Metadata-Version +- Name +- Version +- Platform (multiple) +- Supported-Platform (multiple) +- Summary +- Description (optional) -- changed format +- Keywords (optional) +- Home-page (optional) +- Download-URL +- Author (optional) +- Author-email (optional) +- Maintainer (optional) +- Maintainer-email (optional) +- License (optional) +- Classifier (multiple) -- see PEP 241 +- Requires-Python +- Requires-External (multiple) +- Requires-Dist (multiple) +- Provides-Dist (multiple) +- Obsoletes-Dist (multiple) + +""" +import re import os import sys import platform @@ -6,243 +59,309 @@ from email import message_from_file from tokenize import tokenize, NAME, OP, STRING, ENDMARKER from distutils2.util import rfc822_escape +from distutils2.version import is_valid_predicate -# Encoding used for the PKG-INFO files -PKG_INFO_ENCODING = 'utf-8' +try: + # docutils is installed + from docutils.utils import Reporter + from docutils.parsers.rst import Parser + from docutils import frontend + from docutils import nodes + from StringIO import StringIO + class SilentReporter(Reporter): -class DistributionMetadata(object): - """Dummy class to hold the distribution meta-data: name, version, - author, and so forth. - """ + def __init__(self, source, report_level, halt_level, stream=None, + debug=0, encoding='ascii', error_handler='replace'): + self.messages = [] + Reporter.__init__(self, source, report_level, halt_level, stream, + debug, encoding, error_handler) - _METHOD_BASENAMES = ("name", "version", "author", "author_email", - "maintainer", "maintainer_email", "url", - "license", "description", "long_description", - "keywords", "platforms", "fullname", "contact", - "contact_email", "license", "classifiers", - "download_url", - # PEP 314 - "provides", "requires", "obsoletes", - ) - - def __init__(self, path=None): - if path is not None: - self.read_pkg_file(open(path)) - else: - self.name = None - self.version = None - self.author = None - self.author_email = None - self.maintainer = None - self.maintainer_email = None - self.url = None - self.license = None - self.description = None - self.long_description = None - self.keywords = None - self.platforms = None - self.classifiers = None - self.download_url = None - # PEP 314 - self.provides = None - self.requires = None - self.obsoletes = None - - def read_pkg_file(self, file): - """Reads the metadata values from a file object.""" - msg = message_from_file(file) + def system_message(self, level, message, *children, **kwargs): + self.messages.append((level, message, children, kwargs)) - def _read_field(name): - value = msg[name] - if value == 'UNKNOWN': - return None - return value - - def _read_list(name): - values = msg.get_all(name, None) - if values == []: - return None - return values - - metadata_version = msg['metadata-version'] - self.name = _read_field('name') - self.version = _read_field('version') - self.description = _read_field('summary') - # we are filling author only. - self.author = _read_field('author') - self.maintainer = None - self.author_email = _read_field('author-email') - self.maintainer_email = None - self.url = _read_field('home-page') - self.license = _read_field('license') - - if 'download-url' in msg: - self.download_url = _read_field('download-url') - else: - self.download_url = None + _HAS_DOCUTILS = True +except ImportError: + # docutils is not installed + _HAS_DOCUTILS = False - self.long_description = _read_field('description') - self.description = _read_field('summary') +# Encoding used for the PKG-INFO files +PKG_INFO_ENCODING = 'utf-8' - if 'keywords' in msg: - self.keywords = _read_field('keywords').split(',') +_LINE_PREFIX = re.compile('\n \|') +_241_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', + 'Summary', 'Description', + 'Keywords', 'Home-page', 'Author', 'Author-email', + 'License') + +_345_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', + 'Supported-Platform', 'Summary', 'Description', + 'Keywords', 'Home-page', 'Author', 'Author-email', + 'Maintainer', 'Maintainer-email', 'License', + 'Classifier', 'Download-URL', 'Obsoletes-Dist', + 'Provides-Dist', 'Requires-Dist', 'Requires-Python', + 'Requires-External') + +_ATTR2FIELD = {'metadata_version': 'Metadata-Version', + 'name': 'Name', + 'version': 'Version', + 'platform': 'Platform', + 'supported_platform': 'Supported-Platform', + 'description': 'Summary', + 'long_description': 'Description', + 'keywords': 'Keywords', + 'url': 'Home-page', + 'author': 'Author', + 'author_email': 'Author-email', + 'maintainer': 'Maintainer', + 'maintainer_email': 'Maintainer-email', + 'licence': 'License', + 'classifier': 'Classifier', + 'download_url': 'Download-URL', + 'obsoletes_dist': 'Obsoletes-Dist', + 'provides_dist': 'Provides-Dist', + 'requires_dist': 'Requires-Dist', + 'requires_python': 'Requires-Python', + 'requires_external': 'Requires-External', + 'requires': 'Requires', + 'provides': 'Provides', + 'obsoletes': 'Obsoletes', + } + +_PREDICATE_FIELDS = ('Requires-Dist', 'Obsoletes-Dist', 'Provides-Dist') + +_LISTFIELDS = ('Platform', 'Classifier', 'Obsoletes', + 'Requires', 'Provides', 'Obsoletes-Dist', + 'Provides-Dist', 'Requires-Dist', 'Requires-Python', + 'Requires-External') + +_ELEMENTSFIELD = ('Keywords',) + +_UNICODEFIELDS = ('Author', 'Maintainer', 'Summary', 'Description') - self.platforms = _read_list('platform') - self.classifiers = _read_list('classifier') - # PEP 314 - these fields only exist in 1.1 - if metadata_version == '1.1': - self.requires = _read_list('requires') - self.provides = _read_list('provides') - self.obsoletes = _read_list('obsoletes') - else: - self.requires = None - self.provides = None - self.obsoletes = None - - 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() +class DistributionMetadata(object): + """Distribution meta-data class (1.0 or 1.2). + """ + def __init__(self, path=None, platform_dependant=False): + self._fields = {} + self.version = None + self.docutils_support = _HAS_DOCUTILS + self.platform_dependant = platform_dependant + if path is not None: + self.read(path) - def write_pkg_file(self, file): - """Write the PKG-INFO format data to a file object. - """ - version = '1.0' - if self.provides or self.requires or self.obsoletes: - version = '1.1' - - self._write_field(file, 'Metadata-Version', version) - self._write_field(file, 'Name', self.get_name()) - self._write_field(file, 'Version', self.get_version()) - self._write_field(file, 'Summary', self.get_description()) - self._write_field(file, 'Home-page', self.get_url()) - self._write_field(file, 'Author', self.get_contact()) - self._write_field(file, 'Author-email', self.get_contact_email()) - self._write_field(file, 'License', self.get_license()) - if self.download_url: - self._write_field(file, 'Download-URL', self.download_url) - - long_desc = rfc822_escape(self.get_long_description()) - self._write_field(file, 'Description', long_desc) - - keywords = ','.join(self.get_keywords()) - if keywords: - self._write_field(file, 'Keywords', keywords) - - self._write_list(file, 'Platform', self.get_platforms()) - self._write_list(file, 'Classifier', self.get_classifiers()) - - # PEP 314 - self._write_list(file, 'Requires', self.get_requires()) - self._write_list(file, 'Provides', self.get_provides()) - self._write_list(file, 'Obsoletes', self.get_obsoletes()) + def _guessmetadata_version(self): + for field in self._fields: + if field in _345_FIELDS and field not in _241_FIELDS: + return '1.2' + return '1.0' def _write_field(self, file, name, value): - file.write('%s: %s\n' % (name, self._encode_field(value))) + file.write('%s: %s\n' % (name, value)) def _write_list (self, file, name, values): for value in values: self._write_field(file, name, value) def _encode_field(self, value): - if value is None: - return None if isinstance(value, unicode): return value.encode(PKG_INFO_ENCODING) return str(value) - # -- Metadata query methods ---------------------------------------- - - def get_name(self): - return self.name or "UNKNOWN" - - def get_version(self): - return self.version or "0.0.0" - + def __getitem__(self, name): + return self.get_field(name) + + def __setitem__(self, name, value): + return self.set_field(name, value) + + def _convert_name(self, name): + if name in _241_FIELDS + _345_FIELDS: + return name + name = name.replace('-', '_').lower() + if name in _ATTR2FIELD: + return _ATTR2FIELD[name] + return name + + def _default_value(self, name): + if name in _LISTFIELDS + _ELEMENTSFIELD: + return [] + return 'UNKNOWN' + + def _check_rst_data(self, data): + """Returns warnings when the provided data doesn't compile.""" + source_path = StringIO() + parser = Parser() + settings = frontend.OptionParser().get_default_values() + settings.tab_width = 4 + settings.pep_references = None + settings.rfc_references = None + reporter = SilentReporter(source_path, + settings.report_level, + settings.halt_level, + stream=settings.warning_stream, + debug=settings.debug, + encoding=settings.error_encoding, + error_handler=settings.error_encoding_error_handler) + + document = nodes.document(settings, reporter, source=source_path) + document.note_source(source_path, -1) + try: + parser.parse(data, document) + except AttributeError: + reporter.messages.append((-1, 'Could not finish the parsing.', + '', {})) + + return reporter.messages + + def _platform(self, value): + if not self.platform_dependant or ';' not in value: + return True, value + value, marker = value.split(';') + return _interpret(marker), value + + def _remove_line_prefix(self, value): + return _LINE_PREFIX.sub('\n', value) + + # + # Public APIs + # def get_fullname(self): - return "%s-%s" % (self.get_name(), self.get_version()) - - def get_author(self): - return self._encode_field(self.author) or "UNKNOWN" - - def get_author_email(self): - return self.author_email or "UNKNOWN" - - def get_maintainer(self): - return self._encode_field(self.maintainer) or "UNKNOWN" - - def get_maintainer_email(self): - return self.maintainer_email or "UNKNOWN" - - def get_contact(self): - return (self._encode_field(self.maintainer) or - self._encode_field(self.author) or "UNKNOWN") - - def get_contact_email(self): - return self.maintainer_email or self.author_email or "UNKNOWN" - - def get_url(self): - return self.url or "UNKNOWN" + return '%s-%s' % (self['Name'], self['Version']) - def get_license(self): - return self.license or "UNKNOWN" - get_licence = get_license + def is_metadata_field(self, name): + name = self._convert_name(name) + return name in _241_FIELDS + _345_FIELDS - def get_description(self): - return self._encode_field(self.description) or "UNKNOWN" + def read(self, filepath): + self.read_file(open(filepath)) - def get_long_description(self): - return self._encode_field(self.long_description) or "UNKNOWN" - - def get_keywords(self): - return self.keywords or [] - - def get_platforms(self): - return self.platforms or ["UNKNOWN"] - - def get_classifiers(self): - return self.classifiers or [] - - def get_download_url(self): - return self.download_url or "UNKNOWN" + def read_file(self, fileob): + """Reads the metadata values from a file object.""" + msg = message_from_file(fileob) + version = msg['metadata-version'] + if version in ('1.0', '1.1'): + fields = _241_FIELDS + else: + fields = _345_FIELDS - # PEP 314 - def get_requires(self): - return self.requires or [] + for field in fields: + if field in _LISTFIELDS: + # we can have multiple lines + values = msg.get_all(field) + self.set_field(field, values) + else: + # single line + value = msg[field] + if value is not None: + self.set_field(field, value) - def set_requires(self, value): - import distutils2.versionpredicate - for v in value: - distutils2.versionpredicate.VersionPredicate(v) - self.requires = value + self.version = self._guessmetadata_version() + self.set_field('Metadata-Version', self.version) - def get_provides(self): - return self.provides or [] + def write(self, filepath): + """Write the metadata fields into path. + """ + pkg_info = open(filepath, 'w') + try: + self.write_file(pkg_info) + finally: + pkg_info.close() - def set_provides(self, value): - value = [v.strip() for v in value] - for v in value: - import distutils2.versionpredicate - distutils2.versionpredicate.split_provision(v) - self.provides = value + def write_file(self, fileobject): + """Write the PKG-INFO format data to a file object. + """ + version = self._guessmetadata_version() + if 'Metadata-Version' not in self._fields: + self['Metadata-Version'] = version + if version == '1.0': + fields = _241_FIELDS + else: + fields = _345_FIELDS + for field in fields: + values = self.get_field(field) + if field in _ELEMENTSFIELD: + self._write_field(fileobject, field, ','.join(values)) + continue + if field not in _LISTFIELDS: + if field == 'Description': + values = values.replace('\n', '\n |') + values = [values] + + for value in values: + self._write_field(fileobject, field, value) + + def set_field(self, name, value): + """Controls then sets a metadata field""" + name = self._convert_name(name) + + # XXX need to parse the Requires-Python value + # + if name in _PREDICATE_FIELDS and value is not None: + for v in value: + # check that the values are valid predicates + if not is_valid_predicate(v.split(';')[0]): + raise ValueError('"%s" is not a valid predicate' % v) + if name in _LISTFIELDS + _ELEMENTSFIELD: + if isinstance(value, str): + value = value.split(',') + elif name in _UNICODEFIELDS: + value = self._encode_field(value) + if name == 'Description': + value = self._remove_line_prefix(value) + self._fields[name] = value + + def get_field(self, name): + """Gets a metadata field.""" + name = self._convert_name(name) + if name not in self._fields: + return self._default_value(name) + if name in _UNICODEFIELDS: + value = self._fields[name] + return self._encode_field(value) + elif name in _LISTFIELDS: + value = self._fields[name] + if value is None: + return [] + res = [] + for val in value: + valid, val = self._platform(val) + if not valid: + continue + res.append(self._encode_field(val)) + return res + + elif name in _ELEMENTSFIELD: + valid, value = self._platform(self._fields[name]) + if not valid: + return [] + if isinstance(value, str): + return value.split(',') + valid, value = self._platform(self._fields[name]) + if not valid: + return None + return value - def get_obsoletes(self): - return self.obsoletes or [] + def check(self): + """Checks if the metadata are compliant.""" + missing = [] + for attr in ('Name', 'Version', 'Home-page'): + value = self[attr] + if value == 'UNKNOWN': + missing.append(attr) - def set_obsoletes(self, value): - import distutils2.versionpredicate - for v in value: - distutils2.versionpredicate.VersionPredicate(v) - self.obsoletes = value + if _HAS_DOCUTILS: + warnings = self._check_rst_data(self['Description']) + else: + warnings = [] + return missing, warnings # # micro-language for PEP 345 environment markers # +_STR_LIMIT = "'\"" + class _Operation(object): # restricted set of names @@ -255,7 +374,14 @@ class _Operation(object): 'platform.machine': platform.machine} # allowed operators - ops = {'==': 'op_equal'} + ops = {'==': 'op_equal', + '!=': 'op_nonequal', + '>': 'op_greater', + '>=': 'op_greaterequal', + '<': 'op_less', + '<=': 'op_lessequal', + 'in': 'op_in', + 'not in': 'op_notin'} def __init__(self): self.left = None @@ -268,32 +394,61 @@ class _Operation(object): def op_equal(self, left, right): return left == right + def op_nonequal(self, left, right): + return left != right + + def op_greater(self, left, right): + return left > right + + def op_greaterequal(self, left, right): + return left >= right + + def op_less(self, left, right): + return left < right + + def op_lessequal(self, left, right): + return left <= right + + def op_in(self, left, right): + return left in right + + def op_notin(self, left, right): + return left not in right + def _is_string(self, value): - # XXX need to add " as well - return value.startswith("'") and value.endswith("'") + if value is None or len(value) < 2: + return False + for delimiter in _STR_LIMIT: + if value[0] == value[-1] == delimiter: + return True + return False + + def _is_name(self, value): + return value in self.names def _convert(self, value): if value in self.names: return self.names[value] - return value + return value.strip(_STR_LIMIT) def _check_name(self, value): if value not in self.names: - raise TypeError('Not supported "%s"' % value) + raise NameError(value) + + def _nonsense_op(self): + msg = 'This operation is not supported : "%s"' % str(self) + raise SyntaxError(msg) def __call__(self): + # make sure we do something useful if self._is_string(self.left): - self.left = self.left.strip("'") if self._is_string(self.right): - raise TypeError('Cannot compare two strings') - else: - self._check_name(self.right) + self._nonsense_op() + self._check_name(self.right) else: - if self._is_string(self.right): - self.right = self.right.strip("'") - self._check_name(self.left) - else: - raise TypeError('Cannot compare two strings') + if not self._is_string(self.right): + self._nonsense_op() + self._check_name(self.left) if self.op not in self.ops: raise TypeError('Operator not supported "%s"' % self.op) @@ -339,7 +494,7 @@ class _CHAIN(object): def eat(self, toktype, tokval, rowcol, line, logical_line): if toktype not in (NAME, OP, STRING, ENDMARKER): - raise TypeError('Not supported %s' % line) + raise SyntaxError('Type not supported "%s"' % tokval) if self.op_starting: op = _Operation() @@ -367,7 +522,8 @@ class _CHAIN(object): if isinstance(op, (_OR, _AND)) and op.right is not None: op = op.right - if toktype in (NAME, STRING) or (toktype == OP and tokval == '.'): + if ((toktype in (NAME, STRING) and tokval not in ('in', 'not')) + or (toktype == OP and tokval == '.')): if op.op is None: if op.left is None: op.left = tokval @@ -378,8 +534,11 @@ class _CHAIN(object): op.right = tokval else: op.right += tokval - elif toktype == OP: - op.op = tokval + elif toktype == OP or tokval in ('in', 'not'): + if tokval == 'in' and op.op == 'not': + op.op = 'not in' + else: + op.op = tokval def result(self): for op in self.ops: @@ -389,6 +548,7 @@ class _CHAIN(object): def _interpret(marker): """Interprets a marker and return a result given the environment.""" + marker = marker.strip() operations = _CHAIN() tokenize(StringIO(marker).readline, operations.eat) return operations.result() diff --git a/src/distutils2/tests/LONG_DESC.txt b/src/distutils2/tests/LONG_DESC.txt new file mode 100644 index 0000000..2b4358a --- /dev/null +++ b/src/distutils2/tests/LONG_DESC.txt @@ -0,0 +1,44 @@ +CLVault +======= + +CLVault uses Keyring to provide a command-line utility to safely store +and retrieve passwords. + +Install it using pip or the setup.py script:: + + $ python setup.py install + + $ pip install clvault + +Once it's installed, you will have three scripts installed in your +Python scripts folder, you can use to list, store and retrieve passwords:: + + $ clvault-set blog + Set your password: + Set the associated username (can be blank): tarek + Set a description (can be blank): My blog password + Password set. + + $ clvault-get blog + The username is "tarek" + The password has been copied in your clipboard + + $ clvault-list + Registered services: + blog My blog password + + +*clvault-set* takes a service name then prompt you for a password, and some +optional information about your service. The password is safely stored in +a keyring while the description is saved in a ``.clvault`` file in your +home directory. This file is created automatically the first time the command +is used. + +*clvault-get* copies the password for a given service in your clipboard, and +displays the associated user if any. + +*clvault-list* lists all registered services, with their description when +given. + + +Project page: http://bitbucket.org/tarek/clvault diff --git a/src/distutils2/tests/PKG-INFO b/src/distutils2/tests/PKG-INFO new file mode 100644 index 0000000..f48546e --- /dev/null +++ b/src/distutils2/tests/PKG-INFO @@ -0,0 +1,57 @@ +Metadata-Version: 1.2 +Name: CLVault +Version: 0.5 +Summary: Command-Line utility to store and retrieve passwords +Home-page: http://bitbucket.org/tarek/clvault +Author: Tarek Ziade +Author-email: tarek@ziade.org +License: PSF +Keywords: keyring,password,crypt +Requires-Dist: foo; sys.platform == 'okook' +Requires-Dist: bar; sys.platform == '%s' +Platform: UNKNOWN +Description: CLVault + |======= + | + |CLVault uses Keyring to provide a command-line utility to safely store + |and retrieve passwords. + | + |Install it using pip or the setup.py script:: + | + | $ python setup.py install + | + | $ pip install clvault + | + |Once it's installed, you will have three scripts installed in your + |Python scripts folder, you can use to list, store and retrieve passwords:: + | + | $ clvault-set blog + | Set your password: + | Set the associated username (can be blank): tarek + | Set a description (can be blank): My blog password + | Password set. + | + | $ clvault-get blog + | The username is "tarek" + | The password has been copied in your clipboard + | + | $ clvault-list + | Registered services: + | blog My blog password + | + | + |*clvault-set* takes a service name then prompt you for a password, and some + |optional information about your service. The password is safely stored in + |a keyring while the description is saved in a ``.clvault`` file in your + |home directory. This file is created automatically the first time the command + |is used. + | + |*clvault-get* copies the password for a given service in your clipboard, and + |displays the associated user if any. + | + |*clvault-list* lists all registered services, with their description when + |given. + | + | + |Project page: http://bitbucket.org/tarek/clvault + | diff --git a/src/distutils2/tests/test_ccompiler.py b/src/distutils2/tests/test_ccompiler.py index d34f4de..1f3d25d 100644 --- a/src/distutils2/tests/test_ccompiler.py +++ b/src/distutils2/tests/test_ccompiler.py @@ -5,7 +5,6 @@ from distutils2.tests import captured_stdout from distutils2.compiler.ccompiler import (gen_lib_options, CCompiler, get_default_compiler, customize_compiler) -from distutils2 import debug from distutils2.tests import support class FakeCompiler(object): @@ -34,22 +33,6 @@ class CCompilerTestCase(support.EnvironGuard, unittest2.TestCase): '-lname2'] self.assertEquals(opts, wanted) - def test_debug_print(self): - - class MyCCompiler(CCompiler): - executables = {} - - compiler = MyCCompiler() - __, stdout = captured_stdout(compiler.debug_print, 'xxx') - self.assertEquals(stdout, '') - - debug.DEBUG = True - try: - __, stdout = captured_stdout(compiler.debug_print, 'xxx') - self.assertEquals(stdout, 'xxx\n') - finally: - debug.DEBUG = False - def test_customize_compiler(self): # not testing if default compiler is not unix diff --git a/src/distutils2/tests/test_check.py b/src/distutils2/tests/test_check.py index 5fd15b5..a755607 100644 --- a/src/distutils2/tests/test_check.py +++ b/src/distutils2/tests/test_check.py @@ -1,7 +1,8 @@ """Tests for distutils.command.check.""" import unittest2 -from distutils2.command.check import check, HAS_DOCUTILS +from distutils2.command.check import check +from distutils2.metadata import _HAS_DOCUTILS from distutils2.tests import support from distutils2.errors import DistutilsSetupError @@ -26,14 +27,15 @@ class CheckTestCase(support.LoggingSilencer, # by default, check is checking the metadata # should have some warnings cmd = self._run() - self.assertEquals(cmd._warnings, 2) + self.assert_(cmd._warnings > 0) # now let's add the required fields # and run it again, to make sure we don't get # any warning anymore metadata = {'url': 'xxx', 'author': 'xxx', 'author_email': 'xxx', - 'name': 'xxx', 'version': 'xxx'} + 'name': 'xxx', 'version': 'xxx' + } cmd = self._run(metadata) self.assertEquals(cmd._warnings, 0) @@ -46,7 +48,7 @@ class CheckTestCase(support.LoggingSilencer, self.assertEquals(cmd._warnings, 0) def test_check_document(self): - if not HAS_DOCUTILS: # won't test without docutils + if not _HAS_DOCUTILS: # won't test without docutils return pkg_info, dist = self.create_dist() cmd = check(dist) @@ -62,7 +64,7 @@ class CheckTestCase(support.LoggingSilencer, self.assertEquals(len(msgs), 0) def test_check_restructuredtext(self): - if not HAS_DOCUTILS: # won't test without docutils + if not _HAS_DOCUTILS: # won't test without docutils return # let's see if it detects broken rest in long_description broken_rest = 'title\n===\n\ntest' diff --git a/src/distutils2/tests/test_cmd.py b/src/distutils2/tests/test_cmd.py index 2163e63..e0e3241 100644 --- a/src/distutils2/tests/test_cmd.py +++ b/src/distutils2/tests/test_cmd.py @@ -6,7 +6,6 @@ from distutils2.tests import captured_stdout, run_unittest from distutils2.cmd import Command from distutils2.dist import Distribution from distutils2.errors import DistutilsOptionError -from distutils2 import debug class MyCmd(Command): def initialize_options(self): @@ -104,18 +103,6 @@ class CommandTestCase(unittest2.TestCase): cmd.option2 = 'xxx' self.assertRaises(DistutilsOptionError, cmd.ensure_dirname, 'option2') - def test_debug_print(self): - cmd = self.cmd - __, stdout = captured_stdout(cmd.debug_print, 'xxx') - self.assertEquals(stdout, '') - - debug.DEBUG = True - try: - __, stdout = captured_stdout(cmd.debug_print, 'xxx') - self.assertEquals(stdout, 'xxx\n') - finally: - debug.DEBUG = False - def test_suite(): return unittest2.makeSuite(CommandTestCase) diff --git a/src/distutils2/tests/test_core.py b/src/distutils2/tests/test_core.py index a50fb54..cf9fc26 100644 --- a/src/distutils2/tests/test_core.py +++ b/src/distutils2/tests/test_core.py @@ -78,21 +78,6 @@ class CoreTestCase(support.EnvironGuard, unittest2.TestCase): output = output[:-1] self.assertEqual(cwd, output) - def test_debug_mode(self): - # this covers the code called when DEBUG is set - sys.argv = ['setup.py', '--name'] - __, stdout = captured_stdout(distutils2.core.setup, name='bar') - self.assertEquals(stdout, 'bar\n') - - distutils2.core.DEBUG = True - try: - __, stdout = captured_stdout(distutils2.core.setup, name='bar') - finally: - distutils2.core.DEBUG = False - wanted = "options (after parsing config files):" - lines = stdout.split('\n') - self.assertEquals(lines[0], wanted) - def test_suite(): return unittest2.makeSuite(CoreTestCase) diff --git a/src/distutils2/tests/test_dist.py b/src/distutils2/tests/test_dist.py index 1406d0b..fcf333b 100644 --- a/src/distutils2/tests/test_dist.py +++ b/src/distutils2/tests/test_dist.py @@ -141,7 +141,7 @@ class DistributionTestCase(support.TempdirManager, # let's make sure the file can be written # with Unicode fields. they are encoded with # PKG_INFO_ENCODING - dist.metadata.write_pkg_file(open(my_file, 'w')) + dist.metadata.write_file(open(my_file, 'w')) # regular ascii is of course always usable dist = klass(attrs={'author': 'Mister Cafe', @@ -151,7 +151,7 @@ class DistributionTestCase(support.TempdirManager, 'long_description': 'Hehehe'}) my_file2 = os.path.join(tmp_dir, 'f2') - dist.metadata.write_pkg_file(open(my_file, 'w')) + dist.metadata.write_file(open(my_file, 'w')) def test_empty_options(self): # an empty options dictionary should not stay in the @@ -179,14 +179,14 @@ class DistributionTestCase(support.TempdirManager, def test_finalize_options(self): attrs = {'keywords': 'one,two', - 'platforms': 'one,two'} + 'platform': 'one,two'} dist = Distribution(attrs=attrs) dist.finalize_options() # finalize_option splits platforms and keywords - self.assertEquals(dist.metadata.platforms, ['one', 'two']) - self.assertEquals(dist.metadata.keywords, ['one', 'two']) + self.assertEquals(dist.metadata['platform'], ['one', 'two']) + self.assertEquals(dist.metadata['keywords'], ['one', 'two']) def test_get_command_packages(self): dist = Distribution() @@ -263,65 +263,62 @@ class MetadataTestCase(support.TempdirManager, support.EnvironGuard, self.assertTrue("requires:" not in meta.lower()) self.assertTrue("obsoletes:" not in meta.lower()) - def test_provides(self): + def test_provides_dist(self): attrs = {"name": "package", "version": "1.0", - "provides": ["package", "package.sub"]} + "provides_dist": ["package", "package.sub"]} dist = Distribution(attrs) - self.assertEqual(dist.metadata.get_provides(), - ["package", "package.sub"]) - self.assertEqual(dist.get_provides(), + self.assertEqual(dist.metadata['Provides-Dist'], ["package", "package.sub"]) meta = self.format_metadata(dist) - self.assertTrue("Metadata-Version: 1.1" in meta) + self.assertTrue("Metadata-Version: 1.2" in meta) self.assertTrue("requires:" not in meta.lower()) self.assertTrue("obsoletes:" not in meta.lower()) - def test_provides_illegal(self): + def _test_provides_illegal(self): + # XXX to do: check the versions self.assertRaises(ValueError, Distribution, {"name": "package", "version": "1.0", - "provides": ["my.pkg (splat)"]}) + "provides_dist": ["my.pkg (splat)"]}) - def test_requires(self): + def test_requires_dist(self): attrs = {"name": "package", "version": "1.0", - "requires": ["other", "another (==1.0)"]} + "requires_dist": ["other", "another (==1.0)"]} dist = Distribution(attrs) - self.assertEqual(dist.metadata.get_requires(), - ["other", "another (==1.0)"]) - self.assertEqual(dist.get_requires(), + self.assertEqual(dist.metadata['Requires-Dist'], ["other", "another (==1.0)"]) meta = self.format_metadata(dist) - self.assertTrue("Metadata-Version: 1.1" in meta) + self.assertTrue("Metadata-Version: 1.2" in meta) self.assertTrue("provides:" not in meta.lower()) - self.assertTrue("Requires: other" in meta) - self.assertTrue("Requires: another (==1.0)" in meta) + self.assertTrue("Requires-Dist: other" in meta) + self.assertTrue("Requires-Dist: another (==1.0)" in meta) self.assertTrue("obsoletes:" not in meta.lower()) - def test_requires_illegal(self): + def _test_requires_illegal(self): + # XXX self.assertRaises(ValueError, Distribution, {"name": "package", "version": "1.0", "requires": ["my.pkg (splat)"]}) - def test_obsoletes(self): + def test_obsoletes_dist(self): attrs = {"name": "package", "version": "1.0", - "obsoletes": ["other", "another (<1.0)"]} + "obsoletes_dist": ["other", "another (<1.0)"]} dist = Distribution(attrs) - self.assertEqual(dist.metadata.get_obsoletes(), - ["other", "another (<1.0)"]) - self.assertEqual(dist.get_obsoletes(), + self.assertEqual(dist.metadata['Obsoletes-Dist'], ["other", "another (<1.0)"]) meta = self.format_metadata(dist) - self.assertTrue("Metadata-Version: 1.1" in meta) + self.assertTrue("Metadata-Version: 1.2" in meta) self.assertTrue("provides:" not in meta.lower()) self.assertTrue("requires:" not in meta.lower()) - self.assertTrue("Obsoletes: other" in meta) - self.assertTrue("Obsoletes: another (<1.0)" in meta) + self.assertTrue("Obsoletes-Dist: other" in meta) + self.assertTrue("Obsoletes-Dist: another (<1.0)" in meta) - def test_obsoletes_illegal(self): + def _test_obsoletes_illegal(self): + # XXX self.assertRaises(ValueError, Distribution, {"name": "package", "version": "1.0", @@ -329,7 +326,7 @@ class MetadataTestCase(support.TempdirManager, support.EnvironGuard, def format_metadata(self, dist): sio = StringIO.StringIO() - dist.metadata.write_pkg_file(sio) + dist.metadata.write_file(sio) return sio.getvalue() def test_custom_pydistutils(self): @@ -394,7 +391,7 @@ class MetadataTestCase(support.TempdirManager, support.EnvironGuard, dist = distutils2.dist.Distribution(attrs) meta = self.format_metadata(dist) - meta = meta.replace('\n' + 8 * ' ', '\n') + meta = meta.replace('\n' + 7 * ' ' + '|', '\n') self.assertTrue(long_desc in meta) def test_read_metadata(self): @@ -404,25 +401,25 @@ class MetadataTestCase(support.TempdirManager, support.EnvironGuard, "description": "xxx", "download_url": "http://example.com", "keywords": ['one', 'two'], - "requires": ['foo']} + "requires_dist": ['foo']} dist = Distribution(attrs) metadata = dist.metadata # write it then reloads it PKG_INFO = StringIO.StringIO() - metadata.write_pkg_file(PKG_INFO) + metadata.write_file(PKG_INFO) PKG_INFO.seek(0) - metadata.read_pkg_file(PKG_INFO) - - self.assertEquals(metadata.name, "package") - self.assertEquals(metadata.version, "1.0") - self.assertEquals(metadata.description, "xxx") - self.assertEquals(metadata.download_url, 'http://example.com') - self.assertEquals(metadata.keywords, ['one', 'two']) - self.assertEquals(metadata.platforms, ['UNKNOWN']) - self.assertEquals(metadata.obsoletes, None) - self.assertEquals(metadata.requires, ['foo']) + + metadata.read_file(PKG_INFO) + self.assertEquals(metadata['name'], "package") + self.assertEquals(metadata['version'], "1.0") + self.assertEquals(metadata['description'], "xxx") + self.assertEquals(metadata['download_url'], 'http://example.com') + self.assertEquals(metadata['keywords'], ['one', 'two']) + self.assertEquals(metadata['platform'], []) + self.assertEquals(metadata['obsoletes'], []) + self.assertEquals(metadata['requires-dist'], ['foo']) def test_suite(): suite = unittest2.TestSuite() diff --git a/src/distutils2/tests/test_filelist.py b/src/distutils2/tests/test_filelist.py index 31b3ca9..5fd15a8 100644 --- a/src/distutils2/tests/test_filelist.py +++ b/src/distutils2/tests/test_filelist.py @@ -4,7 +4,6 @@ import unittest2 from distutils2.tests import captured_stdout from distutils2.filelist import glob_to_re, FileList -from distutils2 import debug MANIFEST_IN = """\ include ok @@ -63,18 +62,6 @@ class FileListTestCase(unittest2.TestCase): self.assertEquals(file_list.files, wanted) - def test_debug_print(self): - file_list = FileList() - __, stdout = captured_stdout(file_list.debug_print, 'xxx') - self.assertEquals(stdout, '') - - debug.DEBUG = True - try: - __, stdout = captured_stdout(file_list.debug_print, 'xxx') - self.assertEquals(stdout, 'xxx\n') - finally: - debug.DEBUG = False - def test_suite(): return unittest2.makeSuite(FileListTestCase) diff --git a/src/distutils2/tests/test_metadata.py b/src/distutils2/tests/test_metadata.py index 4b3dbbc..f259463 100644 --- a/src/distutils2/tests/test_metadata.py +++ b/src/distutils2/tests/test_metadata.py @@ -2,6 +2,7 @@ import unittest2 import os import sys +from StringIO import StringIO from distutils2.metadata import DistributionMetadata, _interpret @@ -11,6 +12,8 @@ class DistributionMetadataTestCase(unittest2.TestCase): def test_interpret(self): platform = sys.platform version = sys.version.split()[0] + os_name = os.name + assert _interpret("sys.platform == '%s'" % platform) assert _interpret("sys.platform == '%s' or python_version == '2.4'" \ % platform) @@ -19,8 +22,76 @@ class DistributionMetadataTestCase(unittest2.TestCase): % (platform, version)) assert _interpret("'%s' == sys.platform" % platform) - # need to test errors, and " and various forms - # and add other operators + assert _interpret('os.name == "%s"' % os_name) + + # stuff that need to raise a syntax error + ops = ('os.name == os.name', 'os.name == 2', "'2' == '2'", + 'okpjonon', '', 'os.name ==') + for op in ops: + self.assertRaises(SyntaxError, _interpret, op) + + # combined operations + OP = 'os.name == "%s"' % os_name + AND = ' and ' + OR = ' or ' + assert _interpret(OP+AND+OP) + assert _interpret(OP+AND+OP+AND+OP) + assert _interpret(OP+OR+OP) + assert _interpret(OP+OR+OP+OR+OP) + + # other operators + assert _interpret("os.name != 'buuuu'") + assert _interpret("python_version > '1.0'") + assert _interpret("python_version < '5.0'") + assert _interpret("python_version <= '5.0'") + assert _interpret("python_version >= '1.0'") + assert _interpret("'%s' in os.name" % os_name) + assert _interpret("'buuuu' not in os.name") + assert _interpret("'buuuu' not in os.name and '%s' in os.name" \ + % os_name) + + + def test_metadata_read_write(self): + + PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO') + metadata = DistributionMetadata(PKG_INFO) + res = StringIO() + metadata.write_file(res) + res.seek(0) + res = res.read() + f = open(PKG_INFO) + wanted = f.read() + self.assert_('Keywords: keyring,password,crypt' in res) + f.close() + + def test_metadata_markers(self): + # see if we can be platform-aware + PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO') + content = open(PKG_INFO).read() + content = content % sys.platform + metadata = DistributionMetadata(platform_dependant=True) + metadata.read_file(StringIO(content)) + self.assertEquals(metadata['Requires-Dist'], ['bar']) + + def test_description(self): + PKG_INFO = os.path.join(os.path.dirname(__file__), 'PKG-INFO') + content = open(PKG_INFO).read() + content = content % sys.platform + metadata = DistributionMetadata() + metadata.read_file(StringIO(content)) + + # see if we can read the description now + DESC = os.path.join(os.path.dirname(__file__), 'LONG_DESC.txt') + wanted = open(DESC).read() + self.assertEquals(wanted, metadata['Description']) + + # save the file somewhere and make sure we can read it back + out = StringIO() + metadata.write_file(out) + out.seek(0) + metadata.read_file(out) + self.assertEquals(wanted, metadata['Description']) + def test_suite(): return unittest2.makeSuite(DistributionMetadataTestCase) diff --git a/src/distutils2/tests/test_pypi_versions.py b/src/distutils2/tests/test_pypi_versions.py new file mode 100644 index 0000000..eb5634c --- /dev/null +++ b/src/distutils2/tests/test_pypi_versions.py @@ -0,0 +1,120 @@ +# +## test_pypi_versions.py +## +## A very simple test to see what percentage of the current pypi packages +## have versions that can be converted automatically by distutils' new +## suggest_normalized_version() into PEP-386 compatible versions. +## +## Requires : Python 2.5+ +## +## Written by: ssteinerX@gmail.com +# + +try: + import cPickle as pickle +except: + import pickle + +import xmlrpclib +import os.path +import unittest2 + +from distutils2.version import suggest_normalized_version + +def test_pypi(): + # + ## To re-run from scratch, just delete these two .pkl files + # + INDEX_PICKLE_FILE = 'pypi-index.pkl' + VERSION_PICKLE_FILE = 'pypi-version.pkl' + + package_info = version_info = [] + + # + ## if there's a saved version of the package list + ## restore it + ## else: + ## pull the list down from pypi + ## save a pickled version of it + # + if os.path.exists(INDEX_PICKLE_FILE): + print "Loading saved pypi data..." + with open(INDEX_PICKLE_FILE, 'rb') as f: + package_info = pickle.load(f) + else: + print "Retrieving pypi packages..." + server = xmlrpclib.Server('http://pypi.python.org/pypi') + package_info = server.search({'name': ''}) + + print "Saving package info..." + with open(INDEX_PICKLE_FILE, 'wb') as o: + pickle.dump(package_info, o) + + # + ## If there's a saved list of the versions from the packages + ## restore it + ## else + ## extract versions from the package list + ## save a pickled version of it + # + versions = [] + if os.path.exists(VERSION_PICKLE_FILE): + print "Loading saved version info..." + with open(VERSION_PICKLE_FILE, 'rb') as f: + versions = pickle.load(f) + else: + print "Extracting and saving version info..." + versions = [p['version'] for p in package_info] + with open(VERSION_PICKLE_FILE, 'wb') as o: + pickle.dump(versions, o) + + total_versions = len(versions) + matches = 0.00 + no_sugg = 0.00 + have_sugg = 0.00 + + suggs = [] + no_suggs = [] + + for ver in versions: + sugg = suggest_normalized_version(ver) + if sugg == ver: + matches += 1 + elif sugg == None: + no_sugg += 1 + no_suggs.append(ver) + else: + have_sugg += 1 + suggs.append((ver, sugg)) + + pct = "(%2.2f%%)" + print "Results:" + print "--------" + print "" + print "Suggestions" + print "-----------" + print "" + for ver, sugg in suggs: + print "%s -> %s" % (ver, sugg) + print "" + print "No suggestions" + print "--------------" + for ver in no_suggs: + print ver + print "" + print "Summary:" + print "--------" + print "Total Packages : ", total_versions + print "Already Match : ", matches, pct % (matches/total_versions*100,) + print "Have Suggestion : ", have_sugg, pct % (have_sugg/total_versions*100,) + print "No Suggestion : ", no_sugg, pct % (no_sugg/total_versions*100,) + +class TestPyPI(unittest2.TestCase): + pass + +def test_suite(): + return unittest2.makeSuite(TestPyPI) + +if __name__ == '__main__': + run_unittest(test_suite()) + diff --git a/src/distutils2/tests/test_sdist.py b/src/distutils2/tests/test_sdist.py index 53e381c..df429a2 100644 --- a/src/distutils2/tests/test_sdist.py +++ b/src/distutils2/tests/test_sdist.py @@ -248,7 +248,7 @@ class SDistTestCase(PyPIRCCommandTestCase): cmd.ensure_finalized() cmd.run() warnings = self.get_logs(WARN) - self.assertEquals(len(warnings), 2) + self.assertEquals(len(warnings), 1) # trying with a complete set of metadata self.clear_logs() diff --git a/src/distutils2/tests/test_upload.py b/src/distutils2/tests/test_upload.py index d7fb3e9..29f5297 100644 --- a/src/distutils2/tests/test_upload.py +++ b/src/distutils2/tests/test_upload.py @@ -116,7 +116,7 @@ class uploadTestCase(PyPIRCCommandTestCase): # what did we send ? self.assertIn('dédé', self.last_open.req.data) headers = dict(self.last_open.req.headers) - self.assertEquals(headers['Content-length'], '2085') + self.assert_(headers['Content-length'] > 2000) self.assertTrue(headers['Content-type'].startswith('multipart/form-data')) self.assertEquals(self.last_open.req.get_method(), 'POST') self.assertEquals(self.last_open.req.get_full_url(), diff --git a/src/distutils2/tests/test_util.py b/src/distutils2/tests/test_util.py index e7e77d6..b146b15 100644 --- a/src/distutils2/tests/test_util.py +++ b/src/distutils2/tests/test_util.py @@ -14,7 +14,6 @@ from distutils2.util import (convert_path, change_root, byte_compile) from distutils2 import util from distutils2.tests import support -from distutils2.version import LooseVersion class FakePopen(object): test_class = None diff --git a/src/distutils2/tests/test_version.py b/src/distutils2/tests/test_version.py index 41278bd..2f0f54f 100644 --- a/src/distutils2/tests/test_version.py +++ b/src/distutils2/tests/test_version.py @@ -1,70 +1,161 @@ """Tests for distutils.version.""" -import unittest2 -from distutils2.version import LooseVersion -from distutils2.version import StrictVersion - -class VersionTestCase(unittest2.TestCase): - - def test_prerelease(self): - version = StrictVersion('1.2.3a1') - self.assertEquals(version.version, (1, 2, 3)) - self.assertEquals(version.prerelease, ('a', 1)) - self.assertEquals(str(version), '1.2.3a1') - - version = StrictVersion('1.2.0') - self.assertEquals(str(version), '1.2') - - def test_cmp_strict(self): - versions = (('1.5.1', '1.5.2b2', -1), - ('161', '3.10a', ValueError), - ('8.02', '8.02', 0), - ('3.4j', '1996.07.12', ValueError), - ('3.2.pl0', '3.1.1.6', ValueError), - ('2g6', '11g', ValueError), - ('0.9', '2.2', -1), - ('1.2.1', '1.2', 1), - ('1.1', '1.2.2', -1), - ('1.2', '1.1', 1), - ('1.2.1', '1.2.2', -1), - ('1.2.2', '1.2', 1), - ('1.2', '1.2.2', -1), - ('0.4.0', '0.4', 0), - ('1.13++', '5.5.kw', ValueError)) - - for v1, v2, wanted in versions: - try: - res = StrictVersion(v1).__cmp__(StrictVersion(v2)) - except ValueError: - if wanted is ValueError: - continue - else: - raise AssertionError(("cmp(%s, %s) " - "shouldn't raise ValueError") - % (v1, v2)) - self.assertEquals(res, wanted, - 'cmp(%s, %s) should be %s, got %s' % - (v1, v2, wanted, res)) - - - def test_cmp(self): - versions = (('1.5.1', '1.5.2b2', -1), - ('161', '3.10a', 1), - ('8.02', '8.02', 0), - ('3.4j', '1996.07.12', -1), - ('3.2.pl0', '3.1.1.6', 1), - ('2g6', '11g', -1), - ('0.960923', '2.2beta29', -1), - ('1.13++', '5.5.kw', -1)) - - - for v1, v2, wanted in versions: - res = LooseVersion(v1).__cmp__(LooseVersion(v2)) - self.assertEquals(res, wanted, - 'cmp(%s, %s) should be %s, got %s' % - (v1, v2, wanted, res)) +import unittest +import doctest +import os + +from distutils2.version import NormalizedVersion as V +from distutils2.version import IrrationalVersionError +from distutils2.version import suggest_normalized_version as suggest +from distutils2.version import VersionPredicate + +class VersionTestCase(unittest.TestCase): + + versions = ((V('1.0'), '1.0'), + (V('1.1'), '1.1'), + (V('1.2.3'), '1.2.3'), + (V('1.2'), '1.2'), + (V('1.2.3a4'), '1.2.3a4'), + (V('1.2c4'), '1.2c4'), + (V('1.2.3.4'), '1.2.3.4'), + (V('1.2.3.4.0b3'), '1.2.3.4b3'), + (V('1.2.0.0.0'), '1.2'), + (V('1.0.dev345'), '1.0.dev345'), + (V('1.0.post456.dev623'), '1.0.post456.dev623')) + + def test_basic_versions(self): + + for v, s in self.versions: + self.assertEquals(str(v), s) + + def test_from_parts(self): + + for v, s in self.versions: + parts = v.parts + v2 = V.from_parts(*v.parts) + self.assertEquals(v, v2) + self.assertEquals(str(v), str(v2)) + + def test_irrational_versions(self): + + irrational = ('1', '1.2a', '1.2.3b', '1.02', '1.2a03', + '1.2a3.04', '1.2.dev.2', '1.2dev', '1.2.dev', + '1.2.dev2.post2', '1.2.post2.dev3.post4') + + for s in irrational: + self.assertRaises(IrrationalVersionError, V, s) + + def test_comparison(self): + r""" + >>> V('1.2.0') == '1.2' + Traceback (most recent call last): + ... + TypeError: cannot compare NormalizedVersion and str + + >>> V('1.2.0') == V('1.2') + True + >>> V('1.2.0') == V('1.2.3') + False + >>> V('1.2.0') < V('1.2.3') + True + >>> (V('1.0') > V('1.0b2')) + True + >>> (V('1.0') > V('1.0c2') > V('1.0c1') > V('1.0b2') > V('1.0b1') + ... > V('1.0a2') > V('1.0a1')) + True + >>> (V('1.0.0') > V('1.0.0c2') > V('1.0.0c1') > V('1.0.0b2') > V('1.0.0b1') + ... > V('1.0.0a2') > V('1.0.0a1')) + True + + >>> V('1.0') < V('1.0.post456.dev623') + True + + >>> V('1.0.post456.dev623') < V('1.0.post456') < V('1.0.post1234') + True + + >>> (V('1.0a1') + ... < V('1.0a2.dev456') + ... < V('1.0a2') + ... < V('1.0a2.1.dev456') # e.g. need to do a quick post release on 1.0a2 + ... < V('1.0a2.1') + ... < V('1.0b1.dev456') + ... < V('1.0b2') + ... < V('1.0c1.dev456') + ... < V('1.0c1') + ... < V('1.0.dev7') + ... < V('1.0.dev18') + ... < V('1.0.dev456') + ... < V('1.0.dev1234') + ... < V('1.0') + ... < V('1.0.post456.dev623') # development version of a post release + ... < V('1.0.post456')) + True + """ + # must be a simpler way to call the docstrings + doctest.run_docstring_examples(self.test_comparison, globals(), + name='test_comparison') + + def test_suggest_normalized_version(self): + + self.assertEquals(suggest('1.0'), '1.0') + self.assertEquals(suggest('1.0-alpha1'), '1.0a1') + self.assertEquals(suggest('1.0c2'), '1.0c2') + self.assertEquals(suggest('walla walla washington'), None) + self.assertEquals(suggest('2.4c1'), '2.4c1') + + # from setuptools + self.assertEquals(suggest('0.4a1.r10'), '0.4a1.post10') + self.assertEquals(suggest('0.7a1dev-r66608'), '0.7a1.dev66608') + self.assertEquals(suggest('0.6a9.dev-r41475'), '0.6a9.dev41475') + self.assertEquals(suggest('2.4preview1'), '2.4c1') + self.assertEquals(suggest('2.4pre1') , '2.4c1') + self.assertEquals(suggest('2.1-rc2'), '2.1c2') + + # from pypi + self.assertEquals(suggest('0.1dev'), '0.1.dev0') + self.assertEquals(suggest('0.1.dev'), '0.1.dev0') + + # we want to be able to parse Twisted + # development versions are like post releases in Twisted + self.assertEquals(suggest('9.0.0+r2363'), '9.0.0.post2363') + + # pre-releases are using markers like "pre1" + self.assertEquals(suggest('9.0.0pre1'), '9.0.0c1') + + # we want to be able to parse Tcl-TK + # they us "p1" "p2" for post releases + self.assertEquals(suggest('1.4p1'), '1.4.post1') + + def test_predicate(self): + # VersionPredicate knows how to parse stuff like: + # + # Project (>=version, ver2) + + predicates = ('zope.interface (>3.5.0)', + 'AnotherProject (3.4)', + 'OtherProject (<3.0)', + 'NoVersion', + 'Hey (>=2.5,<2.7)') + + for predicate in predicates: + v = VersionPredicate(predicate) + + assert VersionPredicate('Hey (>=2.5,<2.7)').match('2.6') + assert VersionPredicate('Ho').match('2.6') + assert not VersionPredicate('Hey (>=2.5,!=2.6,<2.7)').match('2.6') + assert VersionPredicate('Ho (<3.0)').match('2.6') + assert VersionPredicate('Ho (<3.0,!=2.5)').match('2.6.0') + assert not VersionPredicate('Ho (<3.0,!=2.6)').match('2.6.0') + + + # XXX need to silent the micro version in this case + #assert not VersionPredicate('Ho (<3.0,!=2.6)').match('2.6.3') def test_suite(): - return unittest2.makeSuite(VersionTestCase) + #README = os.path.join(os.path.dirname(__file__), 'README.txt') + #suite = [doctest.DocFileSuite(README), unittest.makeSuite(VersionTestCase)] + suite = [unittest.makeSuite(VersionTestCase)] + return unittest.TestSuite(suite) if __name__ == "__main__": - unittest2.main(defaultTest="test_suite") + unittest.main(defaultTest="test_suite") + diff --git a/src/distutils2/tests/test_versionpredicate.py b/src/distutils2/tests/test_versionpredicate.py deleted file mode 100644 index 50c59fa..0000000 --- a/src/distutils2/tests/test_versionpredicate.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Tests harness for distutils2.versionpredicate. - -""" - -import distutils2.versionpredicate -import doctest - -def test_suite(): - return doctest.DocTestSuite(distutils2.versionpredicate) diff --git a/src/distutils2/util.py b/src/distutils2/util.py index 5110697..6353814 100644 --- a/src/distutils2/util.py +++ b/src/distutils2/util.py @@ -11,7 +11,6 @@ import sys, os, string, re from distutils2.errors import DistutilsPlatformError from distutils2.spawn import spawn, find_executable from distutils2 import log -from distutils2.version import LooseVersion from distutils2.errors import DistutilsByteCompileError from distutils2._backport import sysconfig as _sysconfig @@ -484,13 +483,13 @@ def _find_exe_version(cmd, pattern=_RE_VERSION): result = pattern.search(out_string) if result is None: return None - return LooseVersion(result.group(1)) + return result.group(1) def get_compiler_versions(): """Returns a tuple providing the versions of gcc, ld and dllwrap For each command, if a command is not found, None is returned. - Otherwise a LooseVersion instance is returned. + Otherwise a string with the version is returned. """ gcc = _find_exe_version('gcc -dumpversion') ld = _find_ld_version() diff --git a/src/distutils2/version.py b/src/distutils2/version.py index 5efe144..bbc5179 100644 --- a/src/distutils2/version.py +++ b/src/distutils2/version.py @@ -1,299 +1,380 @@ -# -# distutils/version.py -# -# Implements multiple version numbering conventions for the -# Python Module Distribution Utilities. -# -# $Id: version.py 70642 2009-03-28 00:48:48Z georg.brandl $ -# - -"""Provides classes to represent module version numbers (one class for -each style of version numbering). There are currently two such classes -implemented: StrictVersion and LooseVersion. - -Every version number class implements the following interface: - * the 'parse' method takes a string and parses it to some internal - representation; if the string is an invalid version number, - 'parse' raises a ValueError exception - * the class constructor takes an optional string argument which, - if supplied, is passed to 'parse' - * __str__ reconstructs the string that was passed to 'parse' (or - an equivalent string -- ie. one that will generate an equivalent - version number instance) - * __repr__ generates Python code to recreate the version number instance - * __cmp__ compares the current instance with either another instance - of the same class or a string (which will be parsed to an instance - of the same class, thus must follow the same rules) -""" - -import string, re -from types import StringType - -class Version: - """Abstract base class for version numbering classes. Just provides - constructor (__init__) and reproducer (__repr__), because those - seem to be the same for all version numbering classes. - """ - - def __init__ (self, vstring=None): - if vstring: - self.parse(vstring) - - def __repr__ (self): - return "%s ('%s')" % (self.__class__.__name__, str(self)) - - -# Interface for version-number classes -- must be implemented -# by the following classes (the concrete ones -- Version should -# be treated as an abstract class). -# __init__ (string) - create and take same action as 'parse' -# (string parameter is optional) -# parse (string) - convert a string representation to whatever -# internal representation is appropriate for -# this style of version numbering -# __str__ (self) - convert back to a string; should be very similar -# (if not identical to) the string supplied to parse -# __repr__ (self) - generate Python code to recreate -# the instance -# __cmp__ (self, other) - compare two version numbers ('other' may -# be an unparsed version string, or another -# instance of your version class) - - -class StrictVersion (Version): - - """Version numbering for anal retentives and software idealists. - Implements the standard interface for version number classes as - described above. A version number consists of two or three - dot-separated numeric components, with an optional "pre-release" tag - on the end. The pre-release tag consists of the letter 'a' or 'b' - followed by a number. If the numeric components of two version - numbers are equal, then one with a pre-release tag will always - be deemed earlier (lesser) than one without. - - The following are valid version numbers (shown in the order that - would be obtained by sorting according to the supplied cmp function): - - 0.4 0.4.0 (these two are equivalent) - 0.4.1 - 0.5a1 - 0.5b3 - 0.5 - 0.9.6 - 1.0 - 1.0.4a3 - 1.0.4b1 - 1.0.4 - - The following are examples of invalid version numbers: - - 1 - 2.7.2.2 - 1.3.a4 - 1.3pl1 - 1.3c4 - - The rationale for this version numbering system will be explained - in the distutils documentation. - """ +import sys +import re - version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$', - re.VERBOSE) +class IrrationalVersionError(Exception): + """This is an irrational version.""" + pass +class HugeMajorVersionNumError(IrrationalVersionError): + """An irrational version because the major version number is huge + (often because a year or date was used). - def parse (self, vstring): - match = self.version_re.match(vstring) + See `error_on_huge_major_num` option in `NormalizedVersion` for details. + This guard can be disabled by setting that option False. + """ + pass + +# A marker used in the second and third parts of the `parts` tuple, for +# versions that don't have those segments, to sort properly. An example +# of versions in sort order ('highest' last): +# 1.0b1 ((1,0), ('b',1), ('f',)) +# 1.0.dev345 ((1,0), ('f',), ('dev', 345)) +# 1.0 ((1,0), ('f',), ('f',)) +# 1.0.post256.dev345 ((1,0), ('f',), ('f', 'post', 256, 'dev', 345)) +# 1.0.post345 ((1,0), ('f',), ('f', 'post', 345, 'f')) +# ^ ^ ^ +# 'b' < 'f' ---------------------/ | | +# | | +# 'dev' < 'f' < 'post' -------------------/ | +# | +# 'dev' < 'f' ----------------------------------------------/ +# Other letters would do, but 'f' for 'final' is kind of nice. +FINAL_MARKER = ('f',) + +VERSION_RE = re.compile(r''' + ^ + (?P<version>\d+\.\d+) # minimum 'N.N' + (?P<extraversion>(?:\.\d+)*) # any number of extra '.N' segments + (?: + (?P<prerel>[abc]|rc) # 'a'=alpha, 'b'=beta, 'c'=release candidate + # 'rc'= alias for release candidate + (?P<prerelversion>\d+(?:\.\d+)*) + )? + (?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)? + $''', re.VERBOSE) + +class NormalizedVersion(object): + """A rational version. + + Good: + 1.2 # equivalent to "1.2.0" + 1.2.0 + 1.2a1 + 1.2.3a2 + 1.2.3b1 + 1.2.3c1 + 1.2.3.4 + TODO: fill this out + + Bad: + 1 # mininum two numbers + 1.2a # release level must have a release serial + 1.2.3b + """ + def __init__(self, s, error_on_huge_major_num=True): + """Create a NormalizedVersion instance from a version string. + + @param s {str} The version string. + @param error_on_huge_major_num {bool} Whether to consider an + apparent use of a year or full date as the major version number + an error. Default True. One of the observed patterns on PyPI before + the introduction of `NormalizedVersion` was version numbers like this: + 2009.01.03 + 20040603 + 2005.01 + This guard is here to strongly encourage the package author to + use an alternate version, because a release deployed into PyPI + and, e.g. downstream Linux package managers, will forever remove + the possibility of using a version number like "1.0" (i.e. + where the major number is less than that huge major number). + """ + self._parse(s, error_on_huge_major_num) + + @classmethod + def from_parts(cls, version, prerelease=FINAL_MARKER, + devpost=FINAL_MARKER): + return cls(cls.parts_to_str((version, prerelease, devpost))) + + def _parse(self, s, error_on_huge_major_num=True): + """Parses a string version into parts.""" + match = VERSION_RE.search(s) if not match: - raise ValueError, "invalid version number '%s'" % vstring - - (major, minor, patch, prerelease, prerelease_num) = \ - match.group(1, 2, 4, 5, 6) - - if patch: - self.version = tuple(map(string.atoi, [major, minor, patch])) + raise IrrationalVersionError(s) + + groups = match.groupdict() + parts = [] + + # main version + block = self._parse_numdots(groups['version'], s, False, 2) + extraversion = groups.get('extraversion') + if extraversion not in ('', None): + block += self._parse_numdots(extraversion[1:], s) + parts.append(tuple(block)) + + # prerelease + prerel = groups.get('prerel') + if prerel is not None: + block = [prerel] + block += self._parse_numdots(groups.get('prerelversion'), s, + pad_zeros_length=1) + parts.append(tuple(block)) else: - self.version = tuple(map(string.atoi, [major, minor]) + [0]) - - if prerelease: - self.prerelease = (prerelease[0], string.atoi(prerelease_num)) - else: - self.prerelease = None - - - def __str__ (self): - - if self.version[2] == 0: - vstring = string.join(map(str, self.version[0:2]), '.') + parts.append(FINAL_MARKER) + + # postdev + if groups.get('postdev'): + post = groups.get('post') + dev = groups.get('dev') + postdev = [] + if post is not None: + postdev.extend([FINAL_MARKER[0], 'post', int(post)]) + if dev is None: + postdev.append(FINAL_MARKER[0]) + if dev is not None: + postdev.extend(['dev', int(dev)]) + parts.append(tuple(postdev)) else: - vstring = string.join(map(str, self.version), '.') - - if self.prerelease: - vstring = vstring + self.prerelease[0] + str(self.prerelease[1]) - - return vstring - - - def __cmp__ (self, other): - if isinstance(other, StringType): - other = StrictVersion(other) - - compare = cmp(self.version, other.version) - if (compare == 0): # have to compare prerelease - - # case 1: neither has prerelease; they're equal - # case 2: self has prerelease, other doesn't; other is greater - # case 3: self doesn't have prerelease, other does: self is greater - # case 4: both have prerelease: must compare them! - - if (not self.prerelease and not other.prerelease): - return 0 - elif (self.prerelease and not other.prerelease): - return -1 - elif (not self.prerelease and other.prerelease): - return 1 - elif (self.prerelease and other.prerelease): - return cmp(self.prerelease, other.prerelease) - - else: # numeric versions don't match -- - return compare # prerelease stuff doesn't matter - - -# end class StrictVersion - - -# The rules according to Greg Stein: -# 1) a version number has 1 or more numbers separated by a period or by -# sequences of letters. If only periods, then these are compared -# left-to-right to determine an ordering. -# 2) sequences of letters are part of the tuple for comparison and are -# compared lexicographically -# 3) recognize the numeric components may have leading zeroes -# -# The LooseVersion class below implements these rules: a version number -# string is split up into a tuple of integer and string components, and -# comparison is a simple tuple comparison. This means that version -# numbers behave in a predictable and obvious way, but a way that might -# not necessarily be how people *want* version numbers to behave. There -# wouldn't be a problem if people could stick to purely numeric version -# numbers: just split on period and compare the numbers as tuples. -# However, people insist on putting letters into their version numbers; -# the most common purpose seems to be: -# - indicating a "pre-release" version -# ('alpha', 'beta', 'a', 'b', 'pre', 'p') -# - indicating a post-release patch ('p', 'pl', 'patch') -# but of course this can't cover all version number schemes, and there's -# no way to know what a programmer means without asking him. -# -# The problem is what to do with letters (and other non-numeric -# characters) in a version number. The current implementation does the -# obvious and predictable thing: keep them as strings and compare -# lexically within a tuple comparison. This has the desired effect if -# an appended letter sequence implies something "post-release": -# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002". -# -# However, if letters in a version number imply a pre-release version, -# the "obvious" thing isn't correct. Eg. you would expect that -# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison -# implemented here, this just isn't so. -# -# Two possible solutions come to mind. The first is to tie the -# comparison algorithm to a particular set of semantic rules, as has -# been done in the StrictVersion class above. This works great as long -# as everyone can go along with bondage and discipline. Hopefully a -# (large) subset of Python module programmers will agree that the -# particular flavour of bondage and discipline provided by StrictVersion -# provides enough benefit to be worth using, and will submit their -# version numbering scheme to its domination. The free-thinking -# anarchists in the lot will never give in, though, and something needs -# to be done to accommodate them. -# -# Perhaps a "moderately strict" version class could be implemented that -# lets almost anything slide (syntactically), and makes some heuristic -# assumptions about non-digits in version number strings. This could -# sink into special-case-hell, though; if I was as talented and -# idiosyncratic as Larry Wall, I'd go ahead and implement a class that -# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is -# just as happy dealing with things like "2g6" and "1.13++". I don't -# think I'm smart enough to do it right though. -# -# In any case, I've coded the test suite for this module (see -# ../test/test_version.py) specifically to fail on things like comparing -# "1.2a2" and "1.2". That's not because the *code* is doing anything -# wrong, it's because the simple, obvious design doesn't match my -# complicated, hairy expectations for real-world version numbers. It -# would be a snap to fix the test suite to say, "Yep, LooseVersion does -# the Right Thing" (ie. the code matches the conception). But I'd rather -# have a conception that matches common notions about version numbers. - -class LooseVersion (Version): - - """Version numbering for anarchists and software realists. - Implements the standard interface for version number classes as - described above. A version number consists of a series of numbers, - separated by either periods or strings of letters. When comparing - version numbers, the numeric components will be compared - numerically, and the alphabetic components lexically. The following - are all valid version numbers, in no particular order: - - 1.5.1 - 1.5.2b2 - 161 - 3.10a - 8.02 - 3.4j - 1996.07.12 - 3.2.pl0 - 3.1.1.6 - 2g6 - 11g - 0.960923 - 2.2beta29 - 1.13++ - 5.5.kw - 2.0b1pl0 - - In fact, there is no such thing as an invalid version number under - this scheme; the rules for comparison are simple and predictable, - but may not always give the results you want (for some definition - of "want"). + parts.append(FINAL_MARKER) + self.parts = tuple(parts) + if error_on_huge_major_num and self.parts[0][0] > 1980: + raise HugeMajorVersionNumError("huge major version number, %r, " + "which might cause future problems: %r" % (self.parts[0][0], s)) + + def _parse_numdots(self, s, full_ver_str, drop_trailing_zeros=True, + pad_zeros_length=0): + """Parse 'N.N.N' sequences, return a list of ints. + + @param s {str} 'N.N.N..." sequence to be parsed + @param full_ver_str {str} The full version string from which this + comes. Used for error strings. + @param drop_trailing_zeros {bool} Whether to drop trailing zeros + from the returned list. Default True. + @param pad_zeros_length {int} The length to which to pad the + returned list with zeros, if necessary. Default 0. + """ + nums = [] + for n in s.split("."): + if len(n) > 1 and n[0] == '0': + raise IrrationalVersionError("cannot have leading zero in " + "version number segment: '%s' in %r" % (n, full_ver_str)) + nums.append(int(n)) + if drop_trailing_zeros: + while nums and nums[-1] == 0: + nums.pop() + while len(nums) < pad_zeros_length: + nums.append(0) + return nums + + def __str__(self): + return self.parts_to_str(self.parts) + + @classmethod + def parts_to_str(cls, parts): + """Transforms a version expressed in tuple into its string + representation.""" + # XXX This doesn't check for invalid tuples + main, prerel, postdev = parts + s = '.'.join(str(v) for v in main) + if prerel is not FINAL_MARKER: + s += prerel[0] + s += '.'.join(str(v) for v in prerel[1:]) + if postdev and postdev is not FINAL_MARKER: + if postdev[0] == 'f': + postdev = postdev[1:] + i = 0 + while i < len(postdev): + if i % 2 == 0: + s += '.' + s += str(postdev[i]) + i += 1 + return s + + def __repr__(self): + return "%s('%s')" % (self.__class__.__name__, self) + + def _cannot_compare(self, other): + raise TypeError("cannot compare %s and %s" + % (type(self).__name__, type(other).__name__)) + + def __eq__(self, other): + if not isinstance(other, NormalizedVersion): + self._cannot_compare(other) + return self.parts == other.parts + + def __lt__(self, other): + if not isinstance(other, NormalizedVersion): + self._cannot_compare(other) + return self.parts < other.parts + + def __ne__(self, other): + return not self.__eq__(other) + + def __gt__(self, other): + return not (self.__lt__(other) or self.__eq__(other)) + + def __le__(self, other): + return self.__eq__(other) or self.__lt__(other) + + def __ge__(self, other): + return self.__eq__(other) or self.__gt__(other) + +def suggest_normalized_version(s): + """Suggest a normalized version close to the given version string. + + If you have a version string that isn't rational (i.e. NormalizedVersion + doesn't like it) then you might be able to get an equivalent (or close) + rational version from this function. + + This does a number of simple normalizations to the given string, based + on observation of versions currently in use on PyPI. Given a dump of + those version during PyCon 2009, 4287 of them: + - 2312 (53.93%) match NormalizedVersion without change + - with the automatic suggestion + - 3474 (81.04%) match when using this suggestion method + + @param s {str} An irrational version string. + @returns A rational version string, or None, if couldn't determine one. """ + try: + NormalizedVersion(s) + return s # already rational + except IrrationalVersionError: + pass + + rs = s.lower() + + # part of this could use maketrans + for orig, repl in (('-alpha', 'a'), ('-beta', 'b'), ('alpha', 'a'), + ('beta', 'b'), ('rc', 'c'), ('-final', ''), + ('-pre', 'c'), + ('-release', ''), ('.release', ''), ('-stable', ''), + ('+', '.'), ('_', '.'), (' ', ''), ('.final', ''), + ('final', '')): + rs = rs.replace(orig, repl) + + # if something ends with dev or pre, we add a 0 + rs = re.sub(r"pre$", r"pre0", rs) + rs = re.sub(r"dev$", r"dev0", rs) + + # if we have something like "b-2" or "a.2" at the end of the + # version, that is pobably beta, alpha, etc + # let's remove the dash or dot + rs = re.sub(r"([abc|rc])[\-\.](\d+)$", r"\1\2", rs) + + # 1.0-dev-r371 -> 1.0.dev371 + # 0.1-dev-r79 -> 0.1.dev79 + rs = re.sub(r"[\-\.](dev)[\-\.]?r?(\d+)$", r".\1\2", rs) + + # Clean: 2.0.a.3, 2.0.b1, 0.9.0~c1 + rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs) + + # Clean: v0.3, v1.0 + if rs.startswith('v'): + rs = rs[1:] + + # Clean leading '0's on numbers. + #TODO: unintended side-effect on, e.g., "2003.05.09" + # PyPI stats: 77 (~2%) better + rs = re.sub(r"\b0+(\d+)(?!\d)", r"\1", rs) + + # Clean a/b/c with no version. E.g. "1.0a" -> "1.0a0". Setuptools infers + # zero. + # PyPI stats: 245 (7.56%) better + rs = re.sub(r"(\d+[abc])$", r"\g<1>0", rs) + + # the 'dev-rNNN' tag is a dev tag + rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs) + + # clean the - when used as a pre delimiter + rs = re.sub(r"-(a|b|c)(\d+)$", r"\1\2", rs) + + # a terminal "dev" or "devel" can be changed into ".dev0" + rs = re.sub(r"[\.\-](dev|devel)$", r".dev0", rs) + + # a terminal "dev" can be changed into ".dev0" + rs = re.sub(r"(?![\.\-])dev$", r".dev0", rs) + + # a terminal "final" or "stable" can be removed + rs = re.sub(r"(final|stable)$", "", rs) + + # The 'r' and the '-' tags are post release tags + # 0.4a1.r10 -> 0.4a1.post10 + # 0.9.33-17222 -> 0.9.3.post17222 + # 0.9.33-r17222 -> 0.9.3.post17222 + rs = re.sub(r"\.?(r|-|-r)\.?(\d+)$", r".post\2", rs) + + # Clean 'r' instead of 'dev' usage: + # 0.9.33+r17222 -> 0.9.3.dev17222 + # 1.0dev123 -> 1.0.dev123 + # 1.0.git123 -> 1.0.dev123 + # 1.0.bzr123 -> 1.0.dev123 + # 0.1a0dev.123 -> 0.1a0.dev123 + # PyPI stats: ~150 (~4%) better + rs = re.sub(r"\.?(dev|git|bzr)\.?(\d+)$", r".dev\2", rs) + + # Clean '.pre' (normalized from '-pre' above) instead of 'c' usage: + # 0.2.pre1 -> 0.2c1 + # 0.2-c1 -> 0.2c1 + # 1.0preview123 -> 1.0c123 + # PyPI stats: ~21 (0.62%) better + rs = re.sub(r"\.?(pre|preview|-c)(\d+)$", r"c\g<2>", rs) + + + # Tcl/Tk uses "px" for their post release markers + rs = re.sub(r"p(\d+)$", r".post\1", rs) + + try: + NormalizedVersion(rs) + return rs # already rational + except IrrationalVersionError: + pass + return None + + +_PREDICATE = re.compile(r"(?i)^\s*([a-z_]\w*(?:\.[a-z_]\w*)*)(.*)") +_VERSIONS = re.compile(r"^\s*\((.*)\)\s*$") +_SPLIT_CMP = re.compile(r"^\s*(<=|>=|<|>|!=|==)\s*([^\s,]+)\s*$") + +def _split_predicate(predicate): + match = _SPLIT_CMP.match(predicate) + if match is None: + # probably no op, we'll use "==" + comp, version = '==', predicate + else: + comp, version = match.groups() + return comp, NormalizedVersion(version) + +class VersionPredicate(object): + """Defines a predicate: ProjectName (>ver1,ver2, ..)""" + + _operators = {"<": lambda x, y: x < y, + ">": lambda x, y: x > y, + "<=": lambda x, y: x <= y, + ">=": lambda x, y: x >= y, + "==": lambda x, y: x == y, + "!=": lambda x, y: x != y} + + def __init__(self, predicate): + predicate = predicate.strip() + match = _PREDICATE.match(predicate) + if match is None: + raise ValueError('Bad predicate "%s"' % predicate) + + self.name, predicates = match.groups() + predicates = predicates.strip() + + predicates = _VERSIONS.match(predicates) + if predicates is not None: + predicates = predicates.groups()[0] + self.predicates = [_split_predicate(pred.strip()) + for pred in predicates.split(',')] + else: + self.predicates = [] + + def match(self, version): + """Check if the provided version matches the predicates.""" + if isinstance(version, str): + version = NormalizedVersion(version) + for operator, predicate in self.predicates: + if not self._operators[operator](version, predicate): + return False + return True + +def is_valid_predicate(predicate): + try: + VersionPredicate(predicate) + except ValueError: + return False + else: + return True - component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) - - def __init__ (self, vstring=None): - if vstring: - self.parse(vstring) - - - def parse (self, vstring): - # I've given up on thinking I can reconstruct the version string - # from the parsed tuple -- so I just store the string here for - # use by __str__ - self.vstring = vstring - components = filter(lambda x: x and x != '.', - self.component_re.split(vstring)) - for i in range(len(components)): - try: - components[i] = int(components[i]) - except ValueError: - pass - - self.version = components - - - def __str__ (self): - return self.vstring - - - def __repr__ (self): - return "LooseVersion ('%s')" % str(self) - - - def __cmp__ (self, other): - if isinstance(other, StringType): - other = LooseVersion(other) - - return cmp(self.version, other.version) - - -# end class LooseVersion diff --git a/src/distutils2/versionpredicate.py b/src/distutils2/versionpredicate.py deleted file mode 100644 index 6c8b361..0000000 --- a/src/distutils2/versionpredicate.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Module for parsing and testing package version predicate strings. -""" -import re -import distutils2.version -import operator - - -re_validPackage = re.compile(r"(?i)^\s*([a-z_]\w*(?:\.[a-z_]\w*)*)(.*)") -# (package) (rest) - -re_paren = re.compile(r"^\s*\((.*)\)\s*$") # (list) inside of parentheses -re_splitComparison = re.compile(r"^\s*(<=|>=|<|>|!=|==)\s*([^\s,]+)\s*$") -# (comp) (version) - - -def splitUp(pred): - """Parse a single version comparison. - - Return (comparison string, StrictVersion) - """ - res = re_splitComparison.match(pred) - if not res: - raise ValueError("bad package restriction syntax: %r" % pred) - comp, verStr = res.groups() - return (comp, distutils2.version.StrictVersion(verStr)) - -compmap = {"<": operator.lt, "<=": operator.le, "==": operator.eq, - ">": operator.gt, ">=": operator.ge, "!=": operator.ne} - -class VersionPredicate: - """Parse and test package version predicates. - - >>> v = VersionPredicate('pyepat.abc (>1.0, <3333.3a1, !=1555.1b3)') - - The `name` attribute provides the full dotted name that is given:: - - >>> v.name - 'pyepat.abc' - - The str() of a `VersionPredicate` provides a normalized - human-readable version of the expression:: - - >>> print v - pyepat.abc (> 1.0, < 3333.3a1, != 1555.1b3) - - The `satisfied_by()` method can be used to determine with a given - version number is included in the set described by the version - restrictions:: - - >>> v.satisfied_by('1.1') - True - >>> v.satisfied_by('1.4') - True - >>> v.satisfied_by('1.0') - False - >>> v.satisfied_by('4444.4') - False - >>> v.satisfied_by('1555.1b3') - False - - `VersionPredicate` is flexible in accepting extra whitespace:: - - >>> v = VersionPredicate(' pat( == 0.1 ) ') - >>> v.name - 'pat' - >>> v.satisfied_by('0.1') - True - >>> v.satisfied_by('0.2') - False - - If any version numbers passed in do not conform to the - restrictions of `StrictVersion`, a `ValueError` is raised:: - - >>> v = VersionPredicate('p1.p2.p3.p4(>=1.0, <=1.3a1, !=1.2zb3)') - Traceback (most recent call last): - ... - ValueError: invalid version number '1.2zb3' - - It the module or package name given does not conform to what's - allowed as a legal module or package name, `ValueError` is - raised:: - - >>> v = VersionPredicate('foo-bar') - Traceback (most recent call last): - ... - ValueError: expected parenthesized list: '-bar' - - >>> v = VersionPredicate('foo bar (12.21)') - Traceback (most recent call last): - ... - ValueError: expected parenthesized list: 'bar (12.21)' - - """ - - def __init__(self, versionPredicateStr): - """Parse a version predicate string. - """ - # Fields: - # name: package name - # pred: list of (comparison string, StrictVersion) - - versionPredicateStr = versionPredicateStr.strip() - if not versionPredicateStr: - raise ValueError("empty package restriction") - match = re_validPackage.match(versionPredicateStr) - if not match: - raise ValueError("bad package name in %r" % versionPredicateStr) - self.name, paren = match.groups() - paren = paren.strip() - if paren: - match = re_paren.match(paren) - if not match: - raise ValueError("expected parenthesized list: %r" % paren) - str = match.groups()[0] - self.pred = [splitUp(aPred) for aPred in str.split(",")] - if not self.pred: - raise ValueError("empty parenthesized list in %r" - % versionPredicateStr) - else: - self.pred = [] - - def __str__(self): - if self.pred: - seq = [cond + " " + str(ver) for cond, ver in self.pred] - return self.name + " (" + ", ".join(seq) + ")" - else: - return self.name - - def satisfied_by(self, version): - """True if version is compatible with all the predicates in self. - The parameter version must be acceptable to the StrictVersion - constructor. It may be either a string or StrictVersion. - """ - for cond, ver in self.pred: - if not compmap[cond](version, ver): - return False - return True - - -_provision_rx = None - -def split_provision(value): - """Return the name and optional version number of a provision. - - The version number, if given, will be returned as a `StrictVersion` - instance, otherwise it will be `None`. - - >>> split_provision('mypkg') - ('mypkg', None) - >>> split_provision(' mypkg( 1.2 ) ') - ('mypkg', StrictVersion ('1.2')) - """ - global _provision_rx - if _provision_rx is None: - _provision_rx = re.compile( - "([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*)(?:\s*\(\s*([^)\s]+)\s*\))?$") - value = value.strip() - m = _provision_rx.match(value) - if not m: - raise ValueError("illegal provides specification: %r" % value) - ver = m.group(2) or None - if ver: - ver = distutils2.version.StrictVersion(ver) - return m.group(1), ver |
