Repository: duecredit/duecredit

Issue:
Outputting to bibtex doesn't filter by used citations
Probably linked to #19 

Example output text

```
$duecredit summary --format text

DueCredit Report:
- Scientific tools library / numpy (v 1.10.4) [1]
- Scientific tools library / scipy (v 0.17) [2]
  - Single linkage hierarchical clustering / scipy.cluster.hierarchy:linkage (v 0.17) [3]
- Machine Learning library / sklearn (v 0.17) [4]

3 packages cited
0 modules cited
1 functions cited

References
----------

[1] Van Der Walt, S., Colbert, S.C. & Varoquaux, G., 2011. The NumPy array: a structure for efficient numerical computation. Computing in Science & Engineering, 13(2), pp.22–30.
[2] Jones, E. et al., 2001. SciPy: Open source scientific tools for Python.
[3] Sibson, R., 1973. SLINK: an optimally efficient algorithm for the single-link cluster method. The Computer Journal, 16(1), pp.30–34.
[4] Pedregosa, F. et al., 2011. Scikit-learn: Machine learning in Python. The Journal of Machine Learning Research, 12, pp.2825–2830.
```

Example output BibTeX

```
$duecredit summary --format bibtex
@article{sneath1962numerical,
        title={Numerical taxonomy},
        author={Sneath, Peter HA and Sokal, Robert R},
        journal={Nature},
        volume={193},
        number={4818},
        pages={855--860},
        year={1962},
        publisher={Nature Publishing Group}
    }
@article{batagelj1995comparing,
        title={Comparing resemblance measures},
        author={Batagelj, Vladimir and Bren, Matevz},
        journal={Journal of classification},
        volume={12},
        number={1},
        pages={73--90},
        year={1995},
        publisher={Springer}
    }
@Misc{JOP+01,
      author =    {Eric Jones and Travis Oliphant and Pearu Peterson and others},
      title =     {{SciPy}: Open source scientific tools for {Python}},
      year =      {2001--},
      url = "http://www.scipy.org/",
      note = {[Online; accessed 2015-07-13]}
    }

...
```

Comments:

Patch:
diff --git a/duecredit/collector.py b/duecredit/collector.py
index b7ee6db..e81055a 100644
--- a/duecredit/collector.py
+++ b/duecredit/collector.py
@@ -160,9 +160,10 @@ class Citation(object):
         else:
             return entry.path.startswith(self.path + '.')
 
+
     @property
     def key(self):
-        return CitationKey(self.path, self.entry.get_key())
+        return CitationKey(self.path, self.entry.key)
 
     @staticmethod
     def get_key(path, entry_key):
diff --git a/duecredit/entries.py b/duecredit/entries.py
index eef2081..97a67bf 100644
--- a/duecredit/entries.py
+++ b/duecredit/entries.py
@@ -18,6 +18,10 @@ class DueCreditEntry(object):
     def get_key(self):
         return self._key
 
+    @property
+    def key(self):
+        return self.get_key()
+
     @property
     def rawentry(self):
         if PY2:
diff --git a/duecredit/io.py b/duecredit/io.py
index 9ed6740..5c09af8 100644
--- a/duecredit/io.py
+++ b/duecredit/io.py
@@ -75,151 +75,141 @@ def import_doi(doi, sleep=0.5, retries=10):
     return bibtex
 
 
