LSSTApplications  11.0-13-gbb96280,12.1+18,12.1+7,12.1-1-g14f38d3+72,12.1-1-g16c0db7+5,12.1-1-g5961e7a+84,12.1-1-ge22e12b+23,12.1-11-g06625e2+4,12.1-11-g0d7f63b+4,12.1-19-gd507bfc,12.1-2-g7dda0ab+38,12.1-2-gc0bc6ab+81,12.1-21-g6ffe579+2,12.1-21-gbdb6c2a+4,12.1-24-g941c398+5,12.1-3-g57f6835+7,12.1-3-gf0736f3,12.1-37-g3ddd237,12.1-4-gf46015e+5,12.1-5-g06c326c+20,12.1-5-g648ee80+3,12.1-5-gc2189d7+4,12.1-6-ga608fc0+1,12.1-7-g3349e2a+5,12.1-7-gfd75620+9,12.1-9-g577b946+5,12.1-9-gc4df26a+10
LSSTDataManagementBasePackage
packages.py
Go to the documentation of this file.
1 """
2 Determine which packages are being used in the system and their versions
3 
4 There are a few different types of packages, and their versions are collected in different ways:
5 1. Run-time libraries (e.g., cfitsio, fftw): we get their version from interrogating the dynamic library
6 2. Python modules (e.g., afw, numpy; galsim is also in this group even though we only use it through the
7  library, because no version information is currently provided through the library): we get their version
8  from the __version__ module variable. Note that this means that we're only aware of modules that have
9  already been imported.
10 3. Other packages provide no run-time accessible version information (e.g., astrometry_net): we get their
11  version from interrogating the environment. Currently, that means EUPS; if EUPS is replaced or dropped then
12  we'll need to consider an alternative means of getting this version information.
13 4. Local versions of packages (a non-installed EUPS package, selected with "setup -r /path/to/package"): we
14  identify these through the environment (EUPS again) and use as a version the path supplemented with the
15  git SHA and, if the git repo isn't clean, an MD5 of the diff.
16 
17 These package versions are collected and stored in a Packages object, which provides useful comparison and
18 persistence features.
19 
20 Example usage:
21 
22  from lsst.base import Packages
23  pkgs = Packages.fromSystem()
24  print "Current packages:", pkgs
25  old = Packages.read("/path/to/packages.pickle")
26  print "Old packages:", old
27  print "Missing packages compared to before:", pkgs.missing(old)
28  print "Extra packages compared to before:", pkgs.extra(old)
29  print "Different packages: ", pkgs.difference(old)
30  old.update(pkgs) # Include any new packages in the old
31  old.write("/path/to/packages.pickle")
32 """
33 from future import standard_library
34 standard_library.install_aliases()
35 from builtins import object
36 
37 import os
38 import sys
39 import hashlib
40 import importlib
41 import subprocess
42 import pickle as pickle
43 from collections import Mapping
44 
45 from .baseLib import getRuntimeVersions
46 
47 __all__ = ["getVersionFromPythonModule", "getPythonPackages", "getEnvironmentPackages", "Packages"]
48 
49 
50 # Packages used at build-time (e.g., header-only)
51 BUILDTIME = set(["boost", "eigen", "tmv"])
52 
53 # Python modules to attempt to load so we can try to get the version
54 # We do this because the version only appears to be available from python, but we use the library
55 PYTHON = set(["galsim"])
56 
57 # Packages that don't seem to have a mechanism for reporting the runtime version
58 # We need to guess the version from the environment
59 ENVIRONMENT = set(["astrometry_net", "astrometry_net_data", "minuit2", "xpa"])
60 
61 
63  """Determine the version of a python module
64 
65  Will raise AttributeError if the __version__ attribute is not set.
66 
67  We supplement the version with information from the __dependency_versions__
68  (a specific variable set by LSST's sconsUtils at build time) only for packages
69  that are typically used only at build-time.
70  """
71  version = module.__version__
72  if hasattr(module, "__dependency_versions__"):
73  # Add build-time dependencies
74  deps = module.__dependency_versions__
75  buildtime = BUILDTIME & set(deps.keys())
76  if buildtime:
77  version += " with " + " ".join("%s=%s" % (pkg, deps[pkg])
78  for pkg in sorted(buildtime))
79  return version
80 
81 
83  """Return a dict of imported python packages and their versions
84 
85  We wade through sys.modules and attempt to determine the version for each
86  module. Note, therefore, that we can only report on modules that have
87  *already* been imported.
88 
89  We don't include any module for which we cannot determine a version.
90  """
91  # Attempt to import libraries that only report their version in python
92  for module in PYTHON:
93  try:
94  importlib.import_module(module)
95  except:
96  pass # It's not available, so don't care
97 
98  packages = {"python": sys.version}
99  # Not iterating with sys.modules.iteritems() because it's not atomic and subject to race conditions
100  moduleNames = list(sys.modules.keys())
101  for name in moduleNames:
102  module = sys.modules[name]
103  try:
104  ver = getVersionFromPythonModule(module)
105  except:
106  continue # Can't get a version from it, don't care
107 
108  # Remove "foo.bar.version" in favor of "foo.bar"
109  # This prevents duplication when the __init__.py includes "from .version import *"
110  found = False
111  for ending in (".version", "._version"):
112  if name.endswith(ending):
113  name = name[:-len(ending)]
114  if name in packages:
115  assert ver == packages[name]
116  found = True
117  elif name in packages:
118  assert ver == packages[name]
119 
120  # Use LSST package names instead of python module names
121  # This matches the names we get from the environment (i.e., EUPS) so we can clobber these build-time
122  # versions if the environment reveals that we're not using the packages as-built.
123  if "lsst" in name:
124  name = name.replace("lsst.", "").replace(".", "_")
125 
126  packages[name] = ver
127 
128  return packages
129 
130 
131 _eups = None # Singleton Eups object
133  """Provide a dict of products and their versions from the environment
134 
135  We use EUPS to determine the version of certain products (those that don't provide
136  a means to determine the version any other way) and to check if uninstalled packages
137  are being used. We only report the product/version for these packages.
138  """
139  try:
140  from eups import Eups
141  from eups.Product import Product
142  except:
143  from lsst.pex.logging import getDefaultLog
144  getDefaultLog().warn("Unable to import eups, so cannot determine package versions from environment")
145  return {}
146 
147  # Cache eups object since creating it can take a while
148  global _eups
149  if not _eups:
150  _eups = Eups()
151  products = _eups.findProducts(tags=["setup"])
152 
153  # Get versions for things we can't determine via runtime mechanisms
154  # XXX Should we just grab everything we can, rather than just a predetermined set?
155  packages = {prod.name: prod.version for prod in products if prod in ENVIRONMENT}
156 
157  # The string 'LOCAL:' (the value of Product.LocalVersionPrefix) in the version name indicates uninstalled
158  # code, so the version could be different than what's being reported by the runtime environment (because
159  # we don't tend to run "scons" every time we update some python file, and even if we did sconsUtils
160  # probably doesn't check to see if the repo is clean).
161  for prod in products:
162  if not prod.version.startswith(Product.LocalVersionPrefix):
163  continue
164  ver = prod.version
165 
166  gitDir = os.path.join(prod.dir, ".git")
167  if os.path.exists(gitDir):
168  # get the git revision and an indication if the working copy is clean
169  revCmd = ["git", "--git-dir=" + gitDir, "--work-tree=" + prod.dir, "rev-parse", "HEAD"]
170  diffCmd = ["git", "--no-pager", "--git-dir=" + gitDir, "--work-tree=" + prod.dir, "diff",
171  "--patch"]
172  try:
173  rev = subprocess.check_output(revCmd).decode().strip()
174  diff = subprocess.check_output(diffCmd)
175  except:
176  ver += "@GIT_ERROR"
177  else:
178  ver += "@" + rev
179  if diff:
180  ver += "+" + hashlib.md5(diff).hexdigest()
181  else:
182  ver += "@NO_GIT"
183 
184  packages[prod.name] = ver
185  return packages
186 
187 
188 class Packages(object):
189  """A table of packages and their versions
190 
191  Essentially a wrapper around a dict with some conveniences.
192  """
193 
194  def __init__(self, packages):
195  """Constructor
196 
197  'packages' should be a mapping {package: version}, such as a dict.
198  """
199  assert isinstance(packages, Mapping)
200  self._packages = packages
201  self._names = set(packages.keys())
202 
203  @classmethod
204  def fromSystem(cls):
205  """Construct from the system
206 
207  Attempts to determine packages by examining the system (python's sys.modules,
208  runtime libraries and EUPS).
209  """
210  packages = {}
211  packages.update(getPythonPackages())
212  packages.update(getRuntimeVersions())
213  packages.update(getEnvironmentPackages()) # Should be last, to override products with LOCAL versions
214  return cls(packages)
215 
216  @classmethod
217  def read(cls, filename):
218  """Read packages from filename"""
219  with open(filename, "rb") as ff:
220  return pickle.load(ff)
221 
222  def write(self, filename):
223  """Write packages to file"""
224  with open(filename, "wb") as ff:
225  pickle.dump(self, ff)
226 
227  def __len__(self):
228  return len(self._packages)
229 
230  def __str__(self):
231  ss = "%s({\n" % self.__class__.__name__
232  # Sort alphabetically by module name, for convenience in reading
233  ss += ",\n".join("%s: %s" % (repr(prod), repr(self._packages[prod])) for
234  prod in sorted(self._names))
235  ss += ",\n})"
236  return ss
237 
238  def __repr__(self):
239  return "%s(%s)" % (self.__class__.__name__, repr(self._packages))
240 
241  def update(self, other):
242  """Update packages using another set of packages
243 
244  No check is made to see if we're clobbering anything.
245  """
246  self._packages.update(other._packages)
247  self._names.update(other._names)
248 
249  def extra(self, other):
250  """Return packages in 'self' but not in 'other'
251 
252  These are extra packages in 'self' compared to 'other'.
253  """
254  return {pkg: self._packages[pkg] for pkg in self._names - other._names}
255 
256  def missing(self, other):
257  """Return packages in 'other' but not in 'self'
258 
259  These are missing packages in 'self' compared to 'other'.
260  """
261  return {pkg: other._packages[pkg] for pkg in other._names - self._names}
262 
263  def difference(self, other):
264  """Return packages different between 'self' and 'other'"""
265  return {pkg: (self._packages[pkg], other._packages[pkg]) for
266  pkg in self._names & other._names if self._packages[pkg] != other._packages[pkg]}
def getEnvironmentPackages
Definition: packages.py:132
std::map< std::string, std::string > getRuntimeVersions()
Return version strings for dependencies.
Definition: versions.cc:54
def getVersionFromPythonModule
Definition: packages.py:62
def warn
Definition: log.py:99