Use fastjsonschema instead of jsonschema.
authorBenjamin Braatz <bb@bbraatz.eu>
Fri, 3 Sep 2021 21:00:57 +0000 (23:00 +0200)
committerBenjamin Braatz <bb@bbraatz.eu>
Fri, 3 Sep 2021 21:00:57 +0000 (23:00 +0200)
controlpi/__init__.py
controlpi/baseplugin.py
controlpi/messagebus.py
setup.py

index dfde4e1a28855f02f53d8ef1dff6f1d9cacfda74..78e1ed6571b9e33719b12165c8013b474add9687 100644 (file)
@@ -11,7 +11,7 @@ The test function is a utility function to test plugins with minimal
 boilerplate code.
 """
 import asyncio
-import jsonschema  # type: ignore
+import fastjsonschema  # type: ignore
 
 from controlpi.messagebus import (MessageBus, BusException,
                                   Message, MessageTemplate)
@@ -27,13 +27,10 @@ CONF_SCHEMA = {'type': 'object',
 
 def _process_conf(message_bus: MessageBus,
                   conf: Dict[str, PluginConf]) -> List[Coroutine]:
-    jsonschema.Draft7Validator.check_schema(CONF_SCHEMA)
-    validator = jsonschema.Draft7Validator(CONF_SCHEMA)
-    valid = True
-    for error in validator.iter_errors(conf):
-        print(error)
-        valid = False
-    if not valid:
+    try:
+        conf = fastjsonschema.validate(CONF_SCHEMA, conf)
+    except fastjsonschema.JsonSchemaException as e:
+        print(f"Configuration not valid:\n{e.message}")
         return []
     plugins = PluginRegistry('controlpi_plugins', BasePlugin)
     coroutines = [message_bus.run()]
index c803462d61dd8bc2108b0ae5739450f99c19143c..72e85f5b91b9755aa6d4588bdb5b9a6b86e20107 100644 (file)
@@ -83,11 +83,11 @@ __pdoc__ = {'BasePlugin.CONF_SCHEMA': False}
 
 from abc import ABC, abstractmethod
 import asyncio
-import jsonschema  # type: ignore
+import fastjsonschema  # type: ignore
 
 from controlpi.messagebus import MessageBus
 
-from typing import Union, Dict, List, Any, Optional
+from typing import Union, Dict, List, Any, Optional, Callable
 JSONSchema = Union[bool, Dict[str, Union[None, str, int, float, bool,
                                          Dict[str, Any], List[Any]]]]
 # Could be more specific.
@@ -158,26 +158,24 @@ class BasePlugin(ABC):
 
     CONF_SCHEMA: JSONSchema = False
 
-    _validator: Optional[jsonschema.Draft7Validator] = None
+    _validate: Optional[Callable[[PluginConf], PluginConf]] = None
 
     def __init__(self, bus: MessageBus, name: str, conf: PluginConf) -> None:
         # noqa: D107
         self.bus = bus
         self.name = name
-        if not type(self)._validator:
-            jsonschema.Draft7Validator.check_schema(type(self).CONF_SCHEMA)
-            type(self)._validator = \
-                jsonschema.Draft7Validator(type(self).CONF_SCHEMA)
-        validator = type(self)._validator
-        assert isinstance(validator, jsonschema.Draft7Validator)
-        valid = True
-        for error in validator.iter_errors(conf):
-            print(error)
-            valid = False
-        if not valid:
+        if not type(self)._validate:
+            type(self)._validate = \
+                fastjsonschema.compile(type(self).CONF_SCHEMA)
+        self.conf = {}
+        validate = type(self)._validate
+        assert validate is not None
+        try:
+            self.conf = validate(conf)
+        except fastjsonschema.JsonSchemaException as e:
+            print(e.message)
             raise ConfException(f"Configuration for '{self.name}'"
                                 " is not valid.")
-        self.conf = conf
         self.process_conf()
 
     @abstractmethod
index d88707a947b61dde77662c89e0e76e07aaba1fb5..4ff345313109d4709d9adb9b60c00714ad524551 100644 (file)
@@ -83,7 +83,7 @@ Client 1: {'sender': '', 'target': 'Client 1'}
 """
 import asyncio
 import json