-class EnumeratedEntries(Iterator):
-    """A container of entries enumerated referenced by their entry_key"""
-    def __init__(self):
-        self._keys2refnr = {}
-        self._refnr2keys = {}
-        self._refnr = 1
-
-    def add(self, entry_key):
-        """Add entry_key and update refnr"""
-        if entry_key not in self._keys2refnr:
-            self._keys2refnr[entry_key] = self._refnr
-            self._refnr2keys[self._refnr] = entry_key
-            self._refnr += 1
-
-    def __getitem__(self, item):
-        if item not in self._keys2refnr:
-            raise KeyError('{0} not present'.format(item))
-        return self._keys2refnr[item]
-
-    def fromrefnr(self, refnr):
-        if refnr not in self._refnr2keys:
-            raise KeyError('{0} not present'.format(refnr))
-        return self._refnr2keys[refnr]
-
-    def __iter__(self):
-        return iteritems(self._keys2refnr)
-
-    # Python 3
-    def __next__(self):
-        return self.next()
-
-    def next(self):
-        yield next(self.__iter__())
-
-    def __len__(self):
-        return len(self._keys2refnr)
+def _is_contained(toppath, subpath):
+    if ':' not in toppath:
+        return ((toppath == subpath) or
+                (subpath.startswith(toppath + '.')) or
+                (subpath.startswith(toppath + ':')))
+    else:
+        return subpath.startswith(toppath + '.')
 
 
-class TextOutput(object):  # TODO some parent class to do what...?
-    def __init__(self, fd, collector, style=None):
+class Output(object):
+    """A generic class for setting up citations that then will be outputted
+    differently (e.g., Bibtex, Text, etc.)"""
+    def __init__(self, fd, collector):
         self.fd = fd
         self.collector = collector
-        # TODO: check that CLS style actually exists
-        self.style = style
-        if 'DUECREDIT_STYLE' in os.environ.keys():
-            self.style = os.environ['DUECREDIT_STYLE']
-        else:
-            self.style = 'harvard1'
 
-    # TODO: refactor name to sth more intuitive
-    def _model_citations(self, tags=None):
+    def _filter_citations(self, tags=None):
+        """Given all the citations, filter only those that the user wants and
+        those that were actually used"""
         if not tags:
             tags = os.environ.get('DUECREDIT_REPORT_TAGS', 'reference-implementation,implementation').split(',')
         tags = set(tags)
 
         citations = self.collector.citations
         if tags != {'*'}:
-            # Filter out citations
+            # Filter out citations based on tags
             citations = dict((k, c)
                              for k, c in iteritems(citations)
                              if tags.intersection(c.tags))
 
-        packages = {}
-        modules = {}
-        objects = {}
-
-        for key in ('citations', 'entry_keys'):
-            packages[key] = defaultdict(list)
-            modules[key] = defaultdict(list)
-            objects[key] = defaultdict(list)
+        packages = defaultdict(list)
+        modules = defaultdict(list)
+        objects = defaultdict(list)
 
-        # for each path store both a list of entry keys and of citations
+        # store the citations according to their path and divide them into
+        # the right level
         for (path, entry_key), citation in iteritems(citations):
             if ':' in path:
-                target_dict = objects
+                objects[path].append(citation)
             elif '.' in path:
-                target_dict = modules
+                modules[path].append(citation)
             else:
-                target_dict = packages
-            target_dict['citations'][path].append(citation)
-            target_dict['entry_keys'][path].append(entry_key)
+                packages[path].append(citation)
+
+        # now we need to filter out the packages that don't have modules
+        # or objects cited, or are specifically requested
+        cited_packages = list(packages)
+        cited_modobj = list(modules) + list(objects)
+        for package in cited_packages:
+            package_citations = packages[package]
+            if list(filter(lambda x: x.cite_module, package_citations)) or \
+               list(filter(lambda x: _is_contained(package, x), cited_modobj)):
+                continue
+            else:
+                # we don't need it
+                del packages[package]
+
         return packages, modules, objects
 
+    def dump(self, tags=None):
+        raise NotImplementedError
+
+
+
+class TextOutput(Output):
+    def __init__(self, fd, collector, style=None):
+        super(TextOutput, self).__init__(fd, collector)
+        self.style = style
+        if 'DUECREDIT_STYLE' in os.environ.keys():
+            self.style = os.environ['DUECREDIT_STYLE']
+        else:
+            self.style = 'harvard1'
+
+
+    @staticmethod
+    def _format_citations(citations, citation_nr):
+        descriptions = map(str, set(str(r.description) for r in citations))
+        versions = map(str, set(str(r.version) for r in citations))
+        refnrs = map(str, [citation_nr[c.entry.key] for c in citations])
+        path = citations[0].path
+
+        return '- {0} / {1} (v {2}) [{3}]\n'.format(
+            ", ".join(descriptions), path, ', '.join(versions), ', '.join(refnrs))
+
     def dump(self, tags=None):
         # get 'model' of citations
