Class: MyPrecious::PyPackageInfo

Inherits:
Object
  • Object
show all
Extended by:
VersionParsing
Includes:
DataCaching, VersionParsing
Defined in:
lib/myprecious/python_packages.rb

Defined Under Namespace

Modules: VersionParsing Classes: FinalVersion, Reader, ReqSpecParser, ReqSpecTransform, Requirement, Version

Constant Summary collapse

COMMON_REQ_FILE_NAMES =
%w[requirements.txt Packages]
MIN_RELEASED_DAYS =
90
MIN_STABLE_DAYS =
14
PACKAGE_CACHE_DIR =
MyPrecious.data_cache(DATA_DIR / "py-package-cache")
CODE_CACHE_DIR =
MyPrecious.data_cache(DATA_DIR / "py-code-cache")
ACCEPTED_URI_SCHEMES =
%w[
  http
  https
  git
  git+git
  git+http
  git+https
  git+ssh
]
VERSION_PATTERN =
/^
  ((?<epoch> \d+ ) ! )?
  (?<final> \d+ (\.\d+)* (\.\*)? )
  (           # Pre-release (a | b | rc) group
    [._-]? 
    (?<pre_group> a(lpha)? | b(eta)? | c | pre(view)? | rc )
    [._-]?
    (?<pre_n> \d* )
  )?
  (           # Post-release group
    (
      [._-]? (post|r(ev)?) [._-]?
      |
      - # Implicit post release
    )
    (?<post> ((?<![._-]) | \d) \d* )
  )?
  (           # Development release group
    [._-]?
    dev
    (?<dev> \d* )
  )?
  (           # Local version segment
    \+
    (?<local>.*)
  )?
$/x

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from VersionParsing

parse_version_str

Methods included from DataCaching

print_error_info

Constructor Details

#initialize(name: nil, version_reqs: [], url: nil, install: false) ⇒ PyPackageInfo

Construct an instance

At least one of the keywords name: or url: MUST be provided.



63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/myprecious/python_packages.rb', line 63

def initialize(name: nil, version_reqs: [], url: nil, install: false)
  super()
  if name.nil? and url.nil?
    raise ArgumentError, "At least one of name: or url: must be specified"
  end
  @name = name
  @version_reqs = version_reqs
  @url = url && URI(url)
  @install = install
  if pinning_req = self.version_reqs.find(&:determinative?)
    current_version = pinning_req.vernum
  end
end

Instance Attribute Details

#installObject Also known as: install?

Returns the value of attribute install.



77
78
79
# File 'lib/myprecious/python_packages.rb', line 77

def install
  @install
end

#nameObject (readonly)

Returns the value of attribute name.



76
77
78
# File 'lib/myprecious/python_packages.rb', line 76

def name
  @name
end

#urlObject (readonly)

Returns the value of attribute url.



76
77
78
# File 'lib/myprecious/python_packages.rb', line 76

def url
  @url
end

#version_reqsObject (readonly)

Returns the value of attribute version_reqs.



76
77
78
# File 'lib/myprecious/python_packages.rb', line 76

def version_reqs
  @version_reqs
end

Class Method Details

.col_title(attr) ⇒ Object

Get an appropriate, human friendly column title for an attribute



51
52
53
54
55
56
# File 'lib/myprecious/python_packages.rb', line 51

def self.col_title(attr)
  case attr
  when :name then 'Package'
  else Reporting.common_col_title(attr)
  end
end

.guess_req_file(fpath) ⇒ Object

Guess the name of the requirements file in the given directory

Best effort (currently, consulting a static list of likely file names for existence), and may return nil.



42
43
44
45
46
# File 'lib/myprecious/python_packages.rb', line 42

def self.guess_req_file(fpath)
  COMMON_REQ_FILE_NAMES.find do |fname|
    fpath.join(fname).exist?
  end
end

Instance Method Details

#ageObject

Age in days of the current version



207
208
209
210
# File 'lib/myprecious/python_packages.rb', line 207

def age
  return @age if defined? @age
  @age = get_age
end

#changelogObject



274
275
276
277
278
# File 'lib/myprecious/python_packages.rb', line 274

def changelog
  # This is wrong
  info = get_package_info['info']
  return info['project_url']
end

#current_versionObject



