refactor: implement generic optional import mechanisms

this project is actively using optional imports, which leads to
duplicate handling here and there. This commit implements logic in
special method to reduce complexity and simplify tests paths
This commit is contained in:
2026-07-22 16:33:59 +03:00
parent bd26e8da68
commit f37c5bd2a2
16 changed files with 83 additions and 83 deletions
+11 -15
View File
@@ -17,18 +17,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
try:
import aiohttp_security
except ImportError:
aiohttp_security = None # type: ignore[assignment]
try:
import aiohttp_session
except ImportError:
aiohttp_session = None # type: ignore[assignment]
from typing import Any
from ahriman.core.module_loader import optional_module
__all__ = [
"authorized_userid",
@@ -39,6 +31,10 @@ __all__ = [
]
aiohttp_security = optional_module("aiohttp_security")
aiohttp_session = optional_module("aiohttp_session")
async def authorized_userid(*args: Any, **kwargs: Any) -> Any:
"""
handle aiohttp security methods
@@ -50,7 +46,7 @@ async def authorized_userid(*args: Any, **kwargs: Any) -> Any:
Returns:
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
"""
if aiohttp_security is not None:
if aiohttp_security:
return await aiohttp_security.authorized_userid(*args, **kwargs) # pylint: disable=no-value-for-parameter
return None
@@ -66,7 +62,7 @@ async def check_authorized(*args: Any, **kwargs: Any) -> Any:
Returns:
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
"""
if aiohttp_security is not None:
if aiohttp_security:
return await aiohttp_security.check_authorized(*args, **kwargs) # pylint: disable=no-value-for-parameter
return None
@@ -82,7 +78,7 @@ async def forget(*args: Any, **kwargs: Any) -> Any:
Returns:
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
"""
if aiohttp_security is not None:
if aiohttp_security:
return await aiohttp_security.forget(*args, **kwargs) # pylint: disable=no-value-for-parameter
return None
@@ -98,7 +94,7 @@ async def get_session(*args: Any, **kwargs: Any) -> Any:
Returns:
Any: empty dictionary in case if no aiohttp_session module found and function call otherwise
"""
if aiohttp_session is not None:
if aiohttp_session:
return await aiohttp_session.get_session(*args, **kwargs)
return {}
@@ -114,6 +110,6 @@ async def remember(*args: Any, **kwargs: Any) -> Any:
Returns:
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
"""
if aiohttp_security is not None:
if aiohttp_security:
return await aiohttp_security.remember(*args, **kwargs) # pylint: disable=no-value-for-parameter
return None
@@ -17,15 +17,20 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
try:
import aiohttp_apispec # type: ignore[import-untyped]
from ahriman.core.module_loader import optional_module
from marshmallow import Schema, fields
except ImportError:
aiohttp_apispec = optional_module("aiohttp_apispec")
marshmallow = optional_module("marshmallow")
if aiohttp_apispec and marshmallow:
Schema = marshmallow.Schema
fields = marshmallow.fields
else:
from unittest.mock import Mock
Schema = Mock # type: ignore[misc]
aiohttp_apispec = None
fields = Mock()
@@ -115,7 +115,7 @@ def apidocs(*,
authorization_required = permission != UserAccess.Unauthorized
def wrapper(handler: Callable[..., Any]) -> Callable[..., Any]:
if aiohttp_apispec is None:
if not aiohttp_apispec:
return handler # apispec is disabled
responses = _response_schema(
+3 -6
View File
@@ -100,20 +100,17 @@ def _servers(application: Application) -> list[dict[str, Any]]:
}]
def setup_apispec(application: Application) -> Any:
def setup_apispec(application: Application) -> None:
"""
setup swagger api specification
Args:
application(Application): web application instance
Returns:
Any: created specification instance if module is available
"""
if aiohttp_apispec is None:
return None
return
return aiohttp_apispec.setup_aiohttp_apispec(
aiohttp_apispec.setup_aiohttp_apispec(
application,
url="/api-docs/swagger.json",
openapi_version="3.0.2",
@@ -17,14 +17,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
try:
import aiohttp_openmetrics
except ImportError:
aiohttp_openmetrics = None # type: ignore[assignment]
from aiohttp.typedefs import Middleware
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse, middleware
from ahriman.core.module_loader import optional_module
from ahriman.web.middlewares import HandlerType
@@ -34,6 +30,9 @@ __all__ = [
]
aiohttp_openmetrics = optional_module("aiohttp_openmetrics")
async def metrics(request: Request) -> Response:
"""
handler for returning metrics
@@ -47,7 +46,7 @@ async def metrics(request: Request) -> Response:
Raises:
HTTPNotFound: endpoint is disabled
"""
if aiohttp_openmetrics is None:
if not aiohttp_openmetrics:
raise HTTPNotFound
return await aiohttp_openmetrics.metrics(request)
@@ -59,7 +58,7 @@ def metrics_handler() -> Middleware:
Returns:
Middleware: middleware function to handle server metrics
"""
if aiohttp_openmetrics is not None:
if aiohttp_openmetrics:
return aiohttp_openmetrics.metrics_middleware
@middleware
+1 -1
View File
@@ -60,7 +60,7 @@ async def server_info(view: BaseView) -> dict[str, Any]:
"username": await authorized_userid(view.request),
},
"autorefresh_intervals": sorted(autorefresh_intervals, key=comparator),
"docs_enabled": aiohttp_apispec is not None,
"docs_enabled": bool(aiohttp_apispec),
"index_url": view.configuration.get("web", "index_url", fallback=None),
"repositories": [
{
@@ -50,7 +50,7 @@ class DocsView(BaseView):
list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set
and returns empty list otherwise
"""
if aiohttp_apispec is None:
if not aiohttp_apispec:
return []
return cls.ROUTES
@@ -51,7 +51,7 @@ class SwaggerView(BaseView):
list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set
and returns empty list otherwise
"""
if aiohttp_apispec is None:
if not aiohttp_apispec:
return []
return cls.ROUTES
@@ -22,6 +22,7 @@ from secrets import token_urlsafe
from typing import ClassVar
from ahriman.core.auth.helpers import get_session, remember
from ahriman.core.module_loader import optional_module
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import LoginSchema, OAuth2Schema
@@ -62,14 +63,12 @@ class LoginView(BaseView):
HTTPMethodNotAllowed: in case if method is used, but OAuth is disabled
HTTPUnauthorized: if case of authorization error
"""
try:
from ahriman.core.auth.oauth import OAuth
except ImportError:
# no aioauth library found
oauth = optional_module("ahriman.core.auth.oauth")
if not oauth:
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
oauth_provider = self.validator
if not isinstance(oauth_provider, OAuth):
if not isinstance(oauth_provider, oauth.OAuth):
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
session = await get_session(self.request)
@@ -48,7 +48,8 @@ def test_setup_apispec(application: Application, mocker: MockerFixture) -> None:
must set api specification
"""
apispec_mock = mocker.patch("aiohttp_apispec.setup_aiohttp_apispec")
assert setup_apispec(application)
setup_apispec(application)
apispec_mock.assert_called_once_with(
application,
url="/api-docs/swagger.json",
@@ -37,7 +37,7 @@ async def test_get_import_error(client_with_auth: TestClient, mocker: MockerFixt
"""
must return 405 on import error
"""
pytest.helpers.import_error("ahriman.core.auth.oauth", ["OAuth"], mocker)
mocker.patch("ahriman.web.views.v1.user.login.optional_module", return_value=None)
response = await client_with_auth.get("/api/v1/login")
assert response.status == 405