From 13b5338ae40ed82860e457816300340edbabba00 Mon Sep 17 00:00:00 2001 From: Stephen Bailey Date: Fri, 12 Feb 2021 11:40:13 -0800 Subject: [PATCH] add desiutil.depend.mergedep + tests --- py/desiutil/depend.py | 45 +++++++++++++++++++++++++++++++++ py/desiutil/test/test_depend.py | 43 ++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/py/desiutil/depend.py b/py/desiutil/depend.py index 94f2a25e..4d91b867 100644 --- a/py/desiutil/depend.py +++ b/py/desiutil/depend.py @@ -174,6 +174,51 @@ def iterdep(header): return return +def mergedep(srchdr, dsthdr, conflict='src'): + '''Merge dependencies from srchdr into dsthdr + + Parameters + ---------- + srchdr : dict-like + source dict-like object, *e.g.* :class:`astropy.io.fits.Header`, + with dependency keywords DEPNAMnn, DEPVERnn + dsthdr : dict-like + destination dict-like object + conflict : str, optional + 'src' or 'dst' or 'exception'; see notes + + Notes + ----- + Dependencies in srchdr are added to dsthdr, modifying it in-place, + adjusting DEPNAMnn/DEPVERnn numbering as needed. If the same dependency + appears in both headers with different versions, ``conflict`` + controls the behavior: + + * if 'src', the srchdr value replaces the dsthdr value + * if 'dst', the dsthdr value is retained unchanged + * if 'exception', raise a ValueError exception + + Raises + ------ + ValueError + If `conflict == 'exception'` and the same dependency name appears + in both headers with different values; or if `conflict` isn't one + of 'src', 'dst', or 'exception'. + ''' + if not conflict in ('src', 'dst', 'exception'): + raise ValueError(f"conflict ({conflict}) should be 'src', 'dst', or 'exception'") + + for name, version in iterdep(srchdr): + if hasdep(dsthdr, name) and getdep(dsthdr, name) != version: + if conflict == 'src': + setdep(dsthdr, name, version) + elif conflict == 'dst': + pass + else: + v2 = getdep(dsthdr, name) + raise ValueError(f'Version conflict for {name}: {version} != {v2}') + else: + setdep(dsthdr, name, version) def add_dependencies(header, module_names=None, long_python=False): '''Adds ``DEPNAMnn``, ``DEPVERnn`` keywords to header for imported modules. diff --git a/py/desiutil/test/test_depend.py b/py/desiutil/test/test_depend.py index 86693ddb..362ba232 100644 --- a/py/desiutil/test/test_depend.py +++ b/py/desiutil/test/test_depend.py @@ -5,7 +5,7 @@ import unittest import sys from collections import OrderedDict -from ..depend import (setdep, getdep, hasdep, iterdep, Dependencies, +from ..depend import (setdep, getdep, hasdep, iterdep, mergedep, Dependencies, add_dependencies) from .. import __version__ as desiutil_version @@ -79,6 +79,47 @@ def test_hasdep(self): self.assertTrue(hasdep(hdr, 'test010')) self.assertFalse(hasdep(hdr, 'test020')) + def test_mergedep(self): + """Test merging dependencies from one header to another + """ + src = dict( + DEPNAM00='blat', DEPVER00='1.0', + DEPNAM01='foo', DEPVER01='2.0', + ) + dst = dict( + DEPNAM00='biz', DEPVER00='3.0', + DEPNAM01='bat', DEPVER01='4.0', + ) + #- dependencies from src should be added to dst + mergedep(src, dst) + self.assertEqual(getdep(src, 'blat'), getdep(dst, 'blat')) + self.assertEqual(getdep(src, 'foo'), getdep(dst, 'foo')) + + #- if conflict='src', a src dependency can replace a dst dependency + dst = dict( + DEPNAM00='biz', DEPVER00='3.0', + DEPNAM01='blat', DEPVER01='4.0', + ) + mergedep(src, dst, conflict='src') + self.assertEqual(getdep(src, 'blat'), getdep(dst, 'blat')) + self.assertEqual(getdep(src, 'foo'), getdep(dst, 'foo')) + + #- if conflict='dst', the dst dependency is kept even if in src + dst = dict( + DEPNAM00='biz', DEPVER00='3.0', + DEPNAM01='blat', DEPVER01='4.0', + ) + mergedep(src, dst, conflict='dst') + self.assertEqual(getdep(dst, 'blat'), '4.0') #- not '1.0' + + #- if conflict='exception', should raise a ValueError + dst = dict( + DEPNAM00='biz', DEPVER00='3.0', + DEPNAM01='blat', DEPVER01='4.0', + ) + with self.assertRaises(ValueError): + mergedep(src, dst, conflict='exception') + @unittest.skipUnless(test_fits_header, 'requires astropy.io.fits') def test_fits_header(self): """Test dependency functions with an actual FITS header.