-        packages, modules, objects = self._model_citations(tags)
-        # mapping key -> refnr
-        enum_entries = EnumeratedEntries()
-
-        citations_ordered = []
-        # set up view
-
-        # package level
-        sublevels = [modules, objects]
-        for package in sorted(packages['entry_keys']):
-            for entry_key in packages['entry_keys'][package]:
-                enum_entries.add(entry_key)
-            citations_ordered.append(package)
-            # sublevels
-            for sublevel in sublevels:
-                for obj in sorted(filter(lambda x: package in x, sublevel['entry_keys'])):
-                    for entry_key_obj in sublevel['entry_keys'][obj]:
-                        enum_entries.add(entry_key_obj)
-                    citations_ordered.append(obj)
-
-        # Now we can "render" different views of our "model"
-        # Here for now just text BUT that is where we can "split" the logic and provide
-        # different renderings given the model -- text, rest, md, tex+latex, whatever
-        self.fd.write('\nDueCredit Report:\n')
+        packages, modules, objects = self._filter_citations(tags)
+        # put everything into a single dict
+        pmo = {}
+        pmo.update(packages)
+        pmo.update(modules)
+        pmo.update(objects)
+
+        # get all the paths
+        paths = sorted(list(pmo))
+        # get all the entry_keys in order
+        entry_keys = [c.entry.key for p in paths for c in pmo[p]]
+        # make a dictionary entry_key -> citation_nr
+        citation_nr = defaultdict(int)
+        refnr = 1
+        for entry_key in entry_keys:
+            if entry_key not in citation_nr:
+                citation_nr[entry_key] = refnr
+                refnr += 1
 
-        for path in citations_ordered:
-            if ':' in path:
-                self.fd.write('  ')
-                target_dict = objects
-            elif '.' in path:
+        self.fd.write('\nDueCredit Report:\n')
+        start_refnr = 1
+        for path in paths:
+            # since they're lexicographically sorted by path, dependencies
+            # should be maintained
+            cit = pmo[path]
+            if ':' in path or '.' in path:
                 self.fd.write('  ')
-                target_dict = modules
-            else:
-                target_dict = packages
-            # TODO: absorb common logic into a common function
-            citations = target_dict['citations'][path]
-            entry_keys = target_dict['entry_keys'][path]
-            descriptions = sorted(map(str, set(str(r.description) for r in citations)))
-            versions = sorted(map(str, set(str(r.version) for r in citations)))
-            refnrs = sorted([str(enum_entries[entry_key]) for entry_key in entry_keys])
-            self.fd.write('- {0} / {1} (v {2}) [{3}]\n'.format(
-                ", ".join(descriptions), path, ', '.join(versions), ', '.join(refnrs)))
+            self.fd.write(self._format_citations(cit, citation_nr))
+            start_refnr += len(cit)
 
         # Print out some stats
-        obj_names = ('packages', 'modules', 'functions')
-        n_citations = map(len, (packages['citations'], modules['citations'], objects['citations']))
-        for citation_type, n in zip(obj_names, n_citations):
-            self.fd.write('\n{0} {1} cited'.format(n, citation_type))
-
-        if enum_entries:
-            citations_fromentrykey = self.collector._citations_fromentrykey()
+        stats = [(len(packages), 'package'),
+                 (len(modules), 'module'),
+                 (len(objects), 'function')]
+        for n, cit_type in stats:
+            self.fd.write('\n{0} {1} cited'.format(n, cit_type if n == 1
+                                                      else cit_type + 's'))
+        # now print out references
+        printed_keys = []
+        if len(pmo) > 0:
             self.fd.write('\n\nReferences\n' + '-' * 10 + '\n')
