LSSTApplications  11.0-13-gbb96280,12.1.rc1,12.1.rc1+1,12.1.rc1+2,12.1.rc1+5,12.1.rc1+8,12.1.rc1-1-g06d7636+1,12.1.rc1-1-g253890b+5,12.1.rc1-1-g3d31b68+7,12.1.rc1-1-g3db6b75+1,12.1.rc1-1-g5c1385a+3,12.1.rc1-1-g83b2247,12.1.rc1-1-g90cb4cf+6,12.1.rc1-1-g91da24b+3,12.1.rc1-2-g3521f8a,12.1.rc1-2-g39433dd+4,12.1.rc1-2-g486411b+2,12.1.rc1-2-g4c2be76,12.1.rc1-2-gc9c0491,12.1.rc1-2-gda2cd4f+6,12.1.rc1-3-g3391c73+2,12.1.rc1-3-g8c1bd6c+1,12.1.rc1-3-gcf4b6cb+2,12.1.rc1-4-g057223e+1,12.1.rc1-4-g19ed13b+2,12.1.rc1-4-g30492a7
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]) for pkg in buildtime)
78  return version
79 
80 
82  """Return a dict of imported python packages and their versions
83 
84  We wade through sys.modules and attempt to determine the version for each
85  module. Note, therefore, that we can only report on modules that have
86  *already* been imported.
87 
88  We don't include any module for which we cannot determine a version.
89  """
90  # Attempt to import libraries that only report their version in python
91  for module in PYTHON:
92  try:
93  importlib.import_module(module)
94  except:
95  pass # It's not available, so don't care
96 
97  packages = {"python": sys.version}
98  # Not iterating with sys.modules.iteritems() because it's not atomic and subject to race conditions
99  moduleNames = list(sys.modules.keys())
100  for name in moduleNames:
101  module = sys.modules[name]
102  try:
103  ver = getVersionFromPythonModule(module)
104  except:
105  continue # Can't get a version from it, don't care
106 
107  # Remove "foo.bar.version" in favor of "foo.bar"
108  # This prevents duplication when the __init__.py includes "from .version import *"
109  found = False
110  for ending in (".version", "._version"):
111  if name.endswith(ending):
112  name = name[:-len(ending)]
113  if name in packages:
114  assert ver == packages[name]
115  found = True
116  elif name in packages:
117  assert ver == packages[name]
118 
119  # Use LSST package names instead of python module names
120  # This matches the names we get from the environment (i.e., EUPS) so we can clobber these build-time
121  # versions if the environment reveals that we're not using the packages as-built.
122  if "lsst" in name:
123  name = name.replace("lsst.", "").replace(".", "_")
124 
125  packages[name] = ver
126 
127  return packages
128 
129 
130 _eups = None # Singleton Eups object
132  """Provide a dict of products and their versions from the environment
133 
134  We use EUPS to determine the version of certain products (those that don't provide
135  a means to determine the version any other way) and to check if uninstalled packages
136  are being used. We only report the product/version for these packages.
137  """
138  try:
139  from eups import Eups
140  from eups.Product import Product
141  except:
142  from lsst.pex.logging import getDefaultLog
143  getDefaultLog().warn("Unable to import eups, so cannot determine package versions from environment")
144  return {}
145 
146  # Cache eups object since creating it can take a while
147  global _eups
148  if not _eups:
149  _eups = Eups()
150  products = _eups.findProducts(tags=["setup"])
151 
152  # Get versions for things we can't determine via runtime mechanisms
153  # XXX Should we just grab everything we can, rather than just a predetermined set?
154  packages = {prod.name: prod.version for prod in products if prod in ENVIRONMENT}
155 
156  # The string 'LOCAL:' (the value of Product.LocalVersionPrefix) in the version name indicates uninstalled
157  # code, so the version could be different than what's being reported by the runtime environment (because
158  # we don't tend to run "scons" every time we update some python file, and even if we did sconsUtils
159  # probably doesn't check to see if the repo is clean).
160  for prod in products:
161  if not prod.version.startswith(Product.LocalVersionPrefix):
162  continue
163  ver = prod.version
164 
165  gitDir = os.path.join(prod.dir, ".git")
166  if os.path.exists(gitDir):
167  # get the git revision and an indication if the working copy is clean
168  revCmd = ["git", "--git-dir=" + gitDir, "--work-tree=" + prod.dir, "rev-parse", "HEAD"]
169  diffCmd = ["git", "--no-pager", "--git-dir=" + gitDir, "--work-tree=" + prod.dir, "diff",
170  "--patch"]
171  try:
172  rev = subprocess.check_output(revCmd).decode().strip()
173  diff = subprocess.check_output(diffCmd)
174  except:
175  ver += "@GIT_ERROR"
176  else:
177  ver += "@" + rev
178  if diff:
179  ver += "+" + hashlib.md5(diff).hexdigest()
180  else:
181  ver += "@NO_GIT"
182 
183  packages[prod.name] = ver
184  return packages
185 
186 
187 class Packages(object):
188  """A table of packages and their versions
189 
190  Essentially a wrapper around a dict with some conveniences.
191  """
192 
193  def __init__(self, packages):
194  """Constructor
195 
196  'packages' should be a mapping {package: version}, such as a dict.
197  """
198  assert isinstance(packages, Mapping)
199  self._packages = packages
200  self._names = set(packages.keys())
201 
202  @classmethod
203  def fromSystem(cls):
204  """Construct from the system
205 
206  Attempts to determine packages by examining the system (python's sys.modules,
207  runtime libraries and EUPS).
208  """
209  packages = {}
210  packages.update(getPythonPackages())
211  packages.update(getRuntimeVersions())
212  packages.update(getEnvironmentPackages()) # Should be last, to override products with LOCAL versions
213  return cls(packages)
214 
215  @classmethod
216  def read(cls, filename):
217  """Read packages from filename"""
218  with open(filename, "rb") as ff:
219  return pickle.load(ff)
220 
221  def write(self, filename):
222  """Write packages to file"""
223  with open(filename, "wb") as ff:
224  pickle.dump(self, ff)
225 
226  def __len__(self):
227  return len(self._packages)
228 
229  def __str__(self):
230  ss = "%s({\n" % self.__class__.__name__
231  # Sort alphabetically by module name, for convenience in reading
232  ss += ",\n".join("%s: %s" % (repr(prod), repr(self._packages[prod])) for
233  prod in sorted(self._names))
234  ss += ",\n})"
235  return ss
236 
237  def __repr__(self):
238  return "%s(%s)" % (self.__class__.__name__, repr(self._packages))
239 
240  def update(self, other):
241  """Update packages using another set of packages
242 
243  No check is made to see if we're clobbering anything.
244  """
245  self._packages.update(other._packages)
246  self._names.update(other._names)
247 
248  def extra(self, other):
249  """Return packages in 'self' but not in 'other'
250 
251  These are extra packages in 'self' compared to 'other'.
252  """
253  return {pkg: self._packages[pkg] for pkg in self._names - other._names}
254 
255  def missing(self, other):
256  """Return packages in 'other' but not in 'self'
257 
258  These are missing packages in 'self' compared to 'other'.
259  """
260  return {pkg: other._packages[pkg] for pkg in other._names - self._names}
261 
262  def difference(self, other):
263  """Return packages different between 'self' and 'other'"""
264  return {pkg: (self._packages[pkg], other._packages[pkg]) for
265  pkg in self._names & other._names if self._packages[pkg] != other._packages[pkg]}
def getEnvironmentPackages
Definition: packages.py:131
std::map< std::string, std::string > getRuntimeVersions()
Definition: versions.cc:54
def getVersionFromPythonModule
Definition: packages.py:62
def warn
Definition: log.py:99