fix(teams): do not call App() when the SDK was never bound

find_spec("microsoft_teams") can be true from sibling namespace packages
while App is still None, so a failed lazy-install crashed connect with
'NoneType' object is not callable instead of a missing-SDK error.
Probe microsoft_teams.apps via the parent first — a dotted find_spec
raises ModuleNotFoundError on 3.11 when the namespace is absent.
This commit is contained in:
xxxigm
2026-08-24 20:01:25 +07:00
committed by kshitij
parent 6ce7ab8bfb
commit f84f94b494
2 changed files with 39 additions and 9 deletions

View File

@@ -58,11 +58,29 @@ except ImportError:
import importlib.util
import sys as _sys
try:
TEAMS_SDK_AVAILABLE = importlib.util.find_spec("microsoft_teams") is not None
except ValueError:
# Test stubs may inject a module without ``__spec__``.
TEAMS_SDK_AVAILABLE = "microsoft_teams" in _sys.modules
def _probe_teams_sdk_available() -> bool:
"""True when ``microsoft_teams.apps`` is on sys.path, without importing it.
Sibling packages (microsoft-teams-api / common / cards) also live under
the ``microsoft_teams`` namespace, so ``find_spec("microsoft_teams")``
alone can be True while ``App`` is still unbound — connect() then
called None and logged ``'NoneType' object is not callable``.
Probe the parent first: ``find_spec("microsoft_teams.apps")`` raises
``ModuleNotFoundError`` on 3.11+ when the parent namespace is absent,
which crashed plugin import and unregistered the Teams platform.
"""
try:
if importlib.util.find_spec("microsoft_teams") is None:
return False
return importlib.util.find_spec("microsoft_teams.apps") is not None
except (ValueError, ModuleNotFoundError, ImportError):
# Test stubs may inject a module without ``__spec__``.
return "microsoft_teams.apps" in _sys.modules
TEAMS_SDK_AVAILABLE = _probe_teams_sdk_available()
ClientOptions = None # type: ignore[assignment,misc]
App = None # type: ignore[assignment,misc]
ActivityContext = None # type: ignore[assignment,misc]
@@ -779,8 +797,13 @@ class TeamsAdapter(BasePlatformAdapter):
# Defensive re-check: create_adapter() already ran the installer
# (ensure_deps_fn) if deps were missing, but connect() can also be
# reached via reconnect paths — re-run to bind SDK globals.
#
# Gate on App, not TEAMS_SDK_AVAILABLE. The latter is a find_spec
# probe and can be True from the microsoft_teams namespace without
# symbols ever being bound (check_teams_requirements returning
# False is ignored if we only inspect the flag).
check_teams_requirements()
if not TEAMS_SDK_AVAILABLE:
if App is None or ClientOptions is None:
self._set_fatal_error(
"MISSING_SDK",
"microsoft-teams-apps could not be installed. "
@@ -858,7 +881,7 @@ class TeamsAdapter(BasePlatformAdapter):
f"Teams connection failed: {e}",
retryable=True,
)
logger.error("[teams] Failed to connect: %s", e)
logger.error("[teams] Failed to connect: %s", e, exc_info=True)
return False
async def disconnect(self) -> None:

View File

@@ -1230,12 +1230,19 @@ def ensure_and_bind(
"""
try:
ensure(feature, prompt=prompt)
except (FeatureUnavailable, Exception):
except FeatureUnavailable as exc:
logger.warning("%s", exc)
return False
except Exception as exc:
logger.warning("Failed to ensure feature %r: %s", feature, exc)
return False
try:
bindings = importer()
except ImportError:
except ImportError as exc:
logger.warning(
"Failed to import feature %r after install: %s", feature, exc
)
return False
target_globals.update(bindings)