diff --git a/src/wuttjamaican/app.py b/src/wuttjamaican/app.py index fef2b30..8bec0d7 100644 --- a/src/wuttjamaican/app.py +++ b/src/wuttjamaican/app.py @@ -152,6 +152,90 @@ class AppHandler: return self.config.get(f'{self.appname}.app_title', default=default or self.default_app_title) + def get_distribution(self, obj=None): + """ + Returns the appropriate Python distribution name. + + If ``obj`` is specified, this will attempt to locate the + distribution based on the top-level module which contains the + object's type/class. + + If ``obj`` is *not* specified, this behaves a bit differently. + It first will look for a :term:`config setting` named + ``wutta.app_dist`` (or similar, dpending on :attr:`appname`). + If there is such a config value, it is returned. Otherwise + the "auto-locate" logic described above happens, but using + ``self`` instead of ``obj``. + + In other words by default this returns the distribution to + which the running :term:`app handler` belongs. + + See also :meth:`get_version()`. + + :param obj: Any object which may be used as a clue to locate + the appropriate distribution. + + :returns: string, or ``None`` + + Also note that a *distribution* name is different from a + *package* name. The distribution name is how things appear on + PyPI for instance. + + If you want to override the default distribution name (and + skip the auto-locate based on app handler) then you can define + it in config: + + .. code-block:: ini + + [wutta] + app_dist = My-Poser-Dist + """ + if obj is None: + print(self.appname) + dist = self.config.get(f'{self.appname}.app_dist') + if dist: + return dist + + # TODO: do we need a config setting for app_package ? + #modpath = self.config.get(f'{self.appname}.app_package') + modpath = None + if not modpath: + modpath = type(obj if obj is not None else self).__module__ + pkgname = modpath.split('.')[0] + + try: + from importlib.metadata import packages_distributions + except ImportError: # python < 3.10 + from importlib_metadata import packages_distributions + + pkgmap = packages_distributions() + if pkgname in pkgmap: + dist = pkgmap[pkgname][0] + return dist + + # fall back to configured dist, if obj lookup failed + if obj is not None: + return self.config.get(f'{self.appname}.app_dist') + + def get_version(self, dist=None, obj=None): + """ + Returns the version of a given Python distribution. + + If ``dist`` is not specified, calls :meth:`get_distribution()` + to get it. (It passes ``obj`` along for this). + + So by default this will return the version of whichever + distribution owns the running :term:`app handler`. + + :returns: Version as string. + """ + from importlib.metadata import version + + if not dist: + dist = self.get_distribution(obj=obj) + if dist: + return version(dist) + def get_model(self): """ Returns the :term:`app model` module. diff --git a/tests/test_app.py b/tests/test_app.py index b2aa391..583ab71 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -2,6 +2,7 @@ import os import shutil +import sys import tempfile import warnings from unittest import TestCase @@ -116,6 +117,88 @@ class TestAppHandler(TestCase): def test_get_title(self): self.assertEqual(self.app.get_title(), 'WuttJamaican') + def test_get_distribution(self): + + # default should always be WuttJamaican (right..?) + dist = self.app.get_distribution() + self.assertEqual(dist, 'WuttJamaican') + + # also works with "non-native" objects + from config import Configuration + config = Configuration({}) + dist = self.app.get_distribution(config) + self.assertEqual(dist, 'python-configuration') + + # can override dist via config + self.config.setdefault('wuttatest.app_dist', 'importlib_metadata') + dist = self.app.get_distribution() + self.assertEqual(dist, 'importlib_metadata') + + # but the provided object takes precedence + dist = self.app.get_distribution(config) + self.assertEqual(dist, 'python-configuration') + + def test_get_distribution_pre_python_3_10(self): + + # the goal here is to get coverage for code which would only + # run on python 3,9 and older, but we only need that coverage + # if we are currently testing python 3.10+ + if sys.version_info.major == 3 and sys.version_info.minor < 10: + pytest.skip("this test is not relevant before python 3.10") + + importlib_metadata = MagicMock() + importlib_metadata.packages_distributions = MagicMock( + return_value={ + 'wuttjamaican': ['WuttJamaican'], + 'config': ['python-configuration'], + }) + + orig_import = __import__ + + def mock_import(name, *args, **kwargs): + if name == 'importlib.metadata': + raise ImportError + if name == 'importlib_metadata': + return importlib_metadata + return orig_import(name, *args, **kwargs) + + with patch('builtins.__import__', side_effect=mock_import): + + # default should always be WuttJamaican (right..?) + dist = self.app.get_distribution() + self.assertEqual(dist, 'WuttJamaican') + + # also works with "non-native" objects + from config import Configuration + config = Configuration({}) + dist = self.app.get_distribution(config) + self.assertEqual(dist, 'python-configuration') + + # hacky sort of test, just in case we can't deduce the + # package dist based on the obj - easy enough since we + # have limited the packages_distributions() above + dist = self.app.get_distribution(42) + self.assertIsNone(dist) + + # can override dist via config + self.config.setdefault('wuttatest.app_dist', 'importlib_metadata') + dist = self.app.get_distribution() + self.assertEqual(dist, 'importlib_metadata') + + # but the provided object takes precedence + dist = self.app.get_distribution(config) + self.assertEqual(dist, 'python-configuration') + + # hacky test again, this time config override should win + dist = self.app.get_distribution(42) + self.assertEqual(dist, 'importlib_metadata') + + def test_get_version(self): + from importlib.metadata import version + + # default should always be for WuttJamaican (right..?) + self.assertEqual(self.app.get_version(), version('WuttJamaican')) + def test_make_title(self): text = self.app.make_title('foo_bar') self.assertEqual(text, "Foo Bar")