-            # collect all the entries used
-            refnr_key = [(nr, enum_entries.fromrefnr(nr)) for nr in range(1, len(enum_entries)+1)]
-            for nr, key in refnr_key:
-                self.fd.write('\n[{0}] '.format(nr))
-                citation_text = get_text_rendering(citations_fromentrykey[key], style=self.style)
-                if PY2:
-                    citation_text = citation_text.encode(_PREFERRED_ENCODING)
-                self.fd.write(citation_text)
+            for path in paths:
+                for cit in pmo[path]:
+                    ek = cit.entry.key
+                    if ek not in printed_keys:
+                        self.fd.write('\n[{0}] '.format(citation_nr[ek]))
+                        self.fd.write(get_text_rendering(cit,
+                                                        style=self.style))
+                        printed_keys.append(ek)
             self.fd.write('\n')
 
 
@@ -312,17 +302,32 @@ class PickleOutput(object):
         with open(filename, 'rb') as f:
             return pickle.load(f)
 
-class BibTeXOutput(object):  # TODO some parent class to do what...?
+class BibTeXOutput(Output):
     def __init__(self, fd, collector):
-        self.fd = fd
-        self.collector = collector
+        super(BibTeXOutput, self).__init__(fd, collector)
 
-    def dump(self):
-        for citation in self.collector.citations.values():
+    def dump(self, tags=None):
+        packages, modules, objects = self._filter_citations(tags)
+        # get all the citations in order
+        pmo = {}
+        pmo.update(packages)
+        pmo.update(modules)
+        pmo.update(objects)
+
+        # get all the paths
+        paths = sorted(list(pmo))
+
+        entries = []
+        for path in paths:
+            for c in pmo[path]:
+                if c.entry not in entries:
+                    entries.append(c.entry)
+
+        for entry in entries:
             try:
-                bibtex = get_bibtex_rendering(citation.entry)
+                bibtex = get_bibtex_rendering(entry)
             except:
-                lgr.warning("Failed to generate bibtex for %s" % citation.entry)
+                lgr.warning("Failed to generate bibtex for %s" % entry)
                 continue
             self.fd.write(bibtex.rawentry + "\n")
 


Test patch:
diff --git a/duecredit/tests/test_io.py b/duecredit/tests/test_io.py
index b895115..b46eb71 100644
--- a/duecredit/tests/test_io.py
+++ b/duecredit/tests/test_io.py
@@ -21,8 +21,8 @@ from mock import patch
 from ..collector import DueCreditCollector, Citation
 from .test_collector import _sample_bibtex, _sample_doi
 from ..entries import BibTeX, DueCreditEntry, Doi
-from ..io import TextOutput, PickleOutput, import_doi, EnumeratedEntries, \
-    get_text_rendering, format_bibtex
+from ..io import TextOutput, PickleOutput, import_doi, \
+    get_text_rendering, format_bibtex, _is_contained, Output, BibTeXOutput
 from ..utils import with_tempfile
 
 from nose.tools import assert_equal, assert_is_instance, assert_raises, \
@@ -85,18 +85,174 @@ def test_pickleoutput(fn):
                      collector_loaded._entries.keys())
         os.unlink(fn)
 
