Commit 489117c3 authored by PJ Eby's avatar PJ Eby

Added support for specifying options on requirements, so that a package's

optional dependencies can be included when processing nested dependencies.
Next up: tests for the resolve() algorithm.

--HG--
branch : setuptools
extra : convert_revision : svn%3A6015fed2-1504-0410-9fe1-9d1591cc4771/sandbox/trunk/setuptools%4041008
parent 1fb2b027
......@@ -14,7 +14,7 @@ The package resource API is designed to work with normal filesystem packages,
method.
"""
__all__ = [
'register_loader_type', 'get_provider', 'IResourceProvider',
'register_loader_type', 'get_provider', 'IResourceProvider',
'ResourceManager', 'AvailableDistributions', 'require', 'resource_string',
'resource_stream', 'resource_filename', 'set_extraction_path',
'cleanup_resources', 'parse_requirements', 'parse_version',
......@@ -25,6 +25,7 @@ __all__ = [
]
import sys, os, zipimport, time, re
from sets import ImmutableSet
class ResolutionError(Exception):
"""Abstract base for dependency resolution errors"""
......@@ -38,7 +39,6 @@ class DistributionNotFound(ResolutionError):
class InvalidOption(ResolutionError):
"""Invalid or unrecognized option name for a distribution"""
_provider_factories = {}
def register_loader_type(loader_type, provider_factory):
......@@ -418,7 +418,6 @@ def require(*requirements):
* get_distro_source() isn't implemented
* Distribution.install_on() isn't implemented
* Requirement.options isn't implemented
* AvailableDistributions.resolve() is untested
* AvailableDistributions.scan() is untested
......@@ -449,6 +448,7 @@ def require(*requirements):
class DefaultProvider:
"""Provides access to package resources in the filesystem"""
......@@ -668,9 +668,11 @@ def yield_lines(strs):
LINE_END = re.compile(r"\s*(#.*)?$").match # whitespace and comment
CONTINUE = re.compile(r"\s*\\\s*(#.*)?$").match # line continuation
DISTRO = re.compile(r"\s*(\w+)").match # Distribution name
DISTRO = re.compile(r"\s*(\w+)").match # Distribution or option
VERSION = re.compile(r"\s*(<=?|>=?|==|!=)\s*((\w|\.)+)").match # version info
COMMA = re.compile(r"\s*,").match # comma between items
OBRACKET = re.compile(r"\s*\[").match
CBRACKET = re.compile(r"\s*\]").match
EGG_NAME = re.compile(
r"(?P<name>[^-]+)"
......@@ -693,8 +695,6 @@ def _parse_version_parts(s):
yield '*final' # ensure that alpha/beta/candidate are before final
def parse_version(s):
"""Convert a version string to a sortable key
......@@ -867,15 +867,12 @@ def parse_requirements(strs):
"""
# create a steppable iterator, so we can handle \-continuations
lines = iter(yield_lines(strs))
for line in lines:
line = line.replace('-','_')
match = DISTRO(line)
if not match:
raise ValueError("Missing distribution spec", line)
distname = match.group(1)
p = match.end()
specs = []
while not LINE_END(line,p):
def scan_list(ITEM,TERMINATOR,line,p,groups,item_name):
items = []
while not TERMINATOR(line,p):
if CONTINUE(line,p):
try:
line = lines.next().replace('-','_'); p = 0
......@@ -883,64 +880,111 @@ def parse_requirements(strs):
raise ValueError(
"\\ must not appear on the last nonblank line"
)
match = VERSION(line,p)
match = ITEM(line,p)
if not match:
raise ValueError("Expected version spec in",line,"at",line[p:])
op,val = match.group(1,2)
specs.append((op,val.replace('_','-')))
raise ValueError("Expected "+item_name+" in",line,"at",line[p:])
items.append(match.group(*groups))
p = match.end()
match = COMMA(line,p)
if match:
p = match.end() # skip the comma
elif not LINE_END(line,p):
raise ValueError("Expected ',' or EOL in",line,"at",line[p:])
elif not TERMINATOR(line,p):
raise ValueError(
"Expected ',' or end-of-list in",line,"at",line[p:]
)
match = TERMINATOR(line,p)
if match: p = match.end() # skip the terminator, if any
return line, p, items
for line in lines:
line = line.replace('-','_')
match = DISTRO(line)
if not match:
raise ValueError("Missing distribution spec", line)
distname = match.group(1)
p = match.end()
options = []
match = OBRACKET(line,p)
if match:
p = match.end()
line, p, options = scan_list(DISTRO,CBRACKET,line,p,(1,),"option")
line, p, specs = scan_list(VERSION,LINE_END,line,p,(1,2),"version spec")
specs = [(op,val.replace('_','-')) for op,val in specs]
yield Requirement(distname.replace('_','-'), specs, options)
yield Requirement(distname.replace('_','-'), specs)
class Requirement:
def __init__(self, distname, specs=()):
def __init__(self, distname, specs=(), options=()):
self.distname = distname
self.key = distname.lower()
index = [(parse_version(v),state_machine[op],op,v) for op,v in specs]
index.sort()
self.specs = [(op,ver) for parsed,trans,op,ver in index]
self.index = index
self.index, self.options = index, tuple(options)
self.hashCmp = (
self.key, tuple([(op,parsed) for parsed,trans,op,ver in index]),
ImmutableSet(map(str.lower,options))
)
self.__hash = hash(self.hashCmp)
def __str__(self):
return self.distname + ','.join([''.join(s) for s in self.specs])
def __repr__(self):
return "Requirement(%r, %r)" % (self.distname, self.specs)
return "Requirement(%r, %r, %r)" % \
(self.distname,self.specs,self.options)
def __eq__(self,other):
return isinstance(other,Requirement) \
and self.key==other.key and self.specs==other.specs
return isinstance(other,Requirement) and self.hashCmp==other.hashCmp
def __contains__(self,item):
if isinstance(item,Distribution):
if item.key <> self.key:
return False
if item.key <> self.key: return False
item = item.parsed_version
elif isinstance(item,basestring):
item = parse_version(item)
last = True
for parsed,trans,op,ver in self.index:
action = trans[cmp(item,parsed)]
if action=='F':
return False
elif action=='T':
return True
elif action=='+':
last = True
elif action=='-':
last = False
if action=='F': return False
elif action=='T': return True
elif action=='+': last = True
elif action=='-': last = False
return last
def __hash__(self):
return self.__hash
#@staticmethod
def parse(s):
reqs = list(parse_requirements(s))
......@@ -948,10 +992,11 @@ class Requirement:
if len(reqs)==1:
return reqs[0]
raise ValueError("Expected only one requirement", s)
raise ValueError("No requirements found", s)
raise ValueError("No requirements found", s)
parse = staticmethod(parse)
state_machine = {
# =><
'<' : '--T',
......@@ -962,6 +1007,7 @@ state_machine = {
'!=': 'F..',
}
def _get_mro(cls):
"""Get an mro for a type or classic class"""
if not isinstance(cls,type):
......@@ -969,6 +1015,7 @@ def _get_mro(cls):
return cls.__mro__[1:]
return cls.__mro__
def _find_adapter(registry, ob):
"""Return an adapter factory for `ob` from `registry`"""
for t in _get_mro(getattr(ob, '__class__', type(ob))):
......@@ -995,7 +1042,7 @@ def split_sections(s):
for line in yield_lines(s):
if line.startswith("["):
if line.endswith("]"):
if content:
if section or content:
yield section, content
section = line[1:-1].strip().lower()
content = []
......@@ -1005,8 +1052,16 @@ def split_sections(s):
content.append(line)
# wrap up last segment
if content:
yield section, content
yield section, content
# Set up global resource manager
......@@ -1023,3 +1078,30 @@ _initialize(globals())
from unittest import TestCase, makeSuite
from pkg_resources import *
import pkg_resources, sys
from sets import ImmutableSet
class Metadata:
"""Mock object to return metadata as if from an on-disk distribution"""
......@@ -18,7 +19,6 @@ class Metadata:
return yield_lines(self.get_metadata(name))
class DistroTests(TestCase):
def testCollection(self):
......@@ -167,12 +167,13 @@ class RequirementsTests(TestCase):
def testBasics(self):
r = Requirement.parse("Twisted>=1.2")
self.assertEqual(str(r),"Twisted>=1.2")
self.assertEqual(repr(r),"Requirement('Twisted', [('>=', '1.2')])")
self.assertEqual(repr(r),"Requirement('Twisted', [('>=', '1.2')], ())")
self.assertEqual(r, Requirement("Twisted", [('>=','1.2')]))
self.assertEqual(r, Requirement("twisTed", [('>=','1.2')]))
self.assertNotEqual(r, Requirement("Twisted", [('>=','2.0')]))
self.assertNotEqual(r, Requirement("Zope", [('>=','1.2')]))
self.assertNotEqual(r, Requirement("Zope", [('>=','3.0')]))
self.assertNotEqual(r, Requirement.parse("Twisted[extras]>=1.2"))
def testOrdering(self):
r1 = Requirement("Twisted", [('==','1.2c1'),('>=','1.2')])
......@@ -200,6 +201,46 @@ class RequirementsTests(TestCase):
self.failUnless(v in r, (v,r))
for v in ('1.2c1','1.3.1','1.5','1.9.1','2.0','2.5','3.0','4.0'):
self.failUnless(v not in r, (v,r))
def testOptionsAndHashing(self):
r1 = Requirement.parse("Twisted[foo,bar]>=1.2")
r2 = Requirement.parse("Twisted[bar,FOO]>=1.2")
r3 = Requirement.parse("Twisted[BAR,FOO]>=1.2.0")
self.assertEqual(r1,r2)
self.assertEqual(r1,r3)
self.assertEqual(r1.options, ("foo","bar"))
self.assertEqual(r2.options, ("bar","FOO"))
self.assertEqual(hash(r1), hash(r2))
self.assertEqual(
hash(r1), hash(("twisted", ((">=",parse_version("1.2")),),
ImmutableSet(["foo","bar"])))
)
......@@ -233,7 +274,7 @@ class ParseTests(TestCase):
"""
)
),
[(None,["x"]), ("y",["z","a"]), ("b",["c"]), ("q",["v"])]
[(None,["x"]), ("y",["z","a"]), ("b",["c"]), ("d",[]), ("q",["v"])]
)
self.assertRaises(ValueError,list,pkg_resources.split_sections("[foo"))
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment