Refactor code structure for improved readability and maintainability

This commit is contained in:
claudi 2026-04-07 09:10:53 +02:00
parent 389d72a136
commit aa4c067ea8
1685 changed files with 393439 additions and 71932 deletions

View file

@ -0,0 +1,37 @@
from .base import (
SchemaStrategy,
TypedSchemaStrategy
)
from .scalar import (
Typeless,
Null,
Boolean,
Number,
String
)
from .array import List, Tuple
from .object import Object
BASIC_SCHEMA_STRATEGIES = (
Null,
Boolean,
Number,
String,
List,
Tuple,
Object
)
__all__ = (
'SchemaStrategy',
'TypedSchemaStrategy',
'Null',
'Boolean',
'Number',
'String',
'List',
'Tuple',
'Object',
'Typeless',
'BASIC_SCHEMA_STRATEGIES'
)

View file

@ -0,0 +1,79 @@
from .base import SchemaStrategy
class BaseArray(SchemaStrategy):
"""
abstract array schema strategy
"""
KEYWORDS = ('type', 'items')
@staticmethod
def match_object(obj):
return isinstance(obj, list)
def to_schema(self):
schema = super().to_schema()
schema['type'] = 'array'
if self._items:
schema['items'] = self.items_to_schema()
return schema
class List(BaseArray):
"""
strategy for list-style array schemas. This is the default
strategy for arrays.
"""
@staticmethod
def match_schema(schema):
return schema.get('type') == 'array' \
and isinstance(schema.get('items', {}), dict)
def __init__(self, node_class):
super().__init__(node_class)
self._items = node_class()
def add_schema(self, schema):
super().add_schema(schema)
if 'items' in schema:
self._items.add_schema(schema['items'])
def add_object(self, obj):
for item in obj:
self._items.add_object(item)
def items_to_schema(self):
return self._items.to_schema()
class Tuple(BaseArray):
"""
strategy for tuple-style array schemas. These will always have
an items key to preserve the fact that it's a tuple.
"""
@staticmethod
def match_schema(schema):
return schema.get('type') == 'array' \
and isinstance(schema.get('items'), list)
def __init__(self, node_class):
super().__init__(node_class)
self._items = [node_class()]
def add_schema(self, schema):
super().add_schema(schema)
if 'items' in schema:
self._add(schema['items'], 'add_schema')
def add_object(self, obj):
self._add(obj, 'add_object')
def _add(self, items, func):
while len(self._items) < len(items):
self._items.append(self.node_class())
for subschema, item in zip(self._items, items):
getattr(subschema, func)(item)
def items_to_schema(self):
return [item.to_schema() for item in self._items]

View file

@ -0,0 +1,78 @@
from copy import copy
from warnings import warn
class SchemaStrategy:
"""
base schema strategy. This contains the common interface for
all subclasses:
* match_schema
* match_object
* __init__
* add_schema
* add_object
* to_schema
* __eq__
"""
KEYWORDS = ('type',)
@classmethod
def match_schema(cls, schema):
raise NotImplementedError("'match_schema' not implemented")
@classmethod
def match_object(cls, obj):
raise NotImplementedError("'match_object' not implemented")
def __init__(self, node_class):
self.node_class = node_class
self._extra_keywords = {}
def add_schema(self, schema):
self._add_extra_keywords(schema)
def _add_extra_keywords(self, schema):
for keyword, value in schema.items():
if keyword in self.KEYWORDS:
continue
elif keyword not in self._extra_keywords:
self._extra_keywords[keyword] = value
elif self._extra_keywords[keyword] != value:
warn(('Schema incompatible. Keyword {0!r} has conflicting '
'values ({1!r} vs. {2!r}). Using {1!r}').format(
keyword, self._extra_keywords[keyword], value))
def add_object(self, obj):
pass
def to_schema(self):
return copy(self._extra_keywords)
def __eq__(self, other):
""" Required for SchemaBuilder.__eq__ to work properly """
return (isinstance(other, self.__class__)
and self.__dict__ == other.__dict__)
class TypedSchemaStrategy(SchemaStrategy):
"""
base schema strategy class for scalar types. Subclasses define
these two class constants:
* `JS_TYPE`: a valid value of the `type` keyword
* `PYTHON_TYPE`: Python type objects - can be a tuple of types
"""
@classmethod
def match_schema(cls, schema):
return schema.get('type') == cls.JS_TYPE
@classmethod
def match_object(cls, obj):
return isinstance(obj, cls.PYTHON_TYPE)
def to_schema(self):
schema = super().to_schema()
schema['type'] = self.JS_TYPE
return schema

View file

@ -0,0 +1,97 @@
from collections import defaultdict
from re import search
from .base import SchemaStrategy
class Object(SchemaStrategy):
"""
object schema strategy
"""
KEYWORDS = ('type', 'properties', 'patternProperties', 'required')
@staticmethod
def match_schema(schema):
return schema.get('type') == 'object'
@staticmethod
def match_object(obj):
return isinstance(obj, dict)
def __init__(self, node_class):
super().__init__(node_class)
self._properties = defaultdict(node_class)
self._pattern_properties = defaultdict(node_class)
self._required = None
self._include_empty_required = False
def add_schema(self, schema):
super().add_schema(schema)
if 'properties' in schema:
for prop, subschema in schema['properties'].items():
subnode = self._properties[prop]
if subschema is not None:
subnode.add_schema(subschema)
if 'patternProperties' in schema:
for pattern, subschema in schema['patternProperties'].items():
subnode = self._pattern_properties[pattern]
if subschema is not None:
subnode.add_schema(subschema)
if 'required' in schema:
required = set(schema['required'])
if not required:
self._include_empty_required = True
if self._required is None:
self._required = required
else:
self._required &= required
def add_object(self, obj):
properties = set()
for prop, subobj in obj.items():
pattern = None
if prop not in self._properties:
pattern = self._matching_pattern(prop)
if pattern is not None:
self._pattern_properties[pattern].add_object(subobj)
else:
properties.add(prop)
self._properties[prop].add_object(subobj)
if self._required is None:
self._required = properties
else:
self._required &= properties
def _matching_pattern(self, prop):
for pattern in self._pattern_properties.keys():
if search(pattern, prop):
return pattern
def _add(self, items, func):
while len(self._items) < len(items):
self._items.append(self._schema_node_class())
for subschema, item in zip(self._items, items):
getattr(subschema, func)(item)
def to_schema(self):
schema = super().to_schema()
schema['type'] = 'object'
if self._properties:
schema['properties'] = self._properties_to_schema(
self._properties)
if self._pattern_properties:
schema['patternProperties'] = self._properties_to_schema(
self._pattern_properties)
if self._required or self._include_empty_required:
schema['required'] = sorted(self._required)
return schema
def _properties_to_schema(self, properties):
schema_properties = {}
for prop, schema_node in properties.items():
schema_properties[prop] = schema_node.to_schema()
return schema_properties

View file

@ -0,0 +1,78 @@
from .base import SchemaStrategy, TypedSchemaStrategy
class Typeless(SchemaStrategy):
"""
schema strategy for schemas with no type. This is only used when
there is no other active strategy, and it will be merged into the
first typed strategy that gets added.
"""
@classmethod
def match_schema(cls, schema):
return 'type' not in schema
@classmethod
def match_object(cls, obj):
return False
class Null(TypedSchemaStrategy):
"""
strategy for null schemas
"""
JS_TYPE = 'null'
PYTHON_TYPE = type(None)
class Boolean(TypedSchemaStrategy):
"""
strategy for boolean schemas
"""
JS_TYPE = 'boolean'
PYTHON_TYPE = bool
class String(TypedSchemaStrategy):
"""
strategy for string schemas - works for ascii and unicode strings
"""
JS_TYPE = 'string'
PYTHON_TYPE = str
class Number(SchemaStrategy):
"""
strategy for integer and number schemas. It automatically
converts from `integer` to `number` when a float object or a
number schema is added
"""
JS_TYPES = ('integer', 'number')
PYTHON_TYPES = (int, float)
@classmethod
def match_schema(cls, schema):
return schema.get('type') in cls.JS_TYPES
@classmethod
def match_object(cls, obj):
# cannot use isinstance() because boolean is a subtype of int
return type(obj) in cls.PYTHON_TYPES
def __init__(self, node_class):
super().__init__(node_class)
self._type = 'integer'
def add_schema(self, schema):
super().add_schema(schema)
if schema.get('type') == 'number':
self._type = 'number'
def add_object(self, obj):
if isinstance(obj, float):
self._type = 'number'
def to_schema(self):
schema = super().to_schema()
schema['type'] = self._type
return schema