153
154
155
# File 'lib/myprecious/python_packages.rb', line 153

def current_version
  @current_version
end

#current_version=(val) ⇒ Object



157
158
159
# File 'lib/myprecious/python_packages.rb', line 157

def current_version=(val)
  @current_version = val.kind_of?(Version) ? val : parse_version_str(val)
end

#cvesObject



265
266
267
268
269
270
271
272
# File 'lib/myprecious/python_packages.rb', line 265

def cves
  resolve_name!
  resolve_version!
  
  CVEs.get_for(name, current_version.to_s).map do |cve, applicability|
    cve
  end
end


284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/myprecious/python_packages.rb', line 284

def days_between_current_and_recommended
  v, cv_rel = versions_with_release.find do |v, r|
    case 
    when current_version.prerelease?
      v < current_version
    else
      v == current_version
    end
  end || []
  v, rv_rel = versions_with_release.find {|v, r| v == recommended_version} || []
  return nil if cv_rel.nil? || rv_rel.nil?
  
  return ((rv_rel - cv_rel) / ONE_DAY).to_i
end

#direct_reference?Boolean

Was this requirement specified as a direct reference to a URL providing the package?

Returns:

  • (Boolean)


84
85
86
# File 'lib/myprecious/python_packages.rb', line 84

def direct_reference?
  !url.nil?
end

#homepage_uriObject



256
257
258
# File 'lib/myprecious/python_packages.rb', line 256

def homepage_uri
  get_package_info['info']['home_page']
end

#incorporate(other_req) ⇒ Object

Incorporate the requirements for this package specified in another object into this instance



141
142
143
144
145
146
147
148
149
150
151
# File 'lib/myprecious/python_packages.rb', line 141

def incorporate(other_req)
  if other_req.name != self.name
    raise ArgumentError, "Cannot incorporate requirements for #{other_req.name} into #{self.name}"
  end
  
  self.version_reqs.concat(other_req.version_reqs)
  self.install ||= other_req.install
  if current_version.nil? && (pinning_req = self.version_reqs.find(&:determinative?))
    current_version = pinning_req.vernum
  end
end

#latest_releasedObject



216
217
218
# File 'lib/myprecious/python_packages.rb', line 216

def latest_released
  Date.parse(versions_with_release[0][1].to_s).to_s      
end

#latest_versionObject



212
213
214
# File 'lib/myprecious/python_packages.rb', line 212

def latest_version
  versions_with_release[0][0].to_s
end

#latest_version_satisfying_reqsObject



196
197
198
199
200
201
202
# File 'lib/myprecious/python_packages.rb', line 196

def latest_version_satisfying_reqs
  versions_with_release.each do |ver, rel_date|
    return ver if self.satisfied_by?(ver.to_s)
    return ver if version_reqs.all? {|req| req.satisfied_by?(ver.to_s)}
  end
  return nil
end

#licenseObject



260
261
262
263
# File 'lib/myprecious/python_packages.rb', line 260

def license
  # TODO: Implement better, showing difference between current and recommended
  LicenseDescription.new(get_package_info['info']['license'])
end

#obsolescenceObject



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/myprecious/python_packages.rb', line 299

def obsolescence
  at_least_moderate = false
  if current_version.kind_of?(Version) && recommended_version
    cv_major = [current_version.epoch, current_version.final.first]
    rv_major = [recommended_version.epoch, recommended_version.final.first]
    
    case 
    when rv_major[0] < cv_major[0]
      return nil
    when cv_major[0] < rv_major[0]
      # Can't compare, rely on days_between_current_and_recommended
    when cv_major[1] + 1 < rv_major[1]
      return :severe
    when cv_major[1] < rv_major[1]
      at_least_moderate = true
    end
    
    days_between = days_between_current_and_recommended
    
    return Reporting.obsolescence_by_age(
      days_between,
      at_least_moderate: at_least_moderate,
    )
  end
end

#pypi_release_url(release) ⇒ Object



925
926
927
# File 'lib/myprecious/python_packages.rb', line 925

def pypi_release_url(release)
  "https://pypi.org/pypi/#{name}/#{release}/json"
end

#pypi_urlObject



921
922
923
# File 'lib/myprecious/python_packages.rb', line 921

