@@ -59,6 +59,220 @@ def run_tests(self):
5959__dir__ = os .path .dirname (__file__ )
6060
6161
62+ from distutils .version import LooseVersion # noqa (late-import)
63+
64+
65+ class PEP440Version (LooseVersion ):
66+ """
67+ Basic PEP440 version with a few features.
68+
69+ Uses the same version semantics as LooseVersion,
70+ with the addition that a ``v`` prefix is allowed
71+ in the version as required by PEP 440.
72+
73+ vstring may be a list, tuple or string.
74+
75+ v_prefix indicates whether output of the version
76+ should include a v prefix.
77+
78+ v_prefix is auto-detected by default.
79+ Set to False to remove if present, or True to add if missing.
80+ """
81+
82+ def __init__ (self , vstring = None , v_prefix = None ):
83+ self ._v_prefix = v_prefix
84+
85+ if isinstance (vstring , (list , tuple )):
86+ type_ = type (vstring )
87+ vstring = '.' .join (str (i ) for i in vstring )
88+ else :
89+ type_ = list
90+
91+ vstring = vstring .strip ()
92+
93+ if vstring .startswith ('v' ):
94+ vstring = vstring [1 :]
95+ if vstring .startswith ('!' ):
96+ raise ValueError ('Invalid use of epoch' )
97+ if v_prefix is not False :
98+ self ._v_prefix = True
99+
100+ # Can not use super(..) on Python 2.7
101+ LooseVersion .__init__ (self , vstring )
102+ if self ._v_prefix :
103+ self .vstring = 'v' + self .vstring
104+ if len (self .version ) > 1 and self .version [1 ] == '!' :
105+ self ._epoch = self .version [0 ]
106+ if not isinstance (self ._epoch , int ) or len (self .version ) < 3 :
107+ raise ValueError ('Invalid use of epoch' )
108+
109+ # Normalise to lower case
110+ self .version = [
111+ x if isinstance (x , int ) else x .lower () for x in self .version
112+ if x not in ('-' , '_' )]
113+
114+ if self .version [- 1 ] != '*' and not isinstance (self .version [- 1 ], int ):
115+ self .version += (0 , )
116+
117+ if type_ is tuple :
118+ self .version = tuple (self .version )
119+
120+ self ._final = None
121+ self ._previous = None
122+
123+ def __repr__ (self ):
124+ return "%s('%s')" % (self .__class__ .__name__ , str (self ))
125+
126+ @property
127+ def is_dev (self ):
128+ return any (part == 'dev' for part in self .version )
129+
130+ @property
131+ def has_epoch (self ):
132+ return any (part == '!' for part in self .version )
133+
134+ @property
135+ def final (self ):
136+ """
137+ Provide only the final component of the version.
138+
139+ A new instance is return if this instance is not final.
140+ """
141+ if self .has_epoch :
142+ raise NotImplementedError
143+
144+ if self ._final is not None :
145+ return self ._final
146+
147+ for i , part in enumerate (self .version ):
148+ if not isinstance (part , int ):
149+ final = self .version [:i ]
150+ break
151+ else :
152+ self ._final = self
153+ return self
154+
155+ self ._final = PEP440Version (final , self ._v_prefix )
156+
157+ return self ._final
158+
159+ @property
160+ def is_final (self ):
161+ return self .final == self
162+
163+ @property
164+ def is_zero (self ):
165+ return all (part == 0 for part in self .version )
166+
167+ _zero_message = 'version prior to 0.0 can not exist'
168+
169+ def _estimate_previous (self ):
170+ """
171+ Return a new version calculated to be the previous version.
172+
173+ Currently only handles when the current instance is a final version.
174+
175+ To really get the previous for 1.0.0, we need to consult PyPi,
176+ git tags, or some other source of all released versions,
177+ to find the highest patch release in the prior minor release, or
178+ highest minor releases if there were no patch releases in the
179+ last minor release, etc.
180+
181+ As a result, currently this assumes that release x.(x-1).0 exists
182+ in that instance.
183+ """
184+ if self ._previous :
185+ return self ._previous
186+
187+ assert self .is_final , '%r is not final' % self
188+
189+ if self .is_zero :
190+ raise ValueError (self ._zero_message )
191+
192+ previous = self ._decrement (self .version )
193+ self ._previous = PEP440Version (previous , self ._v_prefix )
194+ return self ._previous
195+
196+ @staticmethod
197+ def _decrement (version ):
198+ pos = len (version ) - 1
199+
200+ # Look for non-zero int part
201+ while pos != 0 and not (isinstance (version [pos ], int ) and version [pos ]):
202+ pos -= 1
203+
204+ previous = []
205+ if pos :
206+ previous = version [:pos ]
207+
208+ previous += (version [pos ] - 1 , )
209+
210+ if len (previous ) == len (version ):
211+ return previous
212+
213+ remaining = version [pos + 1 :- 1 ]
214+
215+ previous += tuple (
216+ 0 if isinstance (i , int ) else i for i in remaining )
217+
218+ previous += ('*' , )
219+
220+ return previous
221+
222+
223+ def egg_name_to_requirement (name ):
224+ name = name .strip ()
225+ parts = name .split ('-' )
226+
227+ # The first part may be v or v0, which would be considered a version
228+ # if processed in the following loop.
229+ name_parts = [parts [0 ]]
230+ # Pre-releases may contain a '-' and be alpha only, so we must
231+ # parse from the second part to find the first version-like part.
232+ for part in parts [1 :]:
233+ version = PEP440Version (part )
234+ if isinstance (version .version [0 ], int ):
235+ break
236+ name_parts .append (part )
237+
238+ version_parts = parts [len (name_parts ):]
239+
240+ if not version_parts :
241+ return name
242+
243+ name = '-' .join (name_parts )
244+
245+ version = PEP440Version ('-' .join (version_parts ))
246+
247+ # Assume that alpha, beta, pre, post & final releases
248+ # are in PyPi so setuptools can find it.
249+ if not version .is_dev :
250+ return name + '==' + str (version )
251+
252+ # setuptools fails if a version is given with any specifier such as
253+ # `==`, `=~`, `>`, if the version is not in PyPi.
254+
255+ # For development releases, which will not usually be PyPi,
256+ # setuptools will typically fail.
257+
258+ # So we estimate a previous release that should exist in PyPi,
259+ # by decrementing the lowest final version part, and use version
260+ # specifier `>` so that the installed package from VCS will have a
261+ # version acceptable to the requirement.
262+
263+ # With major and minor releases, the previous version must be guessed.
264+ # If the version was `2.1.0`, the previous_version will be literally
265+ # `2.0.*` as it assumes that a prior minor release occurred and used
266+ # the same versioning convention.
267+ previous_version = version .final ._estimate_previous ()
268+
269+ if previous_version .is_zero :
270+ raise ValueError (
271+ 'Version %s could not be decremented' % version )
272+
273+ return name + '>' + str (previous_version )
274+
275+
62276def read_requirements (filename ):
63277 """
64278 Parse a requirements file.
@@ -84,7 +298,7 @@ def read_requirements(filename):
84298
85299 DEPENDENCY_LINKS .append (line )
86300
87- line = egg_name . replace ( '-' , '==' )
301+ line = egg_name_to_requirement ( egg_name )
88302
89303 data .append (line )
90304
0 commit comments