diff --git a/docs/api/wuttjamaican/commands.make_appdir.rst b/docs/api/wuttjamaican/commands.make_appdir.rst new file mode 100644 index 0000000..b7627a9 --- /dev/null +++ b/docs/api/wuttjamaican/commands.make_appdir.rst @@ -0,0 +1,6 @@ + +``wuttjamaican.commands.make_appdir`` +===================================== + +.. automodule:: wuttjamaican.commands.make_appdir + :members: diff --git a/docs/api/wuttjamaican/index.rst b/docs/api/wuttjamaican/index.rst index 0870c96..b7e3ca4 100644 --- a/docs/api/wuttjamaican/index.rst +++ b/docs/api/wuttjamaican/index.rst @@ -10,6 +10,7 @@ app commands commands.base + commands.make_appdir commands.setup conf db diff --git a/docs/glossary.rst b/docs/glossary.rst index 4c2aeff..345f4de 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -15,6 +15,11 @@ Glossary just one database (for simple apps) which uses PostgreSQL for the backend. + app dir + Folder containing app-specific config files, log files, etc. + Usually this is named ``app`` and is located at the root of the + virtual environment. + app handler Python object representing the core of the :term:`app`. There is normally just one "global" app handler, which is an instance of diff --git a/setup.cfg b/setup.cfg index 516529b..541b040 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,4 +51,5 @@ console_scripts = wutta = wuttjamaican.commands.base:main wutta.subcommands = + make-appdir = wuttjamaican.commands.make_appdir:MakeAppDir setup = wuttjamaican.commands.setup:Setup diff --git a/src/wuttjamaican/app.py b/src/wuttjamaican/app.py index 39896c7..f2e2b2f 100644 --- a/src/wuttjamaican/app.py +++ b/src/wuttjamaican/app.py @@ -24,6 +24,8 @@ WuttJamaican - app handler """ +import os + from wuttjamaican.util import load_entry_points, load_object, parse_bool @@ -47,6 +49,35 @@ class AppHandler: self.config = config self.handlers = {} + def make_appdir(self, path, subfolders=None, **kwargs): + """ + Establish an :term:`app dir` at the given path. + + Default logic only creates a few subfolders, meant to help + steer the admin toward a convention for sake of where to put + things. But custom app handlers are free to do whatever. + + :param path: Path to the desired app dir. If the path does + not yet exist then it will be created. But regardless it + should be "refreshed" (e.g. missing subfolders created) + when this method is called. + + :param subfolders: Optional list of subfolder names to create + within the app dir. If not specified, defaults will be: + ``['data', 'log', 'work']``. + """ + appdir = path + if not os.path.exists(appdir): + os.makedirs(appdir) + + if not subfolders: + subfolders = ['data', 'log', 'work'] + + for name in subfolders: + path = os.path.join(appdir, name) + if not os.path.exists(path): + os.mkdir(path) + def make_engine_from_config( self, config_dict, diff --git a/src/wuttjamaican/commands/make_appdir.py b/src/wuttjamaican/commands/make_appdir.py new file mode 100644 index 0000000..b501e6e --- /dev/null +++ b/src/wuttjamaican/commands/make_appdir.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# WuttJamaican -- Base package for Wutta Framework +# Copyright © 2023 Lance Edgar +# +# This file is part of Wutta Framework. +# +# Wutta Framework is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Wutta Framework is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# Wutta Framework. If not, see . +# +################################################################################ +""" +WuttJamaican - subcommand: make-appdir +""" + +import os +import sys + +from .base import Subcommand + + +class MakeAppDir(Subcommand): + """ + Make or refresh the "app dir" for virtual environment + """ + name = 'make-appdir' + description = __doc__.strip() + + def add_args(self): + """ """ + self.parser.add_argument('--path', metavar='APPDIR', + help="Optional path to desired app dir. If not specified " + "it will be named ``app`` and placed in the root of the " + "virtual environment.") + + def run(self, args): + """ + This may be used during setup to establish the :term:`app dir` + for a virtual environment. This folder will contain config + files, log files etc. used by the app. + + Calls :meth:`~wuttjamaican.app.AppHandler.make_appdir()` to do + the heavy lifting. + """ + if args.path: + appdir = os.path.abspath(args.path) + else: + appdir = os.path.join(sys.prefix, 'app') + + self.app.make_appdir(appdir) + self.stdout.write(f"established appdir: {appdir}\n") diff --git a/src/wuttjamaican/commands/setup.py b/src/wuttjamaican/commands/setup.py index 27306eb..416fe44 100644 --- a/src/wuttjamaican/commands/setup.py +++ b/src/wuttjamaican/commands/setup.py @@ -29,7 +29,7 @@ from .base import Subcommand class Setup(Subcommand): """ - Install and configure misc. software + Install and configure various software """ name = 'setup' description = __doc__.strip() diff --git a/tests/commands/test_make_appdir.py b/tests/commands/test_make_appdir.py new file mode 100644 index 0000000..a0eaa08 --- /dev/null +++ b/tests/commands/test_make_appdir.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8; -*- + +import os +import shutil +import tempfile +from unittest import TestCase +from unittest.mock import patch + +from wuttjamaican.conf import WuttaConfig +from wuttjamaican.commands import Command, make_appdir + + +class TestMakeAppDir(TestCase): + + def setUp(self): + self.config = WuttaConfig(appname='wuttatest') + self.command = Command(self.config, subcommands={ + 'make-appdir': make_appdir.MakeAppDir, + }) + + def test_run(self): + + # appdir is created, and 3 subfolders added by default + tempdir = tempfile.mkdtemp() + appdir = os.path.join(tempdir, 'app') + self.assertFalse(os.path.exists(appdir)) + self.command.run('make-appdir', '--path', appdir) + self.assertTrue(os.path.exists(appdir)) + self.assertEqual(len(os.listdir(appdir)), 3) + shutil.rmtree(tempdir) + + # subfolders still added if appdir already exists + tempdir = tempfile.mkdtemp() + self.assertTrue(os.path.exists(tempdir)) + self.assertEqual(len(os.listdir(tempdir)), 0) + self.command.run('make-appdir', '--path', tempdir) + self.assertEqual(len(os.listdir(tempdir)), 3) + shutil.rmtree(tempdir) + + # mock out sys.prefix to get coverage + with patch('wuttjamaican.commands.make_appdir.sys') as sys: + tempdir = tempfile.mkdtemp() + appdir = os.path.join(tempdir, 'app') + sys.prefix = tempdir + self.assertFalse(os.path.exists(appdir)) + self.command.run('make-appdir') + self.assertTrue(os.path.exists(appdir)) + self.assertEqual(len(os.listdir(appdir)), 3) + shutil.rmtree(tempdir) diff --git a/tests/test_app.py b/tests/test_app.py index 4b70d0e..4f79366 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,5 +1,8 @@ # -*- coding: utf-8; -*- +import os +import shutil +import tempfile from unittest import TestCase from unittest.mock import patch, MagicMock @@ -9,18 +12,38 @@ from sqlalchemy.engine import Engine from sqlalchemy.pool import NullPool from wuttjamaican import app, db +from wuttjamaican.conf import WuttaConfig class TestAppHandler(TestCase): def setUp(self): - self.config = MagicMock() + self.config = WuttaConfig(appname='wuttatest') self.app = app.AppHandler(self.config) def test_init(self): self.assertIs(self.app.config, self.config) self.assertEqual(self.app.handlers, {}) + def test_make_appdir(self): + + # appdir is created, and 3 subfolders added by default + tempdir = tempfile.mkdtemp() + appdir = os.path.join(tempdir, 'app') + self.assertFalse(os.path.exists(appdir)) + self.app.make_appdir(appdir) + self.assertTrue(os.path.exists(appdir)) + self.assertEqual(len(os.listdir(appdir)), 3) + shutil.rmtree(tempdir) + + # subfolders still added if appdir already exists + tempdir = tempfile.mkdtemp() + self.assertTrue(os.path.exists(tempdir)) + self.assertEqual(len(os.listdir(tempdir)), 0) + self.app.make_appdir(tempdir) + self.assertEqual(len(os.listdir(tempdir)), 3) + shutil.rmtree(tempdir) + def test_make_engine_from_config_basic(self): engine = self.app.make_engine_from_config({ 'sqlalchemy.url': 'sqlite://',