-import jsonschema  # type: ignore
+import fastjsonschema  # type: ignore
 import sys
 
 from typing import Union, Dict, List, Any, Iterable, Callable, Coroutine
@@ -98,37 +98,34 @@ JSONSchema = Union[bool, Dict[str, MessageValue]]
 MessageCallback = Callable[['Message'], Coroutine[Any, Any, None]]
 
 
-# Global cache of JSON schema validators:
-_validators: Dict[str, jsonschema.Draft7Validator] = {}
-# Draft7Validator is hardcoded, because _LATEST_VERSION is non-public in
-# jsonschema and we also perhaps do not want to upgrade automatically.
+# Global cache of JSON schema validation functions:
+_validates: Dict[str, Callable[[MessageValue], MessageValue]] = {}
 
 
-def check_schema(schema: JSONSchema) -> bool:
-    """Check if the given JSON schema is valid."""
-    try:
-        jsonschema.Draft7Validator.check_schema(schema)
-    except jsonschema.exceptions.SchemaError:
-        return False
-    return True
-
-
-def register_schema(schema: JSONSchema) -> None:
+def register_schema(schema: JSONSchema) -> bool:
     """Register the given JSON schema in the global cache."""
-    global _validators
+    global _validates
     schema_string = json.dumps(schema)
-    if schema_string not in _validators:
-        _validators[schema_string] = jsonschema.Draft7Validator(schema)
+    if schema_string not in _validates:
+        if not (isinstance(schema, dict) or isinstance(schema, bool)):
+            return False
+        try:
+            _validates[schema_string] = fastjsonschema.compile(schema)
+        except fastjsonschema.JsonSchemaDefinitionException:
+            return False
+    return True
 
 
 def validate(schema_string: str, value: MessageValue) -> bool:
     """Validate the given MessageValue against the given JSON schema string."""
-    global _validators
-    if schema_string not in _validators:
+    global _validates
+    if schema_string not in _validates:
         schema = json.loads(schema_string)
-        _validators[schema_string] = jsonschema.Draft7Validator(schema)
-    validator = _validators[schema_string]
-    for error in validator.iter_errors(value):
+        _validates[schema_string] = fastjsonschema.compile(schema)
+    validate = _validates[schema_string]
+    try:
+        validate(value)
+    except fastjsonschema.JsonSchemaException:
         return False
     return True
 
@@ -458,14 +455,14 @@ class MessageTemplate(Dict[str, JSONSchema]):
           ...
         TypeError: 'schema' is not a valid value in MessageTemplate
         (not a valid JSON schema).
+        >>> t['key'] = True
         """
         if not isinstance(key, str):
             raise TypeError(f"'{key}' is not a valid key in MessageTemplate"
                             " (not a string).")
-        if not check_schema(value):
+        if not register_schema(value):
             raise TypeError(f"'{value}' is not a valid value in"
                             " MessageTemplate (not a valid JSON schema).")
-        register_schema(value)
         super().__setitem__(key, value)
 
     def update(self, *args, **kwargs) -> None:
@@ -491,6 +488,7 @@ class MessageTemplate(Dict[str, JSONSchema]):
           ...
         TypeError: 'schema' is not a valid value in MessageTemplate
         (not a valid JSON schema).
+        >>> t.update({'key': True})
 
         This is also used in __init__:
         >>> t = MessageTemplate({'key 1': {'const': 'value'},
@@ -514,6 +512,7 @@ class MessageTemplate(Dict[str, JSONSchema]):
           ...
         TypeError: 'schema' is not a valid value in MessageTemplate
         (not a valid JSON schema).
+        >>> t = MessageTemplate({'key': True})
         """
         if args:
             if len(args) > 1:
index 60fa604bc6e8a6d2f02798a284491bb327052d59..fdaee719e5d6f54f6c6a269f1c7851aac48a8f9b 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,7 @@ setuptools.setup(
     package_data={"controlpi": ["py.typed"]},
     zip_safe=False,
     install_requires=[
-        "jsonschema",
+        "fastjsonschema",
         "pyinotify",
     ],
     extras_require={