def pypi_url
  "https://pypi.org/pypi/#{name}/json"
end

Version number recommended based on stability criteria

May return nil if no version meets the established criteria



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/myprecious/python_packages.rb', line 225

def recommended_version
  return nil if versions_with_release.empty?
  return @recommended_version if defined? @recommended_version
  
  orig_time_horizon = time_horizon = \
    Time.now - (MIN_RELEASED_DAYS * ONE_DAY)
  horizon_versegs = nil
  versions_with_release.each do |vn, rd|
    if vn.kind_of?(Version)
      horizon_versegs = nonpatch_versegs(vn)
      break
    end
  end
  
  versions_with_release.each do |ver, released|
    next if ver.kind_of?(String) || ver.prerelease?
    return (@recommended_version = current_version) if current_version && current_version >= ver
    
    # Reset the time-horizon clock if moving back into previous patch-series
    if (nonpatch_versegs(ver) <=> horizon_versegs) < 0
      time_horizon = orig_time_horizon
    end
    
    if released < time_horizon && version_reqs.all? {|r| r.satisfied_by?(ver, strict: false)}
      return (@recommended_version = ver)
    end
    time_horizon = [time_horizon, released - (MIN_STABLE_DAYS * ONE_DAY)].min
  end
  return (@recommended_version = nil)
end

#release_history_urlObject



280
281
282
# File 'lib/myprecious/python_packages.rb', line 280

def release_history_url
  "https://pypi.org/project/#{name}/#history"
end

#resolve_name!Object

For packages specified without a name, do what is necessary to find the name



92
93
94
95
96
97
98
99
100
101
# File 'lib/myprecious/python_packages.rb', line 92

def resolve_name!
  return unless direct_reference?
  
  name_from_setup = setup_data['name']
  if !@name.nil? && @name != name_from_setup
    warn("Requirement file entry for #{@name} points to archive for #{name_from_setup}")
  else
    @name = name_from_setup
  end
end

#resolve_version!Object

For requirements not deterministically specifying a version, determine which version would be installed



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/myprecious/python_packages.rb', line 107

def resolve_version!
  return @current_version if @current_version
  
  if direct_reference?
    # Use setup_data
    @current_version = parse_version_str(setup_data['version'] || '0a0.dev0')
  elsif pinning_req = self.version_reqs.find(&:determinative?)
    @current_version = parse_version_str(pinning_req.vernum)
  else
    # Use data from pypi
    puts "Resolving current version of #{name}..."
    if inferred_ver = latest_version_satisfying_reqs
      self.current_version = inferred_ver
      puts "    -> #{inferred_ver}"
    else
      puts "    (unknown)"
    end
  end
end

#satisfied_by?(version) ⇒ Boolean

Test if the version constraints on this package are satisfied by the given version

All current version requirements are in #version_reqs.

Returns:

  • (Boolean)


133
134
135
# File 'lib/myprecious/python_packages.rb', line 133

def satisfied_by?(version)
  version_reqs.all? {|r| r.satisfied_by?(version)}
end

#versions_with_releaseObject

An Array of Arrays containing version (MyPrecious::PyPackageInfo::Version or String) and release date (Time)

The returned Array is sorted in order of descending version number, with strings not conforming to PEP-440 sorted lexicographically following all PEP-440 conformant versions, the latter presented as MyPrecious::PyPackageInfo::Version objects.



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/myprecious/python_packages.rb', line 170

def versions_with_release
  @versions ||= begin
    all_releases = get_package_info.fetch('releases', {})
    ver_release_pairs = all_releases.each_pair.map do |ver, info|
      [
        parse_version_str(ver),
        info.select {|f| f['packagetype'] == 'sdist'}.map do |f|
          Time.parse(f['upload_time_iso_8601'])
        end.min
      ].freeze
    end
    ver_release_pairs.reject! do |vn, rd|
      (vn.kind_of?(Version) && vn.prerelease?) || rd.nil?
    end
    ver_release_pairs.sort! do |l, r|
      case 
      when l[0].kind_of?(String) && r[0].kind_of?(Version) then -1
      when l[0].kind_of?(Version) && r[0].kind_of?(String) then 1
      else l <=> r
      end
    end
    ver_release_pairs.reverse!
    ver_release_pairs.freeze
  end
end