+
+def test_output():
+    entry = BibTeX(_sample_bibtex)
+    entry2 = BibTeX(_sample_bibtex2)
+
+    # normal use
+    collector = DueCreditCollector()
+    collector.cite(entry, path='package')
+    collector.cite(entry, path='package.module')
+
+    output = Output(None, collector)
+
+    packages, modules, objects = output._filter_citations(tags=['*'])
+
+    assert_equal(len(packages), 1)
+    assert_equal(len(modules), 1)
+    assert_equal(len(objects), 0)
+
+    assert_equal(packages['package'][0],
+                 collector.citations[('package', entry.get_key())])
+    assert_equal(modules['package.module'][0],
+                 collector.citations[('package.module', entry.get_key())])
+
+    # no toppackage
+    collector = DueCreditCollector()
+    collector.cite(entry, path='package')
+    collector.cite(entry, path='package2.module')
+
+    output = Output(None, collector)
+
+    packages, modules, objects = output._filter_citations(tags=['*'])
+
+    assert_equal(len(packages), 0)
+    assert_equal(len(modules), 1)
+    assert_equal(len(objects), 0)
+
+    assert_equal(modules['package2.module'][0],
+                 collector.citations[('package2.module', entry.get_key())])
+
+
+    # toppackage because required
+    collector = DueCreditCollector()
+    collector.cite(entry, path='package', cite_module=True)
+    collector.cite(entry, path='package2.module')
+
+    output = Output(None, collector)
+
+    packages, modules, objects = output._filter_citations(tags=['*'])
+
+    assert_equal(len(packages), 1)
+    assert_equal(len(modules), 1)
+    assert_equal(len(objects), 0)
+
+    assert_equal(packages['package'][0],
+                 collector.citations[('package', entry.get_key())])
+    assert_equal(modules['package2.module'][0],
+                 collector.citations[('package2.module', entry.get_key())])
+
+
+    # check it returns multiple entries
+    collector = DueCreditCollector()
+    collector.cite(entry, path='package')
+    collector.cite(entry2, path='package')
+    collector.cite(entry, path='package.module')
+
+    output = Output(None, collector)
+
+    packages, modules, objects = output._filter_citations(tags=['*'])
+
+    assert_equal(len(packages), 1)
+    assert_equal(len(packages['package']), 2)
+    assert_equal(len(modules), 1)
+    assert_equal(len(objects), 0)
+
+    # sort them in order so we know who is who
+    # entry2 key is Atk...
+    # entry key is XX..
+    packs = sorted(packages['package'], key=lambda x: x.entry.key)
+
+    assert_equal(packs[0],
+                 collector.citations[('package', entry2.get_key())])
+    assert_equal(packs[1],
+                 collector.citations[('package', entry.get_key())])
+    assert_equal(modules['package.module'][0],
+                 collector.citations[('package.module', entry.get_key())])
+
+
+    # check that filtering works
+    collector = DueCreditCollector()
+    collector.cite(entry, path='package', tags=['edu'])
+    collector.cite(entry2, path='package')
+    collector.cite(entry, path='package.module', tags=['edu'])
+
+    output = Output(None, collector)
+
+    packages, modules, objects = output._filter_citations(tags=['edu'])
+
+    assert_equal(len(packages), 1)
+    assert_equal(len(packages['package']), 1)
+    assert_equal(len(modules), 1)
+    assert_equal(len(objects), 0)
+
+    assert_equal(packages['package'][0],
+                 collector.citations[('package', entry.get_key())])
+    assert_equal(modules['package.module'][0],
+                 collector.citations[('package.module', entry.get_key())])
+
 def test_text_output():
     entry = BibTeX(_sample_bibtex)
+    entry2 = BibTeX(_sample_bibtex2)
+
+    # in this case, since we're not citing any module or method, we shouldn't
+    # output anything
     collector = DueCreditCollector()
-    collector.cite(entry, path='module')
+    collector.cite(entry, path='package')
 
     strio = StringIO()
     TextOutput(strio, collector).dump(tags=['*'])
     value = strio.getvalue()
