Add typing

Change-Id: I75d8e9603ae12aa89fe465ce438dd79fa475e48e
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2025-12-02 10:54:49 +00:00
parent d36e00490b
commit 30e23e7ff5
9 changed files with 94 additions and 40 deletions

View File

@@ -13,7 +13,7 @@ repos:
- id: check-yaml
files: .*\.(yaml|yml)$
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.7
rev: v0.14.8
hooks:
- id: ruff-check
args: ['--fix', '--unsafe-fixes']

View File

@@ -19,11 +19,13 @@ import signal
import socket
import sys
import threading
from types import FrameType
from typing import Any
from wsgiref.simple_server import make_server
from wsgiref.simple_server import WSGIRequestHandler
from oslo_config import cfg
from oslo_log import log as logging
from oslo_config import cfg # type: ignore
from oslo_log import log as logging # type: ignore
from prometheus_client import make_wsgi_app
from oslo_metrics import message_router
@@ -63,7 +65,7 @@ logging.setup(CONF, 'oslo-metrics')
class MetricsListener:
def __init__(self, socket_path):
def __init__(self, socket_path: str) -> None:
self.socket_path = socket_path
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
self.unlink(socket_path)
@@ -72,14 +74,14 @@ class MetricsListener:
self.start = True
self.router = message_router.MessageRouter()
def unlink(self, socket_path):
def unlink(self, socket_path: str) -> None:
try:
os.unlink(socket_path)
except OSError:
if os.path.exists(socket_path):
raise
def serve(self):
def serve(self) -> None:
while self.start:
readable, writable, exceptional = select.select(
[self.socket], [], [], 1
@@ -95,7 +97,7 @@ class MetricsListener:
except TimeoutError:
pass
def stop(self):
def stop(self) -> None:
self.socket.close()
self.start = False
@@ -103,20 +105,24 @@ class MetricsListener:
class _SilentHandler(WSGIRequestHandler):
"""WSGI handler that does not log requests."""
def log_message(self, format, *args):
def log_message(self, format: str, *args: Any) -> None:
"""Log nothing."""
httpd = None
def handle_sigterm(_signum, _frame):
def handle_sigterm(_signum: int, _frame: FrameType | None) -> None:
if httpd is None:
# this should never happen
raise RuntimeError('httpd is uninitialized')
LOG.debug("Caught sigterm")
shutdown_thread = threading.Thread(target=httpd.shutdown)
shutdown_thread.start()
def main():
def main() -> None:
cfg.CONF(sys.argv[1:])
socket_path = cfg.CONF.oslo_metrics.metrics_socket_file
m = MetricsListener(socket_path)
@@ -129,18 +135,21 @@ def main():
mt.start()
app = make_wsgi_app()
global httpd
if cfg.CONF.oslo_metrics.wsgi_silent_server:
httpd = make_server(
'',
CONF.oslo_metrics.prometheus_port,
app,
handler_class=_SilentHandler,
)
else:
httpd = make_server('', CONF.oslo_metrics.prometheus_port, app)
signal.signal(signal.SIGTERM, handle_sigterm)
try:
global httpd
if cfg.CONF.oslo_metrics.wsgi_silent_server:
httpd = make_server(
'',
CONF.oslo_metrics.prometheus_port,
app,
handler_class=_SilentHandler,
)
else:
httpd = make_server('', CONF.oslo_metrics.prometheus_port, app)
signal.signal(signal.SIGTERM, handle_sigterm)
httpd.serve_forever()
except KeyboardInterrupt:
pass

View File

@@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
from oslo_log import log as logging # type: ignore
from oslo_utils import importutils
from oslo_metrics import message_type
@@ -25,7 +25,7 @@ MODULE_LISTS = ["oslo_metrics.metrics.oslo_messaging"]
class MessageRouter:
def __init__(self):
def __init__(self) -> None:
self.modules = {}
for m_str in MODULE_LISTS:
mod = importutils.try_import(m_str, False)
@@ -33,14 +33,14 @@ class MessageRouter:
LOG.error("Failed to load module %s", m_str)
self.modules[m_str.split('.')[-1]] = mod
def process(self, raw_string):
def process(self, raw_string: bytes) -> None:
try:
metric = message_type.Metric.from_json(raw_string.decode())
self.dispatch(metric)
except Exception as e:
LOG.error("Failed to parse: %s", e)
def dispatch(self, metric):
def dispatch(self, metric: message_type.Metric) -> None:
if metric.module not in self.modules:
LOG.error("Failed to lookup modules by %s", metric.module)
return

View File

@@ -14,22 +14,28 @@
# under the License.
import json
from typing import Any, TypedDict
class UnSupportedMetricActionError(Exception):
def __init__(self, message=None):
def __init__(self, message: str | None = None) -> None:
self.message = message
class MetricValidationError(Exception):
def __init__(self, message=None):
def __init__(self, message: str | None = None) -> None:
self.message = message
class _MetricActionDict(TypedDict, total=False):
action: str
value: str
class MetricAction:
actions = ['inc', 'observe']
def __init__(self, action, value):
def __init__(self, action: str, value: str) -> None:
if action not in self.actions:
raise UnSupportedMetricActionError(
f"{action} action is not supported"
@@ -38,7 +44,7 @@ class MetricAction:
self.value = value
@classmethod
def validate(cls, metric_action_dict):
def validate(cls, metric_action_dict: _MetricActionDict) -> None:
if "value" not in metric_action_dict:
raise MetricValidationError("action need 'value' field")
if "action" not in metric_action_dict:
@@ -49,18 +55,22 @@ class MetricAction:
)
@classmethod
def from_dict(cls, metric_action_dict):
def from_dict(
cls, metric_action_dict: _MetricActionDict
) -> 'MetricAction':
return cls(metric_action_dict["action"], metric_action_dict["value"])
class Metric:
def __init__(self, module, name, action, **labels):
def __init__(
self, module: str, name: str, action: MetricAction, **labels: str
) -> None:
self.module = module
self.name = name
self.action = action
self.labels = labels
def to_json(self):
def to_json(self) -> str:
raw = {
"module": self.module,
"name": self.name,
@@ -73,7 +83,7 @@ class Metric:
return json.dumps(raw)
@classmethod
def from_json(cls, encoded):
def from_json(cls, encoded: str) -> 'Metric':
metric_dict = json.loads(encoded)
cls._validate(metric_dict)
return Metric(
@@ -84,7 +94,7 @@ class Metric:
)
@classmethod
def _validate(cls, metric_dict):
def _validate(cls, metric_dict: dict[str, Any]) -> None:
if "module" not in metric_dict:
raise MetricValidationError("module should be specified")

0
oslo_metrics/py.typed Normal file
View File

View File

@@ -13,13 +13,14 @@
"""
test_message_process
--------------------
Check that messages are processed correctly
"""
from unittest import mock
from oslo_metrics import message_router
from oslotest import base
from oslotest import base # type: ignore
import prometheus_client

View File

@@ -12,13 +12,16 @@
"""
test_message_validation
--------------------
-----------------------
Check that messages validation is working properly
"""
import json
from typing import Any
from oslo_metrics import message_type
from oslotest import base
from oslotest import base # type: ignore
class TestMetricValidation(base.BaseTestCase):
@@ -29,11 +32,14 @@ class TestMetricValidation(base.BaseTestCase):
try:
func(*args, **kwargs)
self.assertFail()
except Exception as e:
except (
message_type.MetricValidationError,
message_type.UnSupportedMetricActionError,
) as e:
self.assertEqual(message, e.message)
def test_message_validation(self):
metric = {}
metric: dict[str, Any] = {}
message = "module should be specified"
self.assertRaisesWithMessage(
message, message_type.Metric.from_json, json.dumps(metric)

View File

@@ -37,6 +37,20 @@ Repository = "https://opendev.org/openstack/oslo.metrics"
[tool.setuptools]
packages = ["oslo_metrics"]
[tool.mypy]
python_version = "3.10"
show_column_numbers = true
show_error_context = true
strict = true
exclude = '(?x)(doc | examples | releasenotes)'
[[tool.mypy.overrides]]
module = ["oslo_metrics.tests.*"]
disallow_untyped_calls = false
disallow_untyped_defs = false
disallow_subclassing_any = false
disallow_any_generics = false
[tool.ruff]
line-length = 79

16
tox.ini
View File

@@ -7,17 +7,30 @@ allowlist_externals =
find
deps =
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands =
find . -type f -name "*.pyc" -delete
stestr run --slowest {posargs}
[testenv:pep8]
skip_install = true
description =
Run style checks.
deps =
pre-commit
{[testenv:mypy]deps}
commands =
pre-commit run -a
{[testenv:mypy]commands}
[testenv:mypy]
description =
Run type checks.
deps =
{[testenv]deps}
mypy
commands =
mypy --cache-dir="{envdir}/mypy_cache" {posargs:oslo_metrics}
[testenv:venv]
commands = {posargs}
@@ -49,6 +62,7 @@ enable-extensions = H904
[hacking]
import_exceptions =
typing
[testenv:releasenotes]
allowlist_externals =