26
26
import logging
27
27
import re
28
28
from collections import defaultdict
29
+ from functools import cached_property
29
30
from itertools import chain
30
- from typing import Any
31
31
32
32
from tabulate import tabulate
33
33
@@ -43,28 +43,21 @@ def __init__(self, container: TrackedContainer):
43
43
self .container = container
44
44
self .container .run_detached (command = ["sleep" , "infinity" ])
45
45
46
- self .requested : dict [str , set [str ]] | None = None
47
- self .installed : dict [str , set [str ]] | None = None
48
- self .available : dict [str , set [str ]] | None = None
49
- self .comparison : list [dict [str , str ]] = []
50
-
46
+ @cached_property
51
47
def installed_packages (self ) -> dict [str , set [str ]]:
52
48
"""Return the installed packages"""
53
- if self .installed is None :
54
- LOGGER .info ("Grabbing the list of installed packages ..." )
55
- env_export = self .container .exec_cmd ("mamba env export --no-build --json" )
56
- self .installed = CondaPackageHelper ._parse_package_versions (env_export )
57
- return self .installed
49
+ LOGGER .info ("Grabbing the list of installed packages ..." )
50
+ env_export = self .container .exec_cmd ("mamba env export --no-build --json" )
51
+ return self ._parse_package_versions (env_export )
58
52
53
+ @cached_property
59
54
def requested_packages (self ) -> dict [str , set [str ]]:
60
55
"""Return the requested package (i.e. `mamba install <package>`)"""
61
- if self .requested is None :
62
- LOGGER .info ("Grabbing the list of manually requested packages ..." )
63
- env_export = self .container .exec_cmd (
64
- "mamba env export --no-build --json --from-history"
65
- )
66
- self .requested = CondaPackageHelper ._parse_package_versions (env_export )
67
- return self .requested
56
+ LOGGER .info ("Grabbing the list of manually requested packages ..." )
57
+ env_export = self .container .exec_cmd (
58
+ "mamba env export --no-build --json --from-history"
59
+ )
60
+ return self ._parse_package_versions (env_export )
68
61
69
62
@staticmethod
70
63
def _parse_package_versions (env_export : str ) -> dict [str , set [str ]]:
@@ -91,20 +84,16 @@ def _parse_package_versions(env_export: str) -> dict[str, set[str]]:
91
84
packages_dict [package ] = version
92
85
return packages_dict
93
86
87
+ @cached_property
94
88
def available_packages (self ) -> dict [str , set [str ]]:
95
89
"""Return the available packages"""
96
- if self .available is None :
97
- LOGGER .info (
98
- "Grabbing the list of available packages (can take a while) ..."
99
- )
100
- # Keeping command line output since `mamba search --outdated --json` is way too long ...
101
- self .available = CondaPackageHelper ._extract_available (
102
- self .container .exec_cmd ("conda search --outdated --quiet" )
103
- )
104
- return self .available
90
+ LOGGER .info ("Grabbing the list of available packages (can take a while) ..." )
91
+ return self ._extract_available (
92
+ self .container .exec_cmd ("conda search --outdated --quiet" )
93
+ )
105
94
106
95
@staticmethod
107
- def _extract_available (lines : str ) -> dict [str , set [str ]]:
96
+ def _extract_available (lines : str ) -> defaultdict [str , set [str ]]:
108
97
"""Extract packages and versions from the lines returned by the list of packages"""
109
98
ddict = defaultdict (set )
110
99
for line in lines .splitlines ()[2 :]:
@@ -114,39 +103,28 @@ def _extract_available(lines: str) -> dict[str, set[str]]:
114
103
ddict [pkg ].add (version )
115
104
return ddict
116
105
117
- def check_updatable_packages (
118
- self , requested_only : bool = True
119
- ) -> list [dict [str , str ]]:
106
+ def find_updatable_packages (self , requested_only : bool ) -> list [dict [str , str ]]:
120
107
"""Check the updatable packages including or not dependencies"""
121
- requested = self .requested_packages ()
122
- installed = self .installed_packages ()
123
- available = self .available_packages ()
124
- self .comparison = []
125
- for pkg , inst_vs in installed .items ():
126
- if not requested_only or pkg in requested :
127
- avail_vs = sorted (
128
- list (available [pkg ]), key = CondaPackageHelper .semantic_cmp
129
- )
130
- if not avail_vs :
131
- continue
132
- current = min (inst_vs , key = CondaPackageHelper .semantic_cmp )
133
- newest = avail_vs [- 1 ]
134
- if (
135
- avail_vs
136
- and current != newest
137
- and CondaPackageHelper .semantic_cmp (current )
138
- < CondaPackageHelper .semantic_cmp (newest )
139
- ):
140
- self .comparison .append (
141
- {"Package" : pkg , "Current" : current , "Newest" : newest }
142
- )
143
- return self .comparison
108
+ updatable = []
109
+ for pkg , inst_vs in self .installed_packages .items ():
110
+ avail_vs = self .available_packages [pkg ]
111
+ if (requested_only and pkg not in self .requested_packages ) or (
112
+ not avail_vs
113
+ ):
114
+ continue
115
+ newest = sorted (avail_vs , key = CondaPackageHelper .semantic_cmp )[- 1 ]
116
+ current = min (inst_vs , key = CondaPackageHelper .semantic_cmp )
117
+ if CondaPackageHelper .semantic_cmp (
118
+ current
119
+ ) < CondaPackageHelper .semantic_cmp (newest ):
120
+ updatable .append ({"Package" : pkg , "Current" : current , "Newest" : newest })
121
+ return updatable
144
122
145
123
@staticmethod
146
- def semantic_cmp (version_string : str ) -> Any :
124
+ def semantic_cmp (version_string : str ) -> tuple [ int , ...] :
147
125
"""Manage semantic versioning for comparison"""
148
126
149
- def my_split (string : str ) -> list [Any ]:
127
+ def my_split (string : str ) -> list [list [ str ] ]:
150
128
def version_substrs (x : str ) -> list [str ]:
151
129
return re .findall (r"([A-z]+|\d+)" , x )
152
130
@@ -168,15 +146,18 @@ def try_int(version_str: str) -> int:
168
146
mss = list (chain (* my_split (version_string )))
169
147
return tuple (map (try_int , mss ))
170
148
171
- def get_outdated_summary (self , requested_only : bool = True ) -> str :
149
+ def get_outdated_summary (
150
+ self , updatable : list [dict [str , str ]], requested_only : bool
151
+ ) -> str :
172
152
"""Return a summary of outdated packages"""
173
- packages = self .requested if requested_only else self .installed
174
- assert packages is not None
153
+ packages = (
154
+ self .requested_packages if requested_only else self .installed_packages
155
+ )
175
156
nb_packages = len (packages )
176
- nb_updatable = len (self . comparison )
157
+ nb_updatable = len (updatable )
177
158
updatable_ratio = nb_updatable / nb_packages
178
159
return f"{ nb_updatable } /{ nb_packages } ({ updatable_ratio :.0%} ) packages could be updated"
179
160
180
- def get_outdated_table (self ) -> str :
161
+ def get_outdated_table (self , updatable : list [ dict [ str , str ]] ) -> str :
181
162
"""Return a table of outdated packages"""
182
- return tabulate (self . comparison , headers = "keys" )
163
+ return tabulate (updatable , headers = "keys" )
0 commit comments