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.



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

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.



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

def install
  @install
end

#nameObject (readonly)

Returns the value of attribute name.



74
75
76
# File 'lib/myprecious/python_packages.rb', line 74

def name
  @name
end

#urlObject (readonly)

Returns the value of attribute url.



74
75
76
# File 'lib/myprecious/python_packages.rb', line 74

def url
  @url
end

#version_reqsObject (readonly)

Returns the value of attribute version_reqs.



74
75
76
# File 'lib/myprecious/python_packages.rb', line 74

def version_reqs
  @version_reqs
end

Class Method Details

.col_title(attr) ⇒ Object

Get an appropriate, human friendly column title for an attribute



49
50
51
52
53
54
# File 'lib/myprecious/python_packages.rb', line 49

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.



40
41
42
43
44
# File 'lib/myprecious/python_packages.rb', line 40

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



205
206
207
208
# File 'lib/myprecious/python_packages.rb', line 205

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

#changelogObject



263
264
265
266
267
# File 'lib/myprecious/python_packages.rb', line 263

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

#current_versionObject



151
152
153
# File 'lib/myprecious/python_packages.rb', line 151

def current_version
  @current_version
end

#current_version=(val) ⇒ Object



155
156
157
# File 'lib/myprecious/python_packages.rb', line 155

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


269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/myprecious/python_packages.rb', line 269

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)


82
83
84
# File 'lib/myprecious/python_packages.rb', line 82

def direct_reference?
  !url.nil?
end

#homepage_uriObject



254
255
256
# File 'lib/myprecious/python_packages.rb', line 254

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



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

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



214
215
216
# File 'lib/myprecious/python_packages.rb', line 214

def latest_released
  versions_with_release[0][1]
end

#latest_versionObject



210
211
212
# File 'lib/myprecious/python_packages.rb', line 210

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

#latest_version_satisfying_reqsObject



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

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



258
259
260
261
# File 'lib/myprecious/python_packages.rb', line 258

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

#obsolescenceObject



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/myprecious/python_packages.rb', line 284

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



910
911
912
# File 'lib/myprecious/python_packages.rb', line 910

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

#pypi_urlObject



906
907
908
# File 'lib/myprecious/python_packages.rb', line 906

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



223
224
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
# File 'lib/myprecious/python_packages.rb', line 223

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

#resolve_name!Object

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



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

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



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

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)


131
132
133
# File 'lib/myprecious/python_packages.rb', line 131

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.



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

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