1# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*-
2# ex: set sts=4 ts=4 sw=4 noet:
3# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
4#
5#   See COPYING file distributed along with the duecredit package for the
6#   copyright and license terms.
7#
8# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
9"""Provides an adapter to switch between two (active, inactive) collectors
10"""
11
12import os
13import atexit
14
15from .log import lgr
16from .utils import never_fail
17
18def _get_duecredit_enable():
19    env_enable = os.environ.get('DUECREDIT_ENABLE', 'no')
20    if not env_enable.lower() in ('0', '1', 'yes', 'no', 'true', 'false'):
21        lgr.warning("Misunderstood value %s for DUECREDIT_ENABLE. "
22                    "Use 'yes' or 'no', or '0' or '1'")
23    return env_enable.lower() in ('1', 'yes', 'true')
24
25
26@never_fail
27def _get_inactive_due():
28    from .stub import InactiveDueCreditCollector
29    return InactiveDueCreditCollector()
30
31
32@never_fail
33def _get_active_due():
34    from .config import CACHE_DIR, DUECREDIT_FILE
35    from duecredit.collector import CollectorSummary, DueCreditCollector
36    from .io import load_due
37
38    # TODO:  this needs to move to atexit handling, that we load previous
39    # one and them merge with new ones.  Informative bits could be -- how
40    # many new citations we got
41    if os.path.exists(DUECREDIT_FILE):
42        try:
43            due_ = load_due(DUECREDIT_FILE)
44        except Exception as e:
45            lgr.warning("Failed to load previously collected %s. "
46                        "DueCredit will not be active for this session."
47                        % DUECREDIT_FILE)
48            return _get_inactive_due()
49    else:
50        due_ = DueCreditCollector()
51
52    return due_
53
54
55class DueSwitch(object):
56    """Adapter between two types of collectors -- Inactive and Active
57
58    Once activated though, cannot be fully deactivated since it would inject
59    duecredit decorators and register an event atexit.
60    """
61    def __init__(self, inactive, active, activate=False):
62        self.__active = None
63        self.__collectors = {False: inactive, True: active}
64        self.__activations_done = False
65        if not (inactive and active):
66            raise ValueError(
67                "Both inactive and active collectors should be provided. "
68                "Got active=%r, inactive=%r" % (active, inactive)
69            )
70        self.activate(activate)
71
72    @property
73    def active(self):
74        return self.__active
75
76    @never_fail
77    def dump(self, **kwargs):
78        """Dumps summary of the citations
79
80        Parameters
81        ----------
82        **kwargs: dict
83           Passed to `CollectorSummary` constructor.
84        """
85        from duecredit.collector import CollectorSummary
86        due_summary = CollectorSummary(self.__collectors[True], **kwargs)
87        due_summary.dump()
88
89    def __prepare_exit_and_injections(self):
90        # Wrapper to create and dump summary... passing method doesn't work:
91        #  probably removes instance too early
92
93        atexit.register(self.dump)
94
95        # Deal with injector
96        from .injections import DueCreditInjector
97        injector = DueCreditInjector(collector=self.__collectors[True])
98        injector.activate()
99
100    @never_fail
101    def activate(self, activate=True):
102        # 1st step -- if activating/deactivating switch between the two collectors
103        if self.__active is not activate:
104            # we need to switch the state
105            # InactiveDueCollector also has those special methods defined
106            # in DueSwitch so that client code could query/call (for no effect).
107            # So we shouldn't delete or bind them either
108            is_public_or_special = \
109                lambda x: not (x.startswith('_')
110                               or x in ('activate', 'active', 'dump'))
111            # Clean up current bindings first
112            for k in filter(is_public_or_special, dir(self)):
113                delattr(self, k)
114
115            new_due = self.__collectors[activate]
116            for k in filter(is_public_or_special, dir(new_due)):
117                setattr(self, k, getattr(new_due, k))
118
119            self.__active = activate
120
121        # 2nd -- if activating, we might still need to have activations done
122        if activate and not self.__activations_done:
123            try:
124                self.__prepare_exit_and_injections()
125            except Exception as e:
126                lgr.error("Failed to prepare injections etc: %s" % str(e))
127            finally:
128                self.__activations_done = True
129
130
131due = DueSwitch(_get_inactive_due(), _get_active_due(), _get_duecredit_enable())