diff --git a/etiquette/constants.py b/etiquette/constants.py index f57e412..05bf0b5 100644 --- a/etiquette/constants.py +++ b/etiquette/constants.py @@ -252,7 +252,7 @@ ADDITIONAL_MIMETYPES = { # Photodb ########################################################################################## -DEFAULT_DATADIR = '.\\_etiquette' +DEFAULT_DATADIR = '_etiquette' DEFAULT_DBNAME = 'phototagger.db' DEFAULT_CONFIGNAME = 'config.json' DEFAULT_THUMBDIR = 'site_thumbnails' diff --git a/etiquette/exceptions.py b/etiquette/exceptions.py index 85d2c43..9fd00c7 100644 --- a/etiquette/exceptions.py +++ b/etiquette/exceptions.py @@ -192,6 +192,13 @@ class FeatureDisabled(EtiquetteException): ''' error_message = 'This feature has been disabled. Requires {}.' +class NoClosestPhotoDB(EtiquetteException): + ''' + For calls to PhotoDB.closest_photodb where none exists between cwd and + drive root. + ''' + error_message = 'There is no PhotoDB in {} or its parents.' + class NoYields(EtiquetteException): ''' For when all of the yield_* arguments have been provided as False, and thus diff --git a/etiquette/photodb.py b/etiquette/photodb.py index a6527a6..738d6b4 100644 --- a/etiquette/photodb.py +++ b/etiquette/photodb.py @@ -1805,7 +1805,7 @@ class PhotoDB( self.ephemeral_directory = tempfile.TemporaryDirectory(prefix='etiquette_ephem_') data_directory = self.ephemeral_directory.name else: - data_directory = constants.DEFAULT_DATADIR + data_directory = pathclass.cwd().with_child(constants.DEFAULT_DATADIR) if isinstance(data_directory, str): data_directory = helpers.remove_path_badchars(data_directory, allowed=':/\\') @@ -1880,6 +1880,34 @@ class PhotoDB( self.sql_executescript(constants.DB_PRAGMAS) self.sql.commit() + @classmethod + def closest_photodb(cls, *args, **kwargs): + ''' + Starting from the cwd and climbing upwards towards the filesystem root, + look for an existing Etiquette data directory and return the PhotoDB + object. If none exists, raise exceptions.NoClosestPhotoDB. + ''' + cwd = pathclass.cwd() + + path = cwd + while True: + if path.with_child(constants.DEFAULT_DATADIR).is_dir: + break + parent = path.parent + if path == parent: + raise exceptions.NoClosestPhotoDB(cwd) + path = parent + + path = path.with_child(constants.DEFAULT_DATADIR) + photodb = cls( + path, + create=False, + *args, + **kwargs, + ) + photodb.log.debug('Found closest PhotoDB at %s.', path) + return photodb + def __del__(self): self.close() diff --git a/frontends/etiquette_cli.py b/frontends/etiquette_cli.py index 4ff08d7..c914061 100644 --- a/frontends/etiquette_cli.py +++ b/frontends/etiquette_cli.py @@ -6,6 +6,7 @@ import sys from voussoirkit import betterhelp from voussoirkit import interactive from voussoirkit import pathclass +from voussoirkit import pipeable from voussoirkit import spinal from voussoirkit import stringtools from voussoirkit import vlogging @@ -14,31 +15,19 @@ import etiquette LOG_LEVEL = vlogging.NOTSET -class CantFindPhotoDB(Exception): - pass photodbs = {} def find_photodb(): - path = pathclass.cwd() + cwd = pathclass.cwd() + try: + return photodbs[cwd] + except KeyError: + pass - while True: - try: - return photodbs[path] - except KeyError: - pass - if path.with_child('_etiquette').is_dir: - break - if path == path.parent: - raise CantFindPhotoDB() - path = path.parent - - photodb = etiquette.photodb.PhotoDB( - path.with_child('_etiquette'), - create=False, - log_level=LOG_LEVEL, - ) - photodbs[path] = photodb + # If this raises, main will catch it. + photodb = etiquette.photodb.PhotoDB.closest_photodb() + photodbs[cwd] = photodb return photodb # HELPERS ########################################################################################## @@ -780,13 +769,18 @@ def main(argv): ) return args - return betterhelp.subparser_main( - primary_args, - parser, - main_docstring=DOCSTRING, - sub_docstrings=SUB_DOCSTRINGS, - args_postprocessor=pp, - ) + try: + return betterhelp.subparser_main( + primary_args, + parser, + main_docstring=DOCSTRING, + sub_docstrings=SUB_DOCSTRINGS, + args_postprocessor=pp, + ) + except etiquette.exceptions.NoClosestPhotoDB as exc: + pipeable.stderr(exc.error_message) + pipeable.stderr('Try etiquette_cli init') + return 1 if __name__ == '__main__': raise SystemExit(main(sys.argv[1:])) diff --git a/frontends/etiquette_flask/backend/common.py b/frontends/etiquette_flask/backend/common.py index a36cc4f..eb52525 100644 --- a/frontends/etiquette_flask/backend/common.py +++ b/frontends/etiquette_flask/backend/common.py @@ -320,4 +320,4 @@ def send_file(filepath, override_mimetype=None): def init_photodb(*args, **kwargs): global P - P = etiquette.photodb.PhotoDB(*args, **kwargs) + P = etiquette.photodb.PhotoDB.closest_photodb(*args, **kwargs) diff --git a/frontends/etiquette_flask/etiquette_flask_dev.py b/frontends/etiquette_flask/etiquette_flask_dev.py index ba20023..5315259 100644 --- a/frontends/etiquette_flask/etiquette_flask_dev.py +++ b/frontends/etiquette_flask/etiquette_flask_dev.py @@ -18,8 +18,10 @@ import os import sys from voussoirkit import pathclass +from voussoirkit import pipeable from voussoirkit import vlogging +import etiquette import backend site = backend.site @@ -31,7 +33,6 @@ LOG_LEVEL = vlogging.NOTSET def etiquette_flask_launch( *, - create, localhost_only, port, use_https, @@ -55,7 +56,12 @@ def etiquette_flask_launch( if localhost_only: site.localhost_only = True - backend.common.init_photodb(create=create, log_level=LOG_LEVEL) + try: + backend.common.init_photodb(log_level=LOG_LEVEL) + except etiquette.exceptions.NoClosestPhotoDB as exc: + pipeable.stderr(exc.error_message) + pipeable.stderr('Try etiquette_cli init') + return 1 message = f'Starting server on port {port}, pid={os.getpid()}' if use_https: @@ -69,7 +75,6 @@ def etiquette_flask_launch( def etiquette_flask_launch_argparse(args): return etiquette_flask_launch( - create=args.create, localhost_only=args.localhost_only, port=args.port, use_https=args.use_https, @@ -82,7 +87,6 @@ def main(argv): parser = argparse.ArgumentParser() parser.add_argument('port', nargs='?', type=int, default=5000) - parser.add_argument('--dont_create', '--dont-create', '--no-create', dest='create', action='store_false', default=True) parser.add_argument('--https', dest='use_https', action='store_true', default=None) parser.add_argument('--localhost_only', '--localhost-only', dest='localhost_only', action='store_true') parser.set_defaults(func=etiquette_flask_launch_argparse) diff --git a/frontends/etiquette_repl.py b/frontends/etiquette_repl.py index 9169361..26c3e25 100644 --- a/frontends/etiquette_repl.py +++ b/frontends/etiquette_repl.py @@ -10,6 +10,7 @@ import sys import traceback from voussoirkit import interactive +from voussoirkit import pipeable from voussoirkit import vlogging import etiquette @@ -38,7 +39,13 @@ def photag(photo_id): def erepl_argparse(args): global P - P = etiquette.photodb.PhotoDB(create=args.create, log_level=LOG_LEVEL) + + try: + P = etiquette.photodb.PhotoDB.closest_photodb(log_level=LOG_LEVEL) + except etiquette.exceptions.NoClosestPhotoDB as exc: + pipeable.stderr(exc.error_message) + pipeable.stderr('Try etiquette_cli init') + return 1 if args.exec_statement: exec(args.exec_statement) @@ -62,7 +69,6 @@ def main(argv): parser = argparse.ArgumentParser() parser.add_argument('--exec', dest='exec_statement', default=None) - parser.add_argument('--dont_create', '--dont-create', '--no-create', dest='create', action='store_false', default=True) parser.set_defaults(func=erepl_argparse) args = parser.parse_args(argv)