Class: MyPrecious::PyPackageInfo
- Inherits:
-
Object
- Object
- MyPrecious::PyPackageInfo
- 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
-
#install ⇒ Object
(also: #install?)
Returns the value of attribute install.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#url ⇒ Object
readonly
Returns the value of attribute url.
-
#version_reqs ⇒ Object
readonly
Returns the value of attribute version_reqs.
Class Method Summary collapse
-
.col_title(attr) ⇒ Object
Get an appropriate, human friendly column title for an attribute.
-
.guess_req_file(fpath) ⇒ Object
Guess the name of the requirements file in the given directory.
Instance Method Summary collapse
-
#age ⇒ Object
Age in days of the current version.
- #changelog ⇒ Object
- #current_version ⇒ Object
- #current_version=(val) ⇒ Object
- #cves ⇒ Object
- #days_between_current_and_recommended ⇒ Object
-
#direct_reference? ⇒ Boolean
Was this requirement specified as a direct reference to a URL providing the package?.
- #homepage_uri ⇒ Object
-
#incorporate(other_req) ⇒ Object
Incorporate the requirements for this package specified in another object into this instance.
-
#initialize(name: nil, version_reqs: [], url: nil, install: false) ⇒ PyPackageInfo
constructor
Construct an instance.
- #latest_released ⇒ Object
- #latest_version ⇒ Object
- #latest_version_satisfying_reqs ⇒ Object
- #license ⇒ Object
- #obsolescence ⇒ Object
- #pypi_release_url(release) ⇒ Object
- #pypi_url ⇒ Object
-
#recommended_version ⇒ Object
Version number recommended based on stability criteria.
- #release_history_url ⇒ Object
-
#resolve_name! ⇒ Object
For packages specified without a name, do what is necessary to find the name.
-
#resolve_version! ⇒ Object
For requirements not deterministically specifying a version, determine which version would be installed.
-
#satisfied_by?(version) ⇒ Boolean
Test if the version constraints on this package are satisfied by the given version.
-
#versions_with_release ⇒ Object
An Array of Arrays containing version (MyPrecious::PyPackageInfo::Version or String) and release date (Time).
Methods included from VersionParsing
Methods included from DataCaching
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
#install ⇒ Object 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 |
#name ⇒ Object (readonly)
Returns the value of attribute name.
76 77 78 |
# File 'lib/myprecious/python_packages.rb', line 76 def name @name end |
#url ⇒ Object (readonly)
Returns the value of attribute url.
76 77 78 |
# File 'lib/myprecious/python_packages.rb', line 76 def url @url end |
#version_reqs ⇒ Object (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
#age ⇒ Object
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 |
#changelog ⇒ Object
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_version ⇒ Object
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 |
#cves ⇒ Object
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 |
#days_between_current_and_recommended ⇒ Object
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?
84 85 86 |
# File 'lib/myprecious/python_packages.rb', line 84 def direct_reference? !url.nil? end |
#homepage_uri ⇒ Object
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_released ⇒ Object
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_version ⇒ Object
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_reqs ⇒ Object
196 197 198 199 200 201 202 |
# File 'lib/myprecious/python_packages.rb', line 196 def 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 |
#license ⇒ Object
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 |
#obsolescence ⇒ Object
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_url ⇒ Object
921 922 923 |
# File 'lib/myprecious/python_packages.rb', line 921 def pypi_url "https://pypi.org/pypi/#{name}/json" end |
#recommended_version ⇒ Object
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_url ⇒ Object
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 = 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.
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_release ⇒ Object
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 |