diff --git a/.pylintrc b/.pylintrc index bcf5b0b..893bde4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,11 +1,14 @@ # -*- mode: conf; -*- [MESSAGES CONTROL] -disable=all -enable=anomalous-backslash-in-string, - dangerous-default-value, - inconsistent-return-statements, - redefined-argument-from-local, - unspecified-encoding, - unused-argument, - unused-import, +disable= + attribute-defined-outside-init, + fixme, + import-outside-toplevel, + too-many-arguments, + too-many-branches, + too-many-instance-attributes, + too-many-lines, + too-many-locals, + too-many-positional-arguments, + too-many-public-methods, diff --git a/pyproject.toml b/pyproject.toml index 6e75b48..25fddc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ 'importlib-metadata; python_version < "3.10"', "importlib_resources ; python_version < '3.9'", "Mako", + "packaging", "progress", "python-configuration", "typer", diff --git a/src/wuttjamaican/_version.py b/src/wuttjamaican/_version.py index 9cd05c1..6a6bb9c 100644 --- a/src/wuttjamaican/_version.py +++ b/src/wuttjamaican/_version.py @@ -1,4 +1,7 @@ # -*- coding: utf-8; -*- +""" +Package Version +""" from importlib.metadata import version diff --git a/src/wuttjamaican/app.py b/src/wuttjamaican/app.py index aba810b..4e68a87 100644 --- a/src/wuttjamaican/app.py +++ b/src/wuttjamaican/app.py @@ -929,8 +929,8 @@ class AppHandler: # registered via entry points registered = [] - for Handler in load_entry_points(f'{self.appname}.batch.{key}').values(): - spec = Handler.get_spec() + for handler in load_entry_points(f'{self.appname}.batch.{key}').values(): + spec = handler.get_spec() if spec not in handlers: registered.append(spec) if registered: @@ -1045,7 +1045,7 @@ class AppHandler: self.get_email_handler().send_email(*args, **kwargs) -class AppProvider: +class AppProvider: # pylint: disable=too-few-public-methods """ Base class for :term:`app providers`. @@ -1123,6 +1123,7 @@ class GenericHandler: def __init__(self, config): self.config = config self.app = self.config.get_app() + self.modules = {} @property def appname(self): @@ -1139,3 +1140,35 @@ class GenericHandler: Returns the class :term:`spec` string for the handler. """ return f'{cls.__module__}:{cls.__name__}' + + def get_provider_modules(self, module_type): + """ + Returns a list of all available modules of the given type. + + Not all handlers would need such a thing, but notable ones + which do are the :term:`email handler` and :term:`report + handler`. Both can obtain classes (emails or reports) from + arbitrary modules, and this method is used to locate them. + + This will discover all modules exposed by the app + :term:`providers `, which expose an attribute with + name like ``f"{module_type}_modules"``. + + :param module_type: Unique name referring to a particular + "type" of modules to locate, e.g. ``'email'``. + + :returns: List of module objects. + """ + if module_type not in self.modules: + self.modules[module_type] = [] + for provider in self.app.providers.values(): + name = f'{module_type}_modules' + if hasattr(provider, name): + modules = getattr(provider, name) + if modules: + if isinstance(modules, str): + modules = [modules] + for modpath in modules: + module = importlib.import_module(modpath) + self.modules[module_type].append(module) + return self.modules[module_type] diff --git a/src/wuttjamaican/batch.py b/src/wuttjamaican/batch.py index 653df8b..d87d70e 100644 --- a/src/wuttjamaican/batch.py +++ b/src/wuttjamaican/batch.py @@ -348,7 +348,7 @@ class BatchHandler(GenericHandler): * :attr:`~wuttjamaican.db.model.batch.BatchMixin.status_text` """ - def why_not_execute(self, batch, user=None, **kwargs): + def why_not_execute(self, batch, user=None, **kwargs): # pylint: disable=unused-argument """ Returns text indicating the reason (if any) that a given batch should *not* be executed. @@ -385,6 +385,7 @@ class BatchHandler(GenericHandler): raise a ``RuntimeError`` if text was returned. This is done out of safety, to avoid relying on the user interface. """ + return None def describe_execution(self, batch, user=None, **kwargs): """ @@ -467,16 +468,16 @@ class BatchHandler(GenericHandler): if batch.executed: raise ValueError(f"batch has already been executed: {batch}") - reason = self.why_not_execute(batch, user=user, **kwargs) + reason = self.why_not_execute(batch, user=user, **kwargs) # pylint: disable=assignment-from-none if reason: raise RuntimeError(f"batch execution not allowed: {reason}") - result = self.execute(batch, user=user, progress=progress, **kwargs) + result = self.execute(batch, user=user, progress=progress, **kwargs) # pylint: disable=assignment-from-none batch.executed = datetime.datetime.now() batch.executed_by = user return result - def execute(self, batch, user=None, progress=None, **kwargs): + def execute(self, batch, user=None, progress=None, **kwargs): # pylint: disable=unused-argument """ Execute the given batch. @@ -502,6 +503,7 @@ class BatchHandler(GenericHandler): whatever it likes, in which case that will be also returned to the caller from :meth:`do_execute()`. """ + return None def do_delete(self, batch, user, dry_run=False, progress=None, **kwargs): # pylint: disable=unused-argument """ diff --git a/src/wuttjamaican/cli/problems.py b/src/wuttjamaican/cli/problems.py index 4135065..0638fd1 100644 --- a/src/wuttjamaican/cli/problems.py +++ b/src/wuttjamaican/cli/problems.py @@ -44,7 +44,7 @@ def problems( help="System for which to perform checks; can be specified more " "than once. If not specified, all systems are assumed.")] = None, - problems: Annotated[ + problems: Annotated[ # pylint: disable=redefined-outer-name List[str], typer.Option('--problem', '-p', help="Identify a particular problem check; can be specified " @@ -53,7 +53,8 @@ def problems( list_checks: Annotated[ bool, typer.Option('--list', '-l', - help="List available problem checks; optionally filtered per --system and --problem")] = False, + help="List available problem checks; optionally filtered " + "per --system and --problem")] = False, ): """ Find and report on problems with the data or system. diff --git a/src/wuttjamaican/db/handler.py b/src/wuttjamaican/db/handler.py index 849f954..5d8d8af 100644 --- a/src/wuttjamaican/db/handler.py +++ b/src/wuttjamaican/db/handler.py @@ -2,7 +2,7 @@ ################################################################################ # # WuttJamaican -- Base package for Wutta Framework -# Copyright © 2024 Lance Edgar +# Copyright © 2024-2025 Lance Edgar # # This file is part of Wutta Framework. # @@ -34,7 +34,7 @@ class DatabaseHandler(GenericHandler): Base class and default implementation for the :term:`db handler`. """ - def get_dialect(self, bind): + def get_dialect(self, bind): # pylint: disable=empty-docstring """ """ return bind.url.get_dialect().name diff --git a/src/wuttjamaican/db/model/auth.py b/src/wuttjamaican/db/model/auth.py index b776d04..8ff9fc5 100644 --- a/src/wuttjamaican/db/model/auth.py +++ b/src/wuttjamaican/db/model/auth.py @@ -45,10 +45,11 @@ import sqlalchemy as sa from sqlalchemy import orm from sqlalchemy.ext.associationproxy import association_proxy -from . import Base, uuid_column, uuid_fk_column +from wuttjamaican.db.util import uuid_column, uuid_fk_column +from wuttjamaican.db.model.base import Base -class Role(Base): +class Role(Base): # pylint: disable=too-few-public-methods """ Represents an authentication role within the system; used for permission management. @@ -120,7 +121,7 @@ class Role(Base): return self.name or "" -class Permission(Base): +class Permission(Base): # pylint: disable=too-few-public-methods """ Represents a permission granted to a role. """ @@ -145,7 +146,7 @@ class Permission(Base): return self.permission or "" -class User(Base): +class User(Base): # pylint: disable=too-few-public-methods """ Represents a user of the system. @@ -231,7 +232,7 @@ class User(Base): return self.username or "" -class UserRole(Base): +class UserRole(Base): # pylint: disable=too-few-public-methods """ Represents the association between a user and a role; i.e. the user "belongs" or "is assigned" to the role. @@ -260,7 +261,7 @@ class UserRole(Base): """) -class UserAPIToken(Base): +class UserAPIToken(Base): # pylint: disable=too-few-public-methods """ User authentication token for use with HTTP API """ @@ -285,9 +286,13 @@ class UserAPIToken(Base): Raw token string, to be used by API clients. """) - created = sa.Column(sa.DateTime(timezone=True), nullable=False, default=datetime.datetime.now, doc=""" - Date/time when the token was created. - """) + created = sa.Column( + sa.DateTime(timezone=True), + nullable=False, + default=datetime.datetime.now, + doc=""" + Date/time when the token was created. + """) def __str__(self): return self.description or "" diff --git a/src/wuttjamaican/db/model/base.py b/src/wuttjamaican/db/model/base.py index 9dc49ae..b2858ff 100644 --- a/src/wuttjamaican/db/model/base.py +++ b/src/wuttjamaican/db/model/base.py @@ -39,7 +39,7 @@ from sqlalchemy.ext.associationproxy import association_proxy from wuttjamaican.db.util import naming_convention, ModelBase, uuid_column -class WuttaModelBase(ModelBase): +class WuttaModelBase(ModelBase): # pylint: disable=too-few-public-methods """ Base class for data models, from which :class:`Base` inherits. @@ -123,7 +123,7 @@ metadata = sa.MetaData(naming_convention=naming_convention) Base = orm.declarative_base(metadata=metadata, cls=WuttaModelBase) -class Setting(Base): +class Setting(Base): # pylint: disable=too-few-public-methods """ Represents a :term:`config setting`. """ diff --git a/src/wuttjamaican/db/model/batch.py b/src/wuttjamaican/db/model/batch.py index e253e84..0a24c8d 100644 --- a/src/wuttjamaican/db/model/batch.py +++ b/src/wuttjamaican/db/model/batch.py @@ -31,7 +31,8 @@ from sqlalchemy import orm from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.orderinglist import ordering_list -from wuttjamaican.db.model import uuid_column, User +from wuttjamaican.db.model.base import uuid_column +from wuttjamaican.db.model.auth import User from wuttjamaican.db.util import UUID @@ -185,7 +186,7 @@ class BatchMixin: """ @declared_attr - def __table_args__(cls): + def __table_args__(cls): # pylint: disable=no-self-argument return cls.__default_table_args__() @classmethod @@ -200,7 +201,8 @@ class BatchMixin: ) @declared_attr - def batch_type(cls): + def batch_type(cls): # pylint: disable=empty-docstring,no-self-argument + """ """ return cls.__tablename__ uuid = uuid_column() @@ -226,7 +228,8 @@ class BatchMixin: created_by_uuid = sa.Column(UUID(), nullable=False) @declared_attr - def created_by(cls): + def created_by(cls): # pylint: disable=empty-docstring,no-self-argument + """ """ return orm.relationship( User, primaryjoin=lambda: User.uuid == cls.created_by_uuid, @@ -238,7 +241,8 @@ class BatchMixin: executed_by_uuid = sa.Column(UUID(), nullable=True) @declared_attr - def executed_by(cls): + def executed_by(cls): # pylint: disable=empty-docstring,no-self-argument + """ """ return orm.relationship( User, primaryjoin=lambda: User.uuid == cls.executed_by_uuid, @@ -266,7 +270,7 @@ class BatchMixin: return None -class BatchRowMixin: +class BatchRowMixin: # pylint: disable=too-few-public-methods """ Mixin base class for :term:`data models ` which represent a :term:`batch row`. @@ -377,7 +381,7 @@ class BatchRowMixin: uuid = uuid_column() @declared_attr - def __table_args__(cls): + def __table_args__(cls): # pylint: disable=no-self-argument return cls.__default_table_args__() @classmethod @@ -394,7 +398,8 @@ class BatchRowMixin: batch_uuid = sa.Column(UUID(), nullable=False) @declared_attr - def batch(cls): + def batch(cls): # pylint: disable=empty-docstring,no-self-argument + """ """ batch_class = cls.__batch_class__ row_class = cls batch_class.__row_class__ = row_class diff --git a/src/wuttjamaican/db/model/upgrades.py b/src/wuttjamaican/db/model/upgrades.py index 8e94abd..010e26e 100644 --- a/src/wuttjamaican/db/model/upgrades.py +++ b/src/wuttjamaican/db/model/upgrades.py @@ -29,13 +29,13 @@ import datetime import sqlalchemy as sa from sqlalchemy import orm -from . import Base, uuid_column, uuid_fk_column from wuttjamaican.enum import UpgradeStatus -from wuttjamaican.db.util import UUID +from wuttjamaican.db.util import UUID, uuid_column, uuid_fk_column from wuttjamaican.util import make_true_uuid +from wuttjamaican.db.model.base import Base -class Upgrade(Base): +class Upgrade(Base): # pylint: disable=too-few-public-methods """ Represents an app upgrade. """ diff --git a/src/wuttjamaican/db/sess.py b/src/wuttjamaican/db/sess.py index 7fd7a62..9033832 100644 --- a/src/wuttjamaican/db/sess.py +++ b/src/wuttjamaican/db/sess.py @@ -2,7 +2,7 @@ ################################################################################ # # WuttJamaican -- Base package for Wutta Framework -# Copyright © 2023 Lance Edgar +# Copyright © 2023-2025 Lance Edgar # # This file is part of Wutta Framework. # @@ -38,7 +38,7 @@ from sqlalchemy import orm Session = orm.sessionmaker() -class short_session: +class short_session: # pylint: disable=invalid-name """ Context manager for a short-lived database session. diff --git a/src/wuttjamaican/db/util.py b/src/wuttjamaican/db/util.py index 70be40a..24e508f 100644 --- a/src/wuttjamaican/db/util.py +++ b/src/wuttjamaican/db/util.py @@ -26,8 +26,8 @@ Database Utilities import uuid as _uuid from importlib.metadata import version -from packaging.version import Version +from packaging.version import Version import sqlalchemy as sa from sqlalchemy import orm from sqlalchemy.dialects.postgresql import UUID as PGUUID @@ -51,7 +51,7 @@ if Version(version('SQLAlchemy')) < Version('2'): # pragma: no cover SA2 = False -class ModelBase: +class ModelBase: # pylint: disable=empty-docstring """ """ def __iter__(self): @@ -69,7 +69,7 @@ class ModelBase: raise KeyError(f"model instance has no attr with key: {key}") -class UUID(sa.types.TypeDecorator): +class UUID(sa.types.TypeDecorator): # pylint: disable=abstract-method,too-many-ancestors """ Platform-independent UUID type. @@ -82,36 +82,35 @@ class UUID(sa.types.TypeDecorator): """ impl = sa.CHAR cache_ok = True - """ """ # nb. suppress sphinx autodoc for cache_ok + """ """ # nb. suppress sphinx autodoc for cache_ok - def load_dialect_impl(self, dialect): + def load_dialect_impl(self, dialect): # pylint: disable=empty-docstring """ """ if dialect.name == "postgresql": return dialect.type_descriptor(PGUUID()) - else: - return dialect.type_descriptor(sa.CHAR(32)) + return dialect.type_descriptor(sa.CHAR(32)) - def process_bind_param(self, value, dialect): + def process_bind_param(self, value, dialect): # pylint: disable=empty-docstring """ """ if value is None: return value - elif dialect.name == "postgresql": + + if dialect.name == "postgresql": return str(value) - else: - if not isinstance(value, _uuid.UUID): - return "%.32x" % _uuid.UUID(value).int - else: - # hexstring - return "%.32x" % value.int - def process_result_value(self, value, dialect): # pylint: disable=unused-argument + if not isinstance(value, _uuid.UUID): + value = _uuid.UUID(value) + + # hexstring + return f"{value.int:032x}" + + def process_result_value(self, value, dialect): # pylint: disable=unused-argument,empty-docstring """ """ if value is None: return value - else: - if not isinstance(value, _uuid.UUID): - value = _uuid.UUID(value) - return value + if not isinstance(value, _uuid.UUID): + value = _uuid.UUID(value) + return value def uuid_column(*args, **kwargs): @@ -150,8 +149,8 @@ def make_topo_sortkey(model): containing model classes. """ metadata = model.Base.metadata - tables = dict([(table.name, i) - for i, table in enumerate(metadata.sorted_tables, 1)]) + tables = {table.name: i + for i, table in enumerate(metadata.sorted_tables, 1)} def sortkey(name): cls = getattr(model, name) diff --git a/src/wuttjamaican/email.py b/src/wuttjamaican/email.py index 14c5c87..77e702b 100644 --- a/src/wuttjamaican/email.py +++ b/src/wuttjamaican/email.py @@ -24,7 +24,6 @@ Email Handler """ -import importlib import logging import smtplib from email.mime.multipart import MIMEMultipart @@ -41,7 +40,7 @@ from wuttjamaican.util import resource_path log = logging.getLogger(__name__) -class EmailSetting: +class EmailSetting: # pylint: disable=too-few-public-methods """ Base class for all :term:`email settings `. @@ -188,15 +187,15 @@ class Message: self.key = key self.sender = sender self.subject = subject - self.set_recips('to', to) - self.set_recips('cc', cc) - self.set_recips('bcc', bcc) + self.to = self.get_recips(to) + self.cc = self.get_recips(cc) + self.bcc = self.get_recips(bcc) self.replyto = replyto self.txt_body = txt_body self.html_body = html_body self.attachments = attachments or [] - def set_recips(self, name, value): + def get_recips(self, value): # pylint: disable=empty-docstring """ """ if value: if isinstance(value, str): @@ -205,7 +204,7 @@ class Message: raise ValueError("must specify a string, tuple or list value") else: value = [] - setattr(self, name, list(value)) + return list(value) def as_string(self): """ @@ -305,20 +304,12 @@ class EmailHandler(GenericHandler): This will discover all email modules exposed by the :term:`app`, and/or its :term:`providers `. - """ - if not hasattr(self, '_email_modules'): - self._email_modules = [] - for provider in self.app.providers.values(): - if hasattr(provider, 'email_modules'): - modules = provider.email_modules - if modules: - if isinstance(modules, str): - modules = [modules] - for module in modules: - module = importlib.import_module(module) - self._email_modules.append(module) - return self._email_modules + Calls + :meth:`~wuttjamaican.app.GenericHandler.get_provider_modules()` + under the hood, for ``email`` module type. + """ + return self.get_provider_modules('email') def get_email_settings(self): """ @@ -549,7 +540,7 @@ class EmailHandler(GenericHandler): """ return self.get_auto_recips(key, 'bcc') - def get_auto_recips(self, key, typ): + def get_auto_recips(self, key, typ): # pylint: disable=empty-docstring """ """ typ = typ.lower() if typ not in ('to', 'cc', 'bcc'): @@ -589,16 +580,15 @@ class EmailHandler(GenericHandler): return template.render(**context) return None - def get_auto_body_template(self, key, mode): + def get_auto_body_template(self, key, mode): # pylint: disable=empty-docstring """ """ mode = mode.lower() - if mode not in ('txt', 'html'): - raise ValueError("requested mode not supported") - if mode == 'txt': templates = self.txt_templates elif mode == 'html': templates = self.html_templates + else: + raise ValueError("requested mode not supported") try: return templates.get_template(f'{key}.{mode}.mako') diff --git a/src/wuttjamaican/install.py b/src/wuttjamaican/install.py index 28035f1..61be2d3 100644 --- a/src/wuttjamaican/install.py +++ b/src/wuttjamaican/install.py @@ -141,9 +141,10 @@ class InstallHandler(GenericHandler): This is normally called by :meth:`run()`. """ - self.rprint("\n\t[blue]Welcome to {}![/blue]".format(self.app.get_title())) + self.rprint(f"\n\t[blue]Welcome to {self.app.get_title()}![/blue]") self.rprint("\n\tThis tool will install and configure the app.") - self.rprint("\n\t[italic]NB. You should already have created the database in PostgreSQL or MySQL.[/italic]") + self.rprint("\n\t[italic]NB. You should already have created " + "the database in PostgreSQL or MySQL.[/italic]") # shall we continue? if not self.prompt_bool("continue?", True): @@ -221,14 +222,15 @@ class InstallHandler(GenericHandler): error = self.test_db_connection(dbinfo['dburl']) if error: self.rprint("[bold red]cannot connect![/bold red] ..error was:") - self.rprint("\n{}".format(error)) + self.rprint(f"\n{error}") self.rprint("\n\t[bold yellow]aborting mission[/bold yellow]\n") sys.exit(1) self.rprint("[bold green]good[/bold green]") return dbinfo - def make_db_url(self, dbtype, dbhost, dbport, dbname, dbuser, dbpass): + def make_db_url(self, dbtype, dbhost, dbport, dbname, dbuser, dbpass): # pylint: disable=empty-docstring + """ """ from sqlalchemy.engine import URL if dbtype == 'mysql': @@ -243,7 +245,8 @@ class InstallHandler(GenericHandler): port=dbport, database=dbname) - def test_db_connection(self, url): + def test_db_connection(self, url): # pylint: disable=empty-docstring + """ """ import sqlalchemy as sa engine = sa.create_engine(url) @@ -252,7 +255,7 @@ class InstallHandler(GenericHandler): # just need to test interaction and this is a neutral way try: sa.inspect(engine).has_table('whatever') - except Exception as error: + except Exception as error: # pylint: disable=broad-exception-caught return str(error) return None @@ -446,8 +449,8 @@ class InstallHandler(GenericHandler): 'upgrade', 'heads'] subprocess.check_call(cmd) - self.rprint("\n\tdb schema installed to: [bold green]{}[/bold green]".format( - obfuscate_url_pw(db_url))) + self.rprint("\n\tdb schema installed to: " + f"[bold green]{obfuscate_url_pw(db_url)}[/bold green]") return True def show_goodbye(self): @@ -469,7 +472,8 @@ class InstallHandler(GenericHandler): # console utility functions ############################## - def require_prompt_toolkit(self, answer=None): + def require_prompt_toolkit(self, answer=None): # pylint: disable=empty-docstring + """ """ try: import prompt_toolkit # pylint: disable=unused-import except ImportError: @@ -491,7 +495,8 @@ class InstallHandler(GenericHandler): """ rich.print(*args, **kwargs) - def get_prompt_style(self): + def get_prompt_style(self): # pylint: disable=empty-docstring + """ """ from prompt_toolkit.styles import Style # message formatting styles @@ -540,9 +545,9 @@ class InstallHandler(GenericHandler): ] if default is not None: if is_bool: - message.append(('', ' [{}]: '.format('Y' if default else 'N'))) + message.append(('', f' [{"Y" if default else "N"}]: ')) else: - message.append(('', ' [{}]: '.format(default))) + message.append(('', f' [{default}]: ')) else: message.append(('', ': ')) @@ -558,9 +563,9 @@ class InstallHandler(GenericHandler): if is_bool: if text == '': return default - elif text.upper() == 'Y': + if text.upper() == 'Y': return True - elif text.upper() == 'N': + if text.upper() == 'N': return False self.rprint("\n\t[bold yellow]ambiguous, please try again[/bold yellow]\n") return self.prompt_generic(info, default, is_bool=True) diff --git a/src/wuttjamaican/people.py b/src/wuttjamaican/people.py index 3f51240..9513b1b 100644 --- a/src/wuttjamaican/people.py +++ b/src/wuttjamaican/people.py @@ -84,7 +84,7 @@ class PeopleHandler(GenericHandler): person = obj return person - elif isinstance(obj, model.User): + if isinstance(obj, model.User): user = obj if user.person: return user.person diff --git a/src/wuttjamaican/problems.py b/src/wuttjamaican/problems.py index b401ed9..3394c23 100644 --- a/src/wuttjamaican/problems.py +++ b/src/wuttjamaican/problems.py @@ -200,7 +200,7 @@ class ProblemHandler(GenericHandler): :returns: List of system keys. """ checks = self.get_all_problem_checks() - return sorted(set([check.system_key for check in checks])) + return sorted({check.system_key for check in checks}) def get_system_title(self, system_key): """ diff --git a/src/wuttjamaican/progress.py b/src/wuttjamaican/progress.py index 712675c..00d718a 100644 --- a/src/wuttjamaican/progress.py +++ b/src/wuttjamaican/progress.py @@ -2,7 +2,7 @@ ################################################################################ # # WuttJamaican -- Base package for Wutta Framework -# Copyright © 2023-2024 Lance Edgar +# Copyright © 2023-2025 Lance Edgar # # This file is part of Wutta Framework. # @@ -97,17 +97,17 @@ class ConsoleProgress(ProgressBase): def __init__(self, *args, **kwargs): super().__init__(*args) - + self.stderr = kwargs.get('stderr', sys.stderr) self.stderr.write(f"\n{self.message}...\n") - self.bar = Bar(message='', max=self.maximum, width=70, + self.bar = Bar(message='', max=self.maximum, width=70, # pylint: disable=disallowed-name suffix='%(index)d/%(max)d %(percent)d%% ETA %(eta)ds') - def update(self, value): + def update(self, value): # pylint: disable=empty-docstring """ """ self.bar.next() - def finish(self): + def finish(self): # pylint: disable=empty-docstring """ """ self.bar.finish() diff --git a/src/wuttjamaican/reports.py b/src/wuttjamaican/reports.py index 7a55a19..0f84f87 100644 --- a/src/wuttjamaican/reports.py +++ b/src/wuttjamaican/reports.py @@ -24,8 +24,6 @@ Report Utilities """ -import importlib - from wuttjamaican.app import GenericHandler @@ -143,20 +141,12 @@ class ReportHandler(GenericHandler): This will discover all report modules exposed by the :term:`app`, and/or its :term:`providers `. - """ - if not hasattr(self, '_report_modules'): - self._report_modules = [] - for provider in self.app.providers.values(): - if hasattr(provider, 'report_modules'): - modules = provider.report_modules - if modules: - if isinstance(modules, str): - modules = [modules] - for module in modules: - module = importlib.import_module(module) - self._report_modules.append(module) - return self._report_modules + Calls + :meth:`~wuttjamaican.app.GenericHandler.get_provider_modules()` + under the hood, for ``report`` module type. + """ + return self.get_provider_modules('report') def get_reports(self): """ diff --git a/src/wuttjamaican/testing.py b/src/wuttjamaican/testing.py index f566d91..dcdfe2e 100644 --- a/src/wuttjamaican/testing.py +++ b/src/wuttjamaican/testing.py @@ -53,7 +53,7 @@ class FileTestCase(TestCase): class. """ - def setUp(self): + def setUp(self): # pylint: disable=empty-docstring """ """ self.setup_files() @@ -63,14 +63,14 @@ class FileTestCase(TestCase): """ self.tempdir = tempfile.mkdtemp() - def setup_file_config(self): # pragma: no cover + def setup_file_config(self): # pragma: no cover; pylint: disable=empty-docstring """ """ warnings.warn("FileTestCase.setup_file_config() is deprecated; " "please use setup_files() instead", DeprecationWarning, stacklevel=2) self.setup_files() - def tearDown(self): + def tearDown(self): # pylint: disable=empty-docstring """ """ self.teardown_files() @@ -80,7 +80,7 @@ class FileTestCase(TestCase): """ shutil.rmtree(self.tempdir) - def teardown_file_config(self): # pragma: no cover + def teardown_file_config(self): # pragma: no cover; pylint: disable=empty-docstring """ """ warnings.warn("FileTestCase.teardown_file_config() is deprecated; " "please use teardown_files() instead", @@ -99,7 +99,7 @@ class FileTestCase(TestCase): f.write(content) return path - def mkdir(self, dirname): # pylint: disable=unused-argument + def mkdir(self, dirname): # pylint: disable=unused-argument,empty-docstring """ """ warnings.warn("FileTestCase.mkdir() is deprecated; " "please use FileTestCase.mkdtemp() instead", @@ -143,7 +143,7 @@ class ConfigTestCase(FileTestCase): methods for this class. """ - def setUp(self): + def setUp(self): # pylint: disable=empty-docstring """ """ self.setup_config() @@ -155,7 +155,7 @@ class ConfigTestCase(FileTestCase): self.config = self.make_config() self.app = self.config.get_app() - def tearDown(self): + def tearDown(self): # pylint: disable=empty-docstring """ """ self.teardown_config() @@ -165,7 +165,7 @@ class ConfigTestCase(FileTestCase): """ self.teardown_files() - def make_config(self, **kwargs): + def make_config(self, **kwargs): # pylint: disable=empty-docstring """ """ return WuttaConfig(**kwargs) @@ -203,7 +203,7 @@ class DataTestCase(FileTestCase): teardown methods, as this class handles that automatically. """ - def setUp(self): + def setUp(self): # pylint: disable=empty-docstring """ """ self.setup_db() @@ -222,7 +222,7 @@ class DataTestCase(FileTestCase): model.Base.metadata.create_all(bind=self.config.appdb_engine) self.session = self.app.make_session() - def tearDown(self): + def tearDown(self): # pylint: disable=empty-docstring """ """ self.teardown_db() @@ -232,6 +232,6 @@ class DataTestCase(FileTestCase): """ self.teardown_files() - def make_config(self, **kwargs): + def make_config(self, **kwargs): # pylint: disable=empty-docstring """ """ return WuttaConfig(**kwargs) diff --git a/src/wuttjamaican/util.py b/src/wuttjamaican/util.py index 843a87c..6897fe1 100644 --- a/src/wuttjamaican/util.py +++ b/src/wuttjamaican/util.py @@ -115,7 +115,7 @@ def load_entry_points(group, ignore_errors=False): for entry_point in eps: try: ep = entry_point.load() - except: + except Exception: # pylint: disable=broad-exception-caught if not ignore_errors: raise log.warning("failed to load entry point: %s", entry_point, diff --git a/tests/test_app.py b/tests/test_app.py index e05334d..e75b527 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -761,6 +761,9 @@ class TestGenericHandler(ConfigTestCase): kw.setdefault('appname', 'wuttatest') return super().make_config(**kw) + def make_handler(self, **kwargs): + return mod.GenericHandler(self.config, **kwargs) + def test_constructor(self): handler = mod.GenericHandler(self.config) self.assertIs(handler.config, self.config) @@ -769,3 +772,30 @@ class TestGenericHandler(ConfigTestCase): def test_get_spec(self): self.assertEqual(mod.GenericHandler.get_spec(), 'wuttjamaican.app:GenericHandler') + + def test_get_provider_modules(self): + + # no providers, no email modules + with patch.object(self.app, 'providers', new={}): + handler = self.make_handler() + self.assertEqual(handler.get_provider_modules('email'), []) + + # provider may specify modules as list + providers = { + 'wuttatest': MagicMock(email_modules=['wuttjamaican.app']), + } + with patch.object(self.app, 'providers', new=providers): + handler = self.make_handler() + modules = handler.get_provider_modules('email') + self.assertEqual(len(modules), 1) + self.assertIs(modules[0], mod) + + # provider may specify modules as string + providers = { + 'wuttatest': MagicMock(email_modules='wuttjamaican.app'), + } + with patch.object(self.app, 'providers', new=providers): + handler = self.make_handler() + modules = handler.get_provider_modules('email') + self.assertEqual(len(modules), 1) + self.assertIs(modules[0], mod) diff --git a/tests/test_email.py b/tests/test_email.py index 8cf1623..6e1a72d 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -30,28 +30,27 @@ class TestMessage(FileTestCase): def make_message(self, **kwargs): return mod.Message(**kwargs) - def test_set_recips(self): + def test_get_recips(self): msg = self.make_message() - self.assertEqual(msg.to, []) # set as list - msg.set_recips('to', ['sally@example.com']) - self.assertEqual(msg.to, ['sally@example.com']) + recips = msg.get_recips(['sally@example.com']) + self.assertEqual(recips, ['sally@example.com']) # set as tuple - msg.set_recips('to', ('barney@example.com',)) - self.assertEqual(msg.to, ['barney@example.com']) + recips = msg.get_recips(('barney@example.com',)) + self.assertEqual(recips, ['barney@example.com']) # set as string - msg.set_recips('to', 'wilma@example.com') - self.assertEqual(msg.to, ['wilma@example.com']) + recips = msg.get_recips('wilma@example.com') + self.assertEqual(recips, ['wilma@example.com']) # set as null - msg.set_recips('to', None) - self.assertEqual(msg.to, []) + recips = msg.get_recips(None) + self.assertEqual(recips, []) # otherwise error - self.assertRaises(ValueError, msg.set_recips, 'to', {'foo': 'foo@example.com'}) + self.assertRaises(ValueError, msg.get_recips, {'foo': 'foo@example.com'}) def test_as_string(self): diff --git a/tox.ini b/tox.ini index 2d8728a..4d28869 100644 --- a/tox.ini +++ b/tox.ini @@ -12,8 +12,9 @@ extras = tests [testenv:pylint] basepython = python3.11 -extras = +extras = db deps = pylint + prompt_toolkit commands = pylint wuttjamaican [testenv:coverage]