+    assert_true("0 packages cited" in value, msg="value was %s" % value)
+    assert_true("0 modules cited" in value, msg="value was %s" % value)
+    assert_true("0 functions cited" in value, msg="value was %s" % value)
+
+    # but it should be cited if cite_module=True
+    collector = DueCreditCollector()
+    collector.cite(entry, path='package', cite_module=True)
+
+    strio = StringIO()
+    TextOutput(strio, collector).dump(tags=['*'])
+    value = strio.getvalue()
+    assert_true("1 package cited" in value, msg="value was %s" % value)
+    assert_true("0 modules cited" in value, msg="value was %s" % value)
+    assert_true("0 functions cited" in value, msg="value was %s" % value)
+
+    # in this case, we should be citing the package since we are also citing a
+    # submodule
+    collector = DueCreditCollector()
+    collector.cite(entry, path='package')
+    collector.cite(entry, path='package.module')
+
+    strio = StringIO()
+    TextOutput(strio, collector).dump(tags=['*'])
+    value = strio.getvalue()
+    assert_true("1 package cited" in value, msg="value was %s" % value)
+    assert_true("1 module cited" in value, msg="value was %s" % value)
+    assert_true("0 functions cited" in value, msg="value was %s" % value)
     assert_true("Halchenko, Y.O." in value, msg="value was %s" % value)
     assert_true(value.strip().endswith("Frontiers in Neuroinformatics, 6(22)."))
 
 
+    # in this case, we should be citing the package since we are also citing a
+    # submodule
+    collector = DueCreditCollector()
+    collector.cite(entry, path='package')
+    collector.cite(entry2, path='package')
+    collector.cite(entry, path='package.module')
+
+    strio = StringIO()
+    TextOutput(strio, collector).dump(tags=['*'])
+    value = strio.getvalue()
+    assert_true("1 package cited" in value, msg="value was %s" % value)
+    assert_true("1 module cited" in value, msg="value was %s" % value)
+    assert_true("0 functions cited" in value, msg="value was %s" % value)
+    assert_true("Halchenko, Y.O." in value, msg="value was %s" % value)
+    assert_true('[1, 2]' in value, msg="value was %s" %value)
+    assert_false('[3]' in value, msg="value was %s" %value)
+
+
 def test_text_output_dump_formatting():
     due = DueCreditCollector()
 
@@ -125,17 +281,18 @@ def test_text_output_dump_formatting():
                 msg='value was {0}'.format(value))
 
     # now we call it -- check it prints stuff
+    strio = StringIO()
     mymodule('magical', kwarg2=1)
     TextOutput(strio, due).dump(tags=['*'])
     value = strio.getvalue()
-    assert_true('1 packages cited' in value, msg='value was {0}'.format(value))
-    assert_true('1 functions cited' in value, msg='value was {0}'.format(value))
+    assert_true('1 package cited' in value, msg='value was {0}'.format(value))
+    assert_true('1 function cited' in value, msg='value was {0}'.format(value))
     assert_true('(v 0.0.16)' in value,
                 msg='value was {0}'.format(value))
-    assert_equal(len(value.split('\n')), 21, msg='value was {0}'.format(value))
+    assert_equal(len(value.split('\n')), 16, msg='value was {0}'.format(len(value.split('\n'))))
 
     # test we get the reference numbering right
-    samples_bibtex = [_generate_sample_bibtex() for x in range(5)]
+    samples_bibtex = [_generate_sample_bibtex() for x in range(6)]
     # this sucks but at the moment it's the only way to have multiple
     # references for a function
 
@@ -154,7 +311,7 @@ def test_text_output_dump_formatting():
                    path='myothermodule:myotherfunction')
         @due.dcite(BibTeX(samples_bibtex[4]), description='solution to life',
                    path='myothermodule:myotherfunction')
