diff --git a/nbgrader/apps/baseapp.py b/nbgrader/apps/baseapp.py index 48585fbf7..2ea9006e3 100644 --- a/nbgrader/apps/baseapp.py +++ b/nbgrader/apps/baseapp.py @@ -149,35 +149,25 @@ def all_configurable_classes(self) -> TypingList[MetaHasTraits]: if len(app.class_traits(config=True)) > 0: classes.append(app) + def _collect_configurables(module): + """Return a list of all configurable classes from a module""" + + for name in module.__all__: + cls = getattr(module, name) + if hasattr(cls, "class_traits") and cls.class_traits(config=True): + classes.append(cls) + # include plugins that have configurable options - for pg_name in plugins.__all__: - pg = getattr(plugins, pg_name) - if pg.class_traits(config=True): - classes.append(pg) + _collect_configurables(plugins) # include all preprocessors that have configurable options - for pp_name in preprocessors.__all__: - pp = getattr(preprocessors, pp_name) - if len(pp.class_traits(config=True)) > 0: - classes.append(pp) - - # include all the exchange actions - for ex_name in exchange.__all__: - ex = getattr(exchange, ex_name) - if hasattr(ex, "class_traits") and ex.class_traits(config=True): - classes.append(ex) + _collect_configurables(preprocessors) # include all the default exchange actions - for ex_name in exchange.default.__all__: - ex = getattr(exchange, ex_name) - if hasattr(ex, "class_traits") and ex.class_traits(config=True): - classes.append(ex) + _collect_configurables(exchange.default) # include all the converters - for ex_name in converters.__all__: - ex = getattr(converters, ex_name) - if hasattr(ex, "class_traits") and ex.class_traits(config=True): - classes.append(ex) + _collect_configurables(converters) return classes diff --git a/nbgrader/apps/collectapp.py b/nbgrader/apps/collectapp.py index d5ac4cf8b..701b8fa0e 100644 --- a/nbgrader/apps/collectapp.py +++ b/nbgrader/apps/collectapp.py @@ -3,7 +3,8 @@ from traitlets import default from .baseapp import NbGrader, nbgrader_aliases, nbgrader_flags -from ..exchange import Exchange, ExchangeCollect, ExchangeError +from ..exchange.default import Exchange, ExchangeCollect +from ..exchange import ExchangeError aliases = {} diff --git a/nbgrader/exchange/__init__.py b/nbgrader/exchange/__init__.py index 12badf841..7a7f95066 100644 --- a/nbgrader/exchange/__init__.py +++ b/nbgrader/exchange/__init__.py @@ -1,21 +1,28 @@ - -from nbgrader.exchange.abc import (Exchange, ExchangeError, ExchangeCollect, ExchangeFetch, ExchangeFetchAssignment, - ExchangeFetchFeedback, ExchangeList, ExchangeReleaseAssignment, ExchangeRelease, - ExchangeReleaseFeedback, ExchangeSubmit, ExchangeReleaseFeedback) +import warnings +from nbgrader.exchange.abc import ExchangeError +from nbgrader.exchange import abc, default from .exchange_factory import ExchangeFactory +def __getattr__(name): + if name in abc.__all__: + warnings.warn( + f"Importing {name} from nbgrader.exchange is deprecated." + " Import from nbgrader.exchange.abc or the specific " + " exchange implementation instead.".format(name), + DeprecationWarning, + stacklevel=2 + ) + + if hasattr(abc, name): + return getattr(abc, name) + elif hasattr(default, name): + return getattr(default, name) + + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + __all__ = [ - "Exchange", - "ExchangeError", - "ExchangeCollect", - "ExchangeFetch", - "ExchangeFetchAssignment", - "ExchangeFetchFeedback", - "ExchangeList", - "ExchangeRelease", - "ExchangeReleaseAssignment", - "ExchangeReleaseFeedback", - "ExchangeSubmit", + "abc", + "default", "ExchangeFactory", - "default" + "ExchangeError", ] diff --git a/nbgrader/exchange/abc/__init__.py b/nbgrader/exchange/abc/__init__.py index ec860ed6a..d377d3f34 100644 --- a/nbgrader/exchange/abc/__init__.py +++ b/nbgrader/exchange/abc/__init__.py @@ -1,13 +1,13 @@ -from .exchange import ExchangeError, Exchange -from .submit import ExchangeSubmit -from .release_feedback import ExchangeReleaseFeedback -from .release import ExchangeRelease -from .release_assignment import ExchangeReleaseAssignment -from .fetch_feedback import ExchangeFetchFeedback -from .fetch import ExchangeFetch -from .fetch_assignment import ExchangeFetchAssignment -from .collect import ExchangeCollect -from .list import ExchangeList +from .exchange import ExchangeError, ABCExchange as Exchange +from .submit import ABCExchangeSubmit as ExchangeSubmit +from .release_feedback import ABCExchangeReleaseFeedback as ExchangeReleaseFeedback +from .release import ABCExchangeRelease as ExchangeRelease +from .release_assignment import ABCExchangeReleaseAssignment as ExchangeReleaseAssignment +from .fetch_feedback import ABCExchangeFetchFeedback as ExchangeFetchFeedback +from .fetch import ABCExchangeFetch as ExchangeFetch +from .fetch_assignment import ABCExchangeFetchAssignment as ExchangeFetchAssignment +from .collect import ABCExchangeCollect as ExchangeCollect +from .list import ABCExchangeList as ExchangeList __all__ = [ "Exchange", diff --git a/nbgrader/exchange/abc/collect.py b/nbgrader/exchange/abc/collect.py index ed4ebba41..cfe2a4241 100644 --- a/nbgrader/exchange/abc/collect.py +++ b/nbgrader/exchange/abc/collect.py @@ -1,9 +1,9 @@ from traitlets import Bool -from .exchange import Exchange +from .exchange import ABCExchange -class ExchangeCollect(Exchange): +class ABCExchangeCollect(ABCExchange): update = Bool( False, diff --git a/nbgrader/exchange/abc/exchange.py b/nbgrader/exchange/abc/exchange.py index 35178059e..06e6829ad 100644 --- a/nbgrader/exchange/abc/exchange.py +++ b/nbgrader/exchange/abc/exchange.py @@ -1,4 +1,5 @@ import datetime +from abc import abstractmethod from textwrap import dedent @@ -15,7 +16,11 @@ class ExchangeError(Exception): pass -class Exchange(LoggingConfigurable): +class ABCExchange(LoggingConfigurable): + """ + The abstract Exchange, which underlies every step in the exchange process. + """ + assignment_dir = Unicode( ".", help=dedent( @@ -51,7 +56,7 @@ def _valid_timestamp_format(self, proposal): def __init__(self, coursedir=None, authenticator=None, **kwargs): self.coursedir = coursedir self.authenticator = authenticator - super(Exchange, self).__init__(**kwargs) + super(ABCExchange, self).__init__(**kwargs) def fail(self, msg): self.log.fatal(msg) @@ -64,17 +69,17 @@ def set_timestamp(self): self.fail("Invalid timezone: {}".format(self.timezone)) self.timestamp = datetime.datetime.now(tz).strftime(self.timestamp_format) + @abstractmethod def init_src(self): """Compute and check the source paths for the transfer.""" - raise NotImplementedError + @abstractmethod def init_dest(self): """Compute and check the destination paths for the transfer.""" - raise NotImplementedError + @abstractmethod def copy_files(self): """Actually do the file transfer.""" - raise NotImplementedError def start(self): self.set_timestamp() diff --git a/nbgrader/exchange/abc/fetch.py b/nbgrader/exchange/abc/fetch.py index c151d918c..cf445974b 100644 --- a/nbgrader/exchange/abc/fetch.py +++ b/nbgrader/exchange/abc/fetch.py @@ -1,15 +1,15 @@ import warnings -from .fetch_assignment import ExchangeFetchAssignment +from .fetch_assignment import ABCExchangeFetchAssignment -class ExchangeFetch(ExchangeFetchAssignment): +class ABCExchangeFetch(ABCExchangeFetchAssignment): def __init__(self, *args, **kwargs): - super(ExchangeFetch, self).__init__(*args, **kwargs) + super(ABCExchangeFetch, self).__init__(*args, **kwargs) msg = ( - "The ExchangeFetch class is now deprecated, please use " - "ExchangeFetchAssignment instead. This class will be removed in a " + "The ABCExchangeFetch class is now deprecated, please use " + "ABCExchangeFetchAssignment instead. This class will be removed in a " "future version of nbgrader.") warnings.warn(msg, DeprecationWarning) self.log.warning(msg) diff --git a/nbgrader/exchange/abc/fetch_assignment.py b/nbgrader/exchange/abc/fetch_assignment.py index c537855c3..337916afc 100644 --- a/nbgrader/exchange/abc/fetch_assignment.py +++ b/nbgrader/exchange/abc/fetch_assignment.py @@ -1,8 +1,8 @@ from traitlets import Bool -from .exchange import Exchange +from .exchange import ABCExchange -class ExchangeFetchAssignment(Exchange): +class ABCExchangeFetchAssignment(ABCExchange): replace_missing_files = Bool(False, help="Whether to replace missing files on fetch").tag(config=True) diff --git a/nbgrader/exchange/abc/fetch_feedback.py b/nbgrader/exchange/abc/fetch_feedback.py index 8570b07b4..e8cb5dad4 100644 --- a/nbgrader/exchange/abc/fetch_feedback.py +++ b/nbgrader/exchange/abc/fetch_feedback.py @@ -1,5 +1,5 @@ -from .exchange import Exchange +from .exchange import ABCExchange -class ExchangeFetchFeedback(Exchange): +class ABCExchangeFetchFeedback(ABCExchange): pass diff --git a/nbgrader/exchange/abc/list.py b/nbgrader/exchange/abc/list.py index 2d2289ccb..571de2655 100644 --- a/nbgrader/exchange/abc/list.py +++ b/nbgrader/exchange/abc/list.py @@ -1,8 +1,8 @@ from traitlets import Bool -from .exchange import Exchange +from .exchange import ABCExchange -class ExchangeList(Exchange): +class ABCExchangeList(ABCExchange): inbound = Bool(False, help="List inbound files rather than outbound.").tag(config=True) cached = Bool(False, help="List assignments in submission cache.").tag(config=True) @@ -20,7 +20,7 @@ def start(self): if self.inbound and self.cached: self.fail("Options --inbound and --cached are incompatible.") - super(ExchangeList, self).start() + super(ABCExchangeList, self).start() if self.remove: return self.remove_files() diff --git a/nbgrader/exchange/abc/release.py b/nbgrader/exchange/abc/release.py index a4396b2c8..ebb91fc72 100644 --- a/nbgrader/exchange/abc/release.py +++ b/nbgrader/exchange/abc/release.py @@ -1,15 +1,15 @@ import warnings -from .release_assignment import ExchangeReleaseAssignment +from .release_assignment import ABCExchangeReleaseAssignment -class ExchangeRelease(ExchangeReleaseAssignment): +class ABCExchangeRelease(ABCExchangeReleaseAssignment): def __init__(self, *args, **kwargs): - super(ExchangeRelease, self).__init__(*args, **kwargs) + super(ABCExchangeRelease, self).__init__(*args, **kwargs) msg = ( - "The ExchangeRelease class is now deprecated, please use " - "ExchangeReleaseAssignment instead. This class will be removed in " + "The ABCExchangeRelease class is now deprecated, please use " + "ABCExchangeReleaseAssignment instead. This class will be removed in " "a future version of nbgrader.") warnings.warn(msg, DeprecationWarning) self.log.warning(msg) diff --git a/nbgrader/exchange/abc/release_assignment.py b/nbgrader/exchange/abc/release_assignment.py index bf6382011..dd75f5565 100644 --- a/nbgrader/exchange/abc/release_assignment.py +++ b/nbgrader/exchange/abc/release_assignment.py @@ -1,8 +1,8 @@ from traitlets import Bool -from .exchange import Exchange +from .exchange import ABCExchange -class ExchangeReleaseAssignment(Exchange): +class ABCExchangeReleaseAssignment(ABCExchange): force = Bool(False, help="Force overwrite existing files in the exchange.").tag(config=True) diff --git a/nbgrader/exchange/abc/release_feedback.py b/nbgrader/exchange/abc/release_feedback.py index b8f7085e3..8c017a645 100644 --- a/nbgrader/exchange/abc/release_feedback.py +++ b/nbgrader/exchange/abc/release_feedback.py @@ -1,5 +1,5 @@ -from .exchange import Exchange +from .exchange import ABCExchange -class ExchangeReleaseFeedback(Exchange): +class ABCExchangeReleaseFeedback(ABCExchange): pass diff --git a/nbgrader/exchange/abc/submit.py b/nbgrader/exchange/abc/submit.py index 673e8e0ac..349437562 100644 --- a/nbgrader/exchange/abc/submit.py +++ b/nbgrader/exchange/abc/submit.py @@ -1,10 +1,10 @@ from textwrap import dedent from traitlets import Bool -from .exchange import Exchange +from .exchange import ABCExchange -class ExchangeSubmit(Exchange): +class ABCExchangeSubmit(ABCExchange): strict = Bool( False, diff --git a/nbgrader/exchange/default/exchange.py b/nbgrader/exchange/default/exchange.py index 16703ee78..5d59cec5c 100644 --- a/nbgrader/exchange/default/exchange.py +++ b/nbgrader/exchange/default/exchange.py @@ -17,6 +17,10 @@ class Exchange(ABCExchange): + """ + The default implementation of an Exchange based on the local file system. + """ + root = Unicode( "/usr/local/share/nbgrader/exchange", help="The nbgrader exchange directory writable to everyone. MUST be preexisting."