From e01918e607c38b37ee921d4b74f32e6da5f1da83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 May 2022 03:02:54 +0200 Subject: [PATCH] add more docstrings for plugin app --- InvenTree/plugin/admin.py | 3 ++ InvenTree/plugin/api.py | 4 ++ InvenTree/plugin/apps.py | 8 +++ InvenTree/plugin/base/action/api.py | 2 +- InvenTree/plugin/base/action/test_action.py | 4 ++ InvenTree/plugin/base/barcodes/api.py | 10 +++- .../plugin/base/barcodes/test_barcode.py | 15 ++++-- InvenTree/plugin/base/event/mixins.py | 1 + InvenTree/plugin/base/integration/mixins.py | 54 ++++++++++++++++++- .../plugin/base/integration/test_mixins.py | 25 ++++++++- .../plugin/base/label/test_label_mixin.py | 2 +- InvenTree/plugin/base/locate/api.py | 2 +- InvenTree/plugin/base/locate/mixins.py | 2 + InvenTree/plugin/base/locate/test_locate.py | 1 + .../builtin/action/test_simpleactionplugin.py | 1 + .../barcodes/test_inventree_barcode.py | 1 + .../integration/test_core_notifications.py | 3 ++ InvenTree/plugin/helpers.py | 8 +++ InvenTree/plugin/models.py | 9 +++- InvenTree/plugin/plugin.py | 19 +++++-- InvenTree/plugin/registry.py | 16 ++++-- .../samples/integration/broken_sample.py | 1 + .../integration/custom_panel_sample.py | 2 +- .../samples/integration/label_sample.py | 8 +++ .../plugin/samples/integration/sample.py | 1 + .../samples/integration/scheduled_task.py | 8 +++ .../plugin/samples/locate/locate_sample.py | 8 +++ InvenTree/plugin/serializers.py | 9 +++- InvenTree/plugin/template.py | 1 + InvenTree/plugin/test_api.py | 2 + InvenTree/plugin/test_plugin.py | 2 + InvenTree/plugin/views.py | 2 + 32 files changed, 211 insertions(+), 23 deletions(-) diff --git a/InvenTree/plugin/admin.py b/InvenTree/plugin/admin.py index dd3a1b5644..84da9e9d98 100644 --- a/InvenTree/plugin/admin.py +++ b/InvenTree/plugin/admin.py @@ -1,3 +1,4 @@ +"""Admin for plugin app.""" from django.contrib import admin @@ -43,6 +44,7 @@ class PluginSettingInline(admin.TabularInline): ] def has_add_permission(self, request, obj): + """The plugin settings should not be meddled with manually.""" return False @@ -66,6 +68,7 @@ class NotificationUserSettingAdmin(admin.ModelAdmin): ] def has_add_permission(self, request): + """Notifications should not be changed.""" return False diff --git a/InvenTree/plugin/api.py b/InvenTree/plugin/api.py index 00cab46dc3..dadebf0b49 100644 --- a/InvenTree/plugin/api.py +++ b/InvenTree/plugin/api.py @@ -31,6 +31,10 @@ class PluginList(generics.ListAPIView): queryset = PluginConfig.objects.all() def filter_queryset(self, queryset): + """Filter for API requests. + + Filter by mixin with the `mixin` flag + """ queryset = super().filter_queryset(queryset) params = self.request.query_params diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index 2a1ef9f71a..374be254e5 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -1,3 +1,8 @@ +"""Apps file for plugin app. + +This initializes the plugin mechanisms and handles reloading throught the lifecycle. +The main code for plugin special sauce is in the plugin registry in `InvenTree/plugin/registry.py`. +""" import logging @@ -15,9 +20,12 @@ logger = logging.getLogger('inventree') class PluginAppConfig(AppConfig): + """AppConfig for plugins.""" + name = 'plugin' def ready(self): + """The ready method is extended to initialize plugins.""" if settings.PLUGINS_ENABLED: if not canAppAccessDatabase(allow_test=True): logger.info("Skipping plugin loading sequence") # pragma: no cover diff --git a/InvenTree/plugin/base/action/api.py b/InvenTree/plugin/base/action/api.py index 4d77121e2d..735b46f65b 100644 --- a/InvenTree/plugin/base/action/api.py +++ b/InvenTree/plugin/base/action/api.py @@ -17,7 +17,7 @@ class ActionPluginView(APIView): ] def post(self, request, *args, **kwargs): - + """This function checks if all required info was submitted and then performs a plugin_action or returns an error.""" action = request.data.get('action', None) data = request.data.get('data', None) diff --git a/InvenTree/plugin/base/action/test_action.py b/InvenTree/plugin/base/action/test_action.py index c73ebc64b7..ead7e8f259 100644 --- a/InvenTree/plugin/base/action/test_action.py +++ b/InvenTree/plugin/base/action/test_action.py @@ -13,6 +13,10 @@ class ActionMixinTests(TestCase): ACTION_RETURN = 'a action was performed' def setUp(self): + """Setup enviroment for tests. + + Contains multiple sample plugins that are used in the tests + """ class SimplePlugin(ActionMixin, InvenTreePlugin): pass self.plugin = SimplePlugin() diff --git a/InvenTree/plugin/base/barcodes/api.py b/InvenTree/plugin/base/barcodes/api.py index 8b2d49edb1..7cda2576c0 100644 --- a/InvenTree/plugin/base/barcodes/api.py +++ b/InvenTree/plugin/base/barcodes/api.py @@ -1,3 +1,5 @@ +"""API endpoints for barcode plugins.""" + from django.urls import path, re_path, reverse from django.utils.translation import gettext_lazy as _ @@ -40,7 +42,10 @@ class BarcodeScan(APIView): ] def post(self, request, *args, **kwargs): - """Respond to a barcode POST request.""" + """Respond to a barcode POST request. + + Check if required info was provided and then run though the plugin steps or try to match up- + """ data = request.data if 'barcode' not in data: @@ -139,7 +144,10 @@ class BarcodeAssign(APIView): ] def post(self, request, *args, **kwargs): + """Respond to a barcode assign POST request. + Checks inputs and assign barcode (hash) to StockItem. + """ data = request.data if 'barcode' not in data: diff --git a/InvenTree/plugin/base/barcodes/test_barcode.py b/InvenTree/plugin/base/barcodes/test_barcode.py index 3c4f1f7ae1..932ae0d463 100644 --- a/InvenTree/plugin/base/barcodes/test_barcode.py +++ b/InvenTree/plugin/base/barcodes/test_barcode.py @@ -9,6 +9,7 @@ from stock.models import StockItem class BarcodeAPITest(InvenTreeAPITestCase): + """Tests for barcode api.""" fixtures = [ 'category', @@ -18,17 +19,18 @@ class BarcodeAPITest(InvenTreeAPITestCase): ] def setUp(self): + """Setup for all tests.""" super().setUp() self.scan_url = reverse('api-barcode-scan') self.assign_url = reverse('api-barcode-link') def postBarcode(self, url, barcode): - + """Post barcode and return results.""" return self.client.post(url, format='json', data={'barcode': str(barcode)}) def test_invalid(self): - + """Test that invalid requests fail.""" # test scan url response = self.client.post(self.scan_url, format='json', data={}) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) @@ -44,7 +46,10 @@ class BarcodeAPITest(InvenTreeAPITestCase): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_empty(self): + """Test an empty barcode scan. + Ensure that all required data is in teh respomse. + """ response = self.postBarcode(self.scan_url, '') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -157,7 +162,7 @@ class BarcodeAPITest(InvenTreeAPITestCase): self.assertEqual(response.data['stocklocation'], 'Stock location does not exist') def test_integer_barcode(self): - + """Test scan of an integer barcode.""" response = self.postBarcode(self.scan_url, '123456789') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -171,7 +176,7 @@ class BarcodeAPITest(InvenTreeAPITestCase): self.assertIsNone(data['plugin']) def test_array_barcode(self): - + """Test scan of barcode with string encoded array.""" response = self.postBarcode(self.scan_url, "['foo', 'bar']") self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -185,7 +190,7 @@ class BarcodeAPITest(InvenTreeAPITestCase): self.assertIsNone(data['plugin']) def test_barcode_generation(self): - + """Test that a barcode is generated with a scan.""" item = StockItem.objects.get(pk=522) response = self.postBarcode(self.scan_url, item.format_barcode()) diff --git a/InvenTree/plugin/base/event/mixins.py b/InvenTree/plugin/base/event/mixins.py index 945f73a5a1..2aa67b87a9 100644 --- a/InvenTree/plugin/base/event/mixins.py +++ b/InvenTree/plugin/base/event/mixins.py @@ -23,5 +23,6 @@ class EventMixin: MIXIN_NAME = 'Events' def __init__(self): + """Register the mixin.""" super().__init__() self.add_mixin('events', True, __class__) diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py index 57d7b27df5..2a41074b29 100644 --- a/InvenTree/plugin/base/integration/mixins.py +++ b/InvenTree/plugin/base/integration/mixins.py @@ -22,9 +22,11 @@ class SettingsMixin: """Mixin that enables global settings for the plugin.""" class MixinMeta: + """Meta for mixin.""" MIXIN_NAME = 'Settings' def __init__(self): + """Register mixin.""" super().__init__() self.add_mixin('settings', 'has_settings', __class__) self.settings = getattr(self, 'SETTINGS', {}) @@ -91,6 +93,7 @@ class ScheduleMixin: MIXIN_NAME = 'Schedule' def __init__(self): + """Register mixin.""" super().__init__() self.scheduled_tasks = self.get_scheduled_tasks() self.validate_scheduled_tasks() @@ -98,6 +101,10 @@ class ScheduleMixin: self.add_mixin('schedule', 'has_scheduled_tasks', __class__) def get_scheduled_tasks(self): + """Returns `SCHEDULED_TASKS` context. + + Override if you want the scheduled tasks to be dynamic (influenced by settings for example). + """ return getattr(self, 'SCHEDULED_TASKS', {}) @property @@ -216,6 +223,7 @@ class UrlsMixin: MIXIN_NAME = 'URLs' def __init__(self): + """Register mixin.""" super().__init__() self.add_mixin('urls', 'has_urls', __class__) self.urls = self.setup_urls() @@ -259,6 +267,7 @@ class NavigationMixin: MIXIN_NAME = 'Navigation Links' def __init__(self): + """Register mixin.""" super().__init__() self.add_mixin('navigation', 'has_naviation', __class__) self.navigation = self.setup_navigation() @@ -301,6 +310,7 @@ class AppMixin: MIXIN_NAME = 'App registration' def __init__(self): + """Register mixin.""" super().__init__() self.add_mixin('app', 'has_app', __class__) @@ -366,6 +376,7 @@ class APICallMixin: MIXIN_NAME = 'API calls' def __init__(self): + """Register mixin.""" super().__init__() self.add_mixin('api_call', 'has_api_call', __class__) @@ -380,22 +391,49 @@ class APICallMixin: @property def api_url(self): + """Base url path.""" return f'{self.API_METHOD}://{self.get_setting(self.API_URL_SETTING)}' @property def api_headers(self): + """Returns the default headers for requests with api_call. + + Contains a header with the key set in `API_TOKEN` for the plugin it `API_TOKEN_SETTING` is defined. + Check the mixin class docstring for a full example. + """ headers = {'Content-Type': 'application/json'} if getattr(self, 'API_TOKEN_SETTING'): headers[self.API_TOKEN] = self.get_setting(self.API_TOKEN_SETTING) return headers - def api_build_url_args(self, arguments): + def api_build_url_args(self, arguments: dict) -> str: + """Returns an encoded path for the provided dict.""" groups = [] for key, val in arguments.items(): groups.append(f'{key}={",".join([str(a) for a in val])}') return f'?{"&".join(groups)}' - def api_call(self, endpoint, method: str = 'GET', url_args=None, data=None, headers=None, simple_response: bool = True, endpoint_is_url: bool = False): + def api_call(self, endpoint: str, method: str = 'GET', url_args: dict = None, data=None, headers: dict = None, simple_response: bool = True, endpoint_is_url: bool = False): + """Do an API call. + + Simplest call example: + ```python + self.api_call('hello') + ``` + Will call the `{base_url}/hello` with a GET request and - if set - the token for this plugin. + + Args: + endpoint (str): Path to current endpoint. Either the endpoint or the full or if the flag is set + method (str, optional): HTTP method that should be uses - capitalized. Defaults to 'GET'. + url_args (dict, optional): arguments that should be appended to the url. Defaults to None. + data (Any, optional): Data that should be transmitted in the body - must be JSON serializable. Defaults to None. + headers (dict, optional): Headers that should be used for the request. Defaults to self.api_headers. + simple_response (bool, optional): Return the response as JSON. Defaults to True. + endpoint_is_url (bool, optional): The provided endpoint is the full url - do not use self.api_url as base. Defaults to False. + + Returns: + Response + """ if url_args: endpoint += self.api_build_url_args(url_args) @@ -469,9 +507,12 @@ class PanelMixin: """ class MixinMeta: + """Meta for mixin.""" + MIXIN_NAME = 'Panel' def __init__(self): + """Register mixin.""" super().__init__() self.add_mixin('panel', True, __class__) @@ -500,7 +541,16 @@ class PanelMixin: return context def render_panels(self, view, request, context): + """Get panels for a view. + Args: + view: Current view context + request: Current request for passthrough + context: Rendering context + + Returns: + Array of panels + """ panels = [] # Construct an updated context object for template rendering diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py index 7362485f91..40cb1591b8 100644 --- a/InvenTree/plugin/base/integration/test_mixins.py +++ b/InvenTree/plugin/base/integration/test_mixins.py @@ -17,7 +17,10 @@ from plugin.urls import PLUGIN_BASE class BaseMixinDefinition: + """Mixin to test the meta functions of all mixins.""" + def test_mixin_name(self): + """Test that the mixin registers itseld correctly.""" # mixin name self.assertIn(self.MIXIN_NAME, [item['key'] for item in self.mixin.registered_mixins]) # human name @@ -25,6 +28,8 @@ class BaseMixinDefinition: class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase): + """Tests for SettingsMixin.""" + MIXIN_HUMAN_NAME = 'Settings' MIXIN_NAME = 'settings' MIXIN_ENABLE_CHECK = 'has_settings' @@ -32,6 +37,7 @@ class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase): TEST_SETTINGS = {'SETTING1': {'default': '123', }} def setUp(self): + """Setup for all tests.""" class SettingsCls(SettingsMixin, InvenTreePlugin): SETTINGS = self.TEST_SETTINGS self.mixin = SettingsCls() @@ -43,6 +49,7 @@ class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase): super().setUp() def test_function(self): + """Test that the mixin functions.""" # settings variable self.assertEqual(self.mixin.settings, self.TEST_SETTINGS) @@ -60,11 +67,14 @@ class SettingsMixinTest(BaseMixinDefinition, InvenTreeTestCase): class UrlsMixinTest(BaseMixinDefinition, TestCase): + """Tests for UrlsMixin.""" + MIXIN_HUMAN_NAME = 'URLs' MIXIN_NAME = 'urls' MIXIN_ENABLE_CHECK = 'has_urls' def setUp(self): + """Setup for all tests.""" class UrlsCls(UrlsMixin, InvenTreePlugin): def test(): return 'ccc' @@ -76,6 +86,7 @@ class UrlsMixinTest(BaseMixinDefinition, TestCase): self.mixin_nothing = NoUrlsCls() def test_function(self): + """Test that the mixin functions.""" plg_name = self.mixin.plugin_name() # base_url @@ -99,26 +110,32 @@ class UrlsMixinTest(BaseMixinDefinition, TestCase): class AppMixinTest(BaseMixinDefinition, TestCase): + """Tests for AppMixin.""" + MIXIN_HUMAN_NAME = 'App registration' MIXIN_NAME = 'app' MIXIN_ENABLE_CHECK = 'has_app' def setUp(self): + """Setup for all tests.""" class TestCls(AppMixin, InvenTreePlugin): pass self.mixin = TestCls() def test_function(self): - # test that this plugin is in settings + """Test that the sample plugin registers in settings.""" self.assertIn('plugin.samples.integration', settings.INSTALLED_APPS) class NavigationMixinTest(BaseMixinDefinition, TestCase): + """Tests for NavigationMixin.""" + MIXIN_HUMAN_NAME = 'Navigation Links' MIXIN_NAME = 'navigation' MIXIN_ENABLE_CHECK = 'has_naviation' def setUp(self): + """Setup for all tests.""" class NavigationCls(NavigationMixin, InvenTreePlugin): NAVIGATION = [ {'name': 'aa', 'link': 'plugin:test:test_view'}, @@ -131,6 +148,7 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase): self.nothing_mixin = NothingNavigationCls() def test_function(self): + """Test that a correct configuration functions.""" # check right configuration self.assertEqual(self.mixin.navigation, [{'name': 'aa', 'link': 'plugin:test:test_view'}, ]) @@ -139,7 +157,7 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase): self.assertEqual(self.nothing_mixin.navigation_name, '') def test_fail(self): - # check wrong links fails + """Test that wrong links fail.""" with self.assertRaises(NotImplementedError): class NavigationCls(NavigationMixin, InvenTreePlugin): NAVIGATION = ['aa', 'aa'] @@ -147,11 +165,14 @@ class NavigationMixinTest(BaseMixinDefinition, TestCase): class APICallMixinTest(BaseMixinDefinition, TestCase): + """Tests for APICallMixin.""" + MIXIN_HUMAN_NAME = 'API calls' MIXIN_NAME = 'api_call' MIXIN_ENABLE_CHECK = 'has_api_call' def setUp(self): + """Setup for all tests.""" class MixinCls(APICallMixin, SettingsMixin, InvenTreePlugin): NAME = "Sample API Caller" diff --git a/InvenTree/plugin/base/label/test_label_mixin.py b/InvenTree/plugin/base/label/test_label_mixin.py index 0f362287dd..4772d5d41f 100644 --- a/InvenTree/plugin/base/label/test_label_mixin.py +++ b/InvenTree/plugin/base/label/test_label_mixin.py @@ -163,7 +163,7 @@ class LabelMixinTests(InvenTreeAPITestCase): self.get(self.do_url(None, plugin_ref, label), expected_code=400) def test_printing_endpoints(self): - """Cover the endpoints not covered by `test_printing_process`""" + """Cover the endpoints not covered by `test_printing_process`.""" plugin_ref = 'samplelabel' # Activate the label components diff --git a/InvenTree/plugin/base/locate/api.py b/InvenTree/plugin/base/locate/api.py index ee3b613267..a7d6f5d86e 100644 --- a/InvenTree/plugin/base/locate/api.py +++ b/InvenTree/plugin/base/locate/api.py @@ -18,7 +18,7 @@ class LocatePluginView(APIView): ] def post(self, request, *args, **kwargs): - + """Check inputs and offload the task to the plugin.""" # Which plugin to we wish to use? plugin = request.data.get('plugin', None) diff --git a/InvenTree/plugin/base/locate/mixins.py b/InvenTree/plugin/base/locate/mixins.py index 7cf3d80e4d..82780f7823 100644 --- a/InvenTree/plugin/base/locate/mixins.py +++ b/InvenTree/plugin/base/locate/mixins.py @@ -24,9 +24,11 @@ class LocateMixin: """ class MixinMeta: + """Meta for mixin.""" MIXIN_NAME = "Locate" def __init__(self): + """Register the mixin.""" super().__init__() self.add_mixin('locate', True, __class__) diff --git a/InvenTree/plugin/base/locate/test_locate.py b/InvenTree/plugin/base/locate/test_locate.py index 8e20554e1b..32e8540fb2 100644 --- a/InvenTree/plugin/base/locate/test_locate.py +++ b/InvenTree/plugin/base/locate/test_locate.py @@ -9,6 +9,7 @@ from stock.models import StockItem, StockLocation class LocatePluginTests(InvenTreeAPITestCase): + """Tests for LocateMixin.""" fixtures = [ 'category', diff --git a/InvenTree/plugin/builtin/action/test_simpleactionplugin.py b/InvenTree/plugin/builtin/action/test_simpleactionplugin.py index 32da3d8540..6bc2329496 100644 --- a/InvenTree/plugin/builtin/action/test_simpleactionplugin.py +++ b/InvenTree/plugin/builtin/action/test_simpleactionplugin.py @@ -8,6 +8,7 @@ class SimpleActionPluginTests(InvenTreeTestCase): """Tests for SampleIntegrationPlugin.""" def setUp(self): + """Setup for tests.""" super().setUp() self.plugin = SimpleActionPlugin() diff --git a/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py b/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py index c4618483cf..b3fd51c781 100644 --- a/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py +++ b/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py @@ -8,6 +8,7 @@ from InvenTree.api_tester import InvenTreeAPITestCase class TestInvenTreeBarcode(InvenTreeAPITestCase): + """Tests for the integrated InvenTreeBarcode barcode plugin.""" fixtures = [ 'category', diff --git a/InvenTree/plugin/builtin/integration/test_core_notifications.py b/InvenTree/plugin/builtin/integration/test_core_notifications.py index 07c7c5cb41..1ff60c74b1 100644 --- a/InvenTree/plugin/builtin/integration/test_core_notifications.py +++ b/InvenTree/plugin/builtin/integration/test_core_notifications.py @@ -1,3 +1,5 @@ +"""Tests for core_notifications.""" + from part.test_part import BaseNotificationIntegrationTest from plugin import registry from plugin.builtin.integration.core_notifications import \ @@ -6,6 +8,7 @@ from plugin.models import NotificationUserSetting class CoreNotificationTestTests(BaseNotificationIntegrationTest): + """Tests for CoreNotificationsPlugin.""" def test_email(self): """Ensure that the email notifications run.""" diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 10f2706749..247ae72654 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -22,10 +22,17 @@ class IntegrationPluginError(Exception): """Error that encapsulates another error and adds the path / reference of the raising plugin.""" def __init__(self, path, message): + """Init a plugin error. + + Args: + path: Path on which the error occured - used to find out which plugin it was + message: The original error message + """ self.path = path self.message = message def __str__(self): + """Returns the error message.""" return self.message # pragma: no cover @@ -142,6 +149,7 @@ class GitStatus: msg: str = '' def __init__(self, key: str = 'N', status: int = 2, msg: str = '') -> None: + """Define a git Status -> needed for lookup.""" self.key = key self.status = status self.msg = msg diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index 0bed6ece8d..9a58e96fdc 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -24,6 +24,7 @@ class MetadataMixin(models.Model): """ class Meta: + """Meta for MetadataMixin.""" abstract = True metadata = models.JSONField( @@ -74,6 +75,7 @@ class PluginConfig(models.Model): """ class Meta: + """Meta for PluginConfig.""" verbose_name = _("Plugin Configuration") verbose_name_plural = _("Plugin Configurations") @@ -99,6 +101,7 @@ class PluginConfig(models.Model): ) def __str__(self) -> str: + """Nice name for printing.""" name = f'{self.name} - {self.key}' if not self.active: name += '(not active)' @@ -106,7 +109,7 @@ class PluginConfig(models.Model): # extra attributes from the registry def mixins(self): - + """Returns all registered mixins.""" try: return self.plugin._mixinreg except (AttributeError, ValueError): # pragma: no cover @@ -153,6 +156,7 @@ class PluginSetting(common.models.BaseInvenTreeSetting): """This model represents settings for individual plugins.""" class Meta: + """Meta for PluginSetting.""" unique_together = [ ('plugin', 'key'), ] @@ -201,12 +205,14 @@ class NotificationUserSetting(common.models.BaseInvenTreeSetting): """This model represents notification settings for a user.""" class Meta: + """Meta for NotificationUserSetting.""" unique_together = [ ('method', 'user', 'key'), ] @classmethod def get_setting_definition(cls, key, **kwargs): + """Override setting_definition to use notification settings.""" from common.notifications import storage kwargs['settings'] = storage.user_settings @@ -234,4 +240,5 @@ class NotificationUserSetting(common.models.BaseInvenTreeSetting): ) def __str__(self) -> str: + """Nice name of printing.""" return f'{self.key} (for {self.user}): {self.value}' diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 944f06ab6e..8b0c0bc847 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -117,6 +117,10 @@ class MixinBase: """Base set of mixin functions and mechanisms.""" def __init__(self, *args, **kwargs) -> None: + """Init sup-parts. + + Adds state dicts. + """ self._mixinreg = {} self._mixins = {} super().__init__(*args, **kwargs) @@ -180,6 +184,10 @@ class InvenTreePlugin(MixinBase, MetaBase): LICENSE = None def __init__(self): + """Init a plugin. + + Set paths and load metadata. + """ super().__init__() self.add_mixin('base') self.def_path = inspect.getfile(self.__class__) @@ -198,7 +206,7 @@ class InvenTreePlugin(MixinBase, MetaBase): @property def author(self): - """Author of plugin - either from plugin settings or git""" + """Author of plugin - either from plugin settings or git.""" author = getattr(self, 'AUTHOR', None) if not author: author = self.package.get('author') @@ -208,7 +216,7 @@ class InvenTreePlugin(MixinBase, MetaBase): @property def pub_date(self): - """Publishing date of plugin - either from plugin settings or git""" + """Publishing date of plugin - either from plugin settings or git.""" pub_date = getattr(self, 'PUBLISH_DATE', None) if not pub_date: pub_date = self.package.get('date') @@ -226,7 +234,7 @@ class InvenTreePlugin(MixinBase, MetaBase): @property def website(self): - """Website of plugin - if set else None""" + """Website of plugin - if set else None.""" website = getattr(self, 'WEBSITE', None) return website @@ -293,6 +301,11 @@ class InvenTreePlugin(MixinBase, MetaBase): class IntegrationPluginBase(InvenTreePlugin): + """Legacy base class for plugins. + + Do not use! + """ + def __init__(self, *args, **kwargs): """Send warning about using this reference.""" # TODO remove in 0.8.0 diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index b00d15b9d8..16c56f3ab4 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -33,6 +33,10 @@ class PluginsRegistry: """The PluginsRegistry class.""" def __init__(self) -> None: + """Initialize registry. + + Set up all needed references for internal and external states. + """ # plugin registry self.plugins = {} self.plugins_inactive = {} @@ -334,7 +338,11 @@ class PluginsRegistry: # region mixin specific loading ... def activate_plugin_settings(self, plugins): + """Activate plugin settings. + Add all defined settings form the plugins to a unified dict in the registry. + This dict is referenced by the PluginSettings for settings definitions. + """ logger.info('Activating plugin settings') self.mixins_settings = {} @@ -356,7 +364,7 @@ class PluginsRegistry: self.mixins_settings = {} def activate_plugin_schedule(self, plugins): - + """Activate scheudles from plugins with the ScheduleMixin.""" logger.info('Activating plugin tasks') from common.models import InvenTreeSetting @@ -407,7 +415,7 @@ class PluginsRegistry: pass def activate_plugin_app(self, plugins, force_reload=False): - """Activate AppMixin plugins - add custom apps and reload + """Activate AppMixin plugins - add custom apps and reload. :param plugins: list of IntegrationPlugins that should be installed :type plugins: dict @@ -445,7 +453,7 @@ class PluginsRegistry: self._update_urls() def _reregister_contrib_apps(self): - """Fix reloading of contrib apps - models and admin + """Fix reloading of contrib apps - models and admin. This is needed if plugins were loaded earlier and then reloaded as models and admins rely on imports. Those register models and admin in their respective objects (e.g. admin.site for admin). @@ -493,7 +501,7 @@ class PluginsRegistry: return plugin_path def deactivate_plugin_app(self): - """Deactivate AppMixin plugins - some magic required""" + """Deactivate AppMixin plugins - some magic required.""" # unregister models from admin for plugin_path in self.installed_apps: models = [] # the modelrefs need to be collected as poping an item in a iter is not welcomed diff --git a/InvenTree/plugin/samples/integration/broken_sample.py b/InvenTree/plugin/samples/integration/broken_sample.py index a5a2c494de..b873b93fa6 100644 --- a/InvenTree/plugin/samples/integration/broken_sample.py +++ b/InvenTree/plugin/samples/integration/broken_sample.py @@ -10,6 +10,7 @@ class BrokenIntegrationPlugin(InvenTreePlugin): SLUG = 'broken' def __init__(self): + """Raise a KeyError to provoke a range of unit tests and safety mechanisms in the plugin loading mechanism.""" super().__init__() raise KeyError('This is a dummy error') diff --git a/InvenTree/plugin/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py index ce31f65c98..a3228582e6 100644 --- a/InvenTree/plugin/samples/integration/custom_panel_sample.py +++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py @@ -31,7 +31,7 @@ class CustomPanelSample(PanelMixin, SettingsMixin, InvenTreePlugin): } def get_panel_context(self, view, request, context): - + """Returns enriched context.""" ctx = super().get_panel_context(view, request, context) # If we are looking at a StockLocationDetail view, add location context object diff --git a/InvenTree/plugin/samples/integration/label_sample.py b/InvenTree/plugin/samples/integration/label_sample.py index e367722610..52d1a724fe 100644 --- a/InvenTree/plugin/samples/integration/label_sample.py +++ b/InvenTree/plugin/samples/integration/label_sample.py @@ -1,3 +1,7 @@ +"""Simple sample for a plugin with the LabelPrintingMixin. + +This does not function in real usage and is more to show the required components and for unit tests. +""" from plugin import InvenTreePlugin from plugin.mixins import LabelPrintingMixin @@ -13,4 +17,8 @@ class SampleLabelPrinter(LabelPrintingMixin, InvenTreePlugin): VERSION = "0.1" def print_label(self, label, **kwargs): + """Sample printing step. + + Normally here the connection to the printer and transfer of the label would take place. + """ print("OK PRINTING") diff --git a/InvenTree/plugin/samples/integration/sample.py b/InvenTree/plugin/samples/integration/sample.py index 48bab78f91..402159455a 100644 --- a/InvenTree/plugin/samples/integration/sample.py +++ b/InvenTree/plugin/samples/integration/sample.py @@ -23,6 +23,7 @@ class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixi return HttpResponse(f'Hi there {request.user.username} this works') def setup_urls(self): + """Urls that are exposed by this plugin.""" he_urls = [ re_path(r'^he/', self.view_test, name='he'), re_path(r'^ha/', self.view_test, name='ha'), diff --git a/InvenTree/plugin/samples/integration/scheduled_task.py b/InvenTree/plugin/samples/integration/scheduled_task.py index 71ec8007ee..101a2e3687 100644 --- a/InvenTree/plugin/samples/integration/scheduled_task.py +++ b/InvenTree/plugin/samples/integration/scheduled_task.py @@ -6,10 +6,18 @@ from plugin.mixins import ScheduleMixin, SettingsMixin # Define some simple tasks to perform def print_hello(): + """Sample function that can be called on schedule. + + Contents do not matter - therefore no coverage. + """ print("Hello") # pragma: no cover def print_world(): + """Sample function that can be called on schedule. + + Contents do not matter - therefore no coverage. + """ print("World") # pragma: no cover diff --git a/InvenTree/plugin/samples/locate/locate_sample.py b/InvenTree/plugin/samples/locate/locate_sample.py index 99307f04e6..7cf63e2bf2 100644 --- a/InvenTree/plugin/samples/locate/locate_sample.py +++ b/InvenTree/plugin/samples/locate/locate_sample.py @@ -24,7 +24,11 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin): VERSION = "0.2" def locate_stock_item(self, item_pk): + """Locate a StockItem. + Args: + item_pk: primary key for item + """ from stock.models import StockItem logger.info(f"SampleLocatePlugin attempting to locate item ID {item_pk}") @@ -40,7 +44,11 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin): logger.error(f"StockItem ID {item_pk} does not exist!") def locate_stock_location(self, location_pk): + """Locate a StockLocation. + Args: + location_pk: primary key for location + """ from stock.models import StockLocation logger.info(f"SampleLocatePlugin attempting to locate location ID {location_pk}") diff --git a/InvenTree/plugin/serializers.py b/InvenTree/plugin/serializers.py index a29ca56046..a2552692de 100644 --- a/InvenTree/plugin/serializers.py +++ b/InvenTree/plugin/serializers.py @@ -41,12 +41,13 @@ class MetadataSerializer(serializers.ModelSerializer): class PluginConfigSerializer(serializers.ModelSerializer): - """Serializer for a PluginConfig:""" + """Serializer for a PluginConfig.""" meta = serializers.DictField(read_only=True) mixins = serializers.DictField(read_only=True) class Meta: + """Meta for serializer.""" model = PluginConfig fields = [ 'key', @@ -78,6 +79,7 @@ class PluginConfigInstallSerializer(serializers.Serializer): ) class Meta: + """Meta for serializer.""" fields = [ 'url', 'packagename', @@ -85,6 +87,10 @@ class PluginConfigInstallSerializer(serializers.Serializer): ] def validate(self, data): + """Validate inputs. + + Make sure both confirm and url are provided. + """ super().validate(data) # check the base requirements are met @@ -97,6 +103,7 @@ class PluginConfigInstallSerializer(serializers.Serializer): return data def save(self): + """Install a plugin from a package registry and set operational results as instance data.""" data = self.validated_data packagename = data.get('packagename', '') diff --git a/InvenTree/plugin/template.py b/InvenTree/plugin/template.py index e396b1dd6b..c16135a7a3 100644 --- a/InvenTree/plugin/template.py +++ b/InvenTree/plugin/template.py @@ -21,6 +21,7 @@ class PluginTemplateLoader(FilesystemLoader): """ def get_dirs(self): + """Returns all template dir paths in plugins.""" dirname = 'templates' template_dirs = [] diff --git a/InvenTree/plugin/test_api.py b/InvenTree/plugin/test_api.py index 0af5826970..53e5a67e2c 100644 --- a/InvenTree/plugin/test_api.py +++ b/InvenTree/plugin/test_api.py @@ -1,3 +1,4 @@ +"""Tests for general API tests for the plugin app.""" from django.urls import reverse @@ -15,6 +16,7 @@ class PluginDetailAPITest(InvenTreeAPITestCase): ] def setUp(self): + """Setup for all tests.""" self.MSG_NO_PKG = 'Either packagename of URL must be provided' self.PKG_NAME = 'minimal' diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py index f80f72d832..330071bf3d 100644 --- a/InvenTree/plugin/test_plugin.py +++ b/InvenTree/plugin/test_plugin.py @@ -15,6 +15,7 @@ class PluginTagTests(TestCase): """Tests for the plugin extras.""" def setUp(self): + """Setup for all tests.""" self.sample = SampleIntegrationPlugin() self.plugin_no = NoIntegrationPlugin() self.plugin_wrong = WrongIntegrationPlugin() @@ -60,6 +61,7 @@ class InvenTreePluginTests(TestCase): """Tests for InvenTreePlugin.""" def setUp(self): + """Setup for all tests.""" self.plugin = InvenTreePlugin() class NamedPlugin(InvenTreePlugin): diff --git a/InvenTree/plugin/views.py b/InvenTree/plugin/views.py index 1be07a6acf..aa43b9bd09 100644 --- a/InvenTree/plugin/views.py +++ b/InvenTree/plugin/views.py @@ -1,3 +1,5 @@ +"""Views for plugin app.""" + import logging import sys import traceback