-        @due.dcite(BibTeX(_sample_bibtex2), description='solution to life',
+        @due.dcite(BibTeX(samples_bibtex[5]), description='solution to life',
                    path='myothermodule:myotherfunction')
         def myotherfunction(arg42):
             pass
@@ -187,6 +344,59 @@ def test_text_output_dump_formatting():
     import warnings
     assert_true(('ignore', None, UserWarning, None, 0) not in warnings.filters)
 
+
+def test_bibtex_output():
+    entry = BibTeX(_sample_bibtex)
+    entry2 = BibTeX(_sample_bibtex2)
+
+    # in this case, since we're not citing any module or method, we shouldn't
+    # output anything
+    collector = DueCreditCollector()
+    collector.cite(entry, path='package')
+
+    strio = StringIO()
+    BibTeXOutput(strio, collector).dump(tags=['*'])
+    value = strio.getvalue()
+    assert_equal(value, '', msg='Value was {0}'.format(value))
+
+    # impose citing
+    collector = DueCreditCollector()
+    collector.cite(entry, path='package', cite_module=True)
+
+    strio = StringIO()
+    BibTeXOutput(strio, collector).dump(tags=['*'])
+    value = strio.getvalue()
+    assert_equal(value.strip(), _sample_bibtex.strip(), msg='Value was {0}'.format(value))
+
+    # impose filtering
+    collector = DueCreditCollector()
+    collector.cite(entry, path='package', cite_module=True, tags=['edu'])
+    collector.cite(entry2, path='package.module')
+
+    strio = StringIO()
+    BibTeXOutput(strio, collector).dump(tags=['edu'])
+    value = strio.getvalue()
+    assert_equal(value.strip(), _sample_bibtex.strip(), msg='Value was {0}'.format(value))
+
+    # no filtering
+    strio = StringIO()
+    BibTeXOutput(strio, collector).dump(tags=['*'])
+    value = strio.getvalue()
+    assert_equal(value.strip(),
+                 _sample_bibtex.strip() + _sample_bibtex2.rstrip(),
+                 msg='Value was {0}'.format(value))
+
+    # check the we output only unique bibtex entries
+    collector.cite(entry2, path='package')
+    strio = StringIO()
+    BibTeXOutput(strio, collector).dump(tags=['*'])
+    value = strio.getvalue()
+    value_ = sorted(value.strip().split('\n'))
+    bibtex = sorted((_sample_bibtex.strip() + _sample_bibtex2.rstrip()).split('\n'))
+    assert_equal(value_, bibtex,
+                 msg='Value was {0}'.format(value_, bibtex))
+
+
 def _generate_sample_bibtex():
     """
     Generate a random sample bibtex to test multiple references
@@ -214,25 +424,6 @@ def _generate_sample_bibtex():
     sample_bibtex += "}"
     return sample_bibtex
 
-def test_enumeratedentries():
-    enumentries = EnumeratedEntries()
-    assert_false(enumentries)
-
-    # add some entries
-    entries = [('ciao', 1), ('miao', 2), ('bau', 3)]
-    for entry, _ in entries:
-        enumentries.add(entry)
-
-    assert_equal(len(enumentries), 3)
-
-    for entry, nr in entries:
-        assert_equal(nr, enumentries[entry])
-        assert_equal(entry, enumentries.fromrefnr(nr))
-
-    assert_raises(KeyError, enumentries.__getitem__, 'boh')
-    assert_raises(KeyError, enumentries.fromrefnr, 666)
-
-    assert_equal(entries, sorted(enumentries, key=lambda x: x[1]))
 
 @patch('duecredit.io.get_bibtex_rendering')
 @patch('duecredit.io.format_bibtex')
@@ -278,3 +469,14 @@ def test_format_bibtex_zenodo_doi():
     """
     assert_equal(format_bibtex(BibTeX(bibtex_zenodo)),
                  """Ghosh, S. et al., 2016. nipype: Release candidate 1 for version 0.12.0.""")
+
+def test_is_contained():
+    toppath = 'package'
+    assert_true(_is_contained(toppath, 'package.module'))
+    assert_true(_is_contained(toppath, 'package.module.submodule'))
+    assert_true(_is_contained(toppath, 'package.module.submodule:object'))
+    assert_true(_is_contained(toppath, 'package:object'))
+    assert_true(_is_contained(toppath, toppath))
+    assert_false(_is_contained(toppath, 'package2'))
+    assert_false(_is_contained(toppath, 'package2:anotherobject'))
+    assert_false(_is_contained(toppath, 'package2.module:anotherobject'))