Skip to content

Commit

Permalink
Merge pull request #3 from tofarr/use_injecty
Browse files Browse the repository at this point in the history
Using Injecty instead of native DI
  • Loading branch information
tofarr authored Feb 25, 2024
2 parents d94b041 + 0cdf55a commit d2dbc34
Show file tree
Hide file tree
Showing 26 changed files with 356 additions and 308 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Install pytest code coverage
run: python -m pip install ".[dev]"
- name: testcoverage
run: python -m pytest -n auto --cov-report=term-missing --cov=tests --cov=schemey --cov=marshy_config_schemey --cov=schemey_config_default --cov-fail-under=100
run: python -m pytest -n auto --cov-report=term-missing --cov=tests --cov=schemey --cov=injecty_config_schemey --cov-fail-under=100

pylint:
runs-on: ubuntu-latest
Expand All @@ -40,4 +40,4 @@ jobs:
- name: Install pylint
run: python -m pip install ".[dev]"
- name: lint
run: pylint marshy_config_schemey schemey schemey_config_default
run: pylint injecty_config_schemey schemey
20 changes: 8 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,24 +62,20 @@ This demonstrates starting with a json schema and generating python dataclasses

### Configuring the Context itself

Schemey uses a strategy very similar to marshy for configuration.
Schemey uses [Injecty](https://github.org/tofarr/injecty) for configuration.
The default configuration is [here](injecty_config_schemey/__init__.py)

Schemey looks for top level modules starting with the name `schemey_config_*`,
sorts them by `priority`, and applies them to the default context by invoking their
`configure` function. [schemey_config_default/__init__.py](schemey_config_default/__init__.py)
contains the default set of factories.

For example, for a project named `no_more_uuids`, I may add a file `schemey_config_no_more_uuids/__init__.py`:
For example, for a project named `no_more_uuids`, I may add a file `injecty_config_no_more_uuids/__init__.py`:

```
from schemey.factory.schema_factory_abc import SchemaFactoryABC
from schemey.factory.uuid_factory import UuidFactory
priority = 120 # Applied after default
def configure(context):
# For some reason, I don't want to be able to generate schemas for uuids!
context.factories = [
f for f in context.factories
if 'uuid' not in f.__class__.__name__.lower()
]
context.deregister_impl(SchemaFactoryABC, UuidFactory)
```

Expand Down
61 changes: 61 additions & 0 deletions injecty_config_schemey/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from injecty import InjectyContext
from marshy.marshaller.marshaller_abc import MarshallerABC

from schemey.factory.any_of_schema_factory import AnyOfSchemaFactory
from schemey.factory.array_schema_factory import ArraySchemaFactory
from schemey.factory.dataclass_schema_factory import DataclassSchemaFactory
from schemey.factory.datetime_factory import DatetimeFactory
from schemey.factory.enum_schema_factory import EnumSchemaFactory
from schemey.factory.external_type_factory import ExternalTypeFactory
from schemey.factory.factory_schema_factory import FactorySchemaFactory
from schemey.factory.impl_schema_factory import ImplSchemaFactory
from schemey.factory.ref_schema_factory import RefSchemaFactory
from schemey.factory.schema_factory_abc import SchemaFactoryABC
from schemey.factory.simple_type_factory import (
BoolTypeFactory,
IntTypeFactory,
NoneTypeFactory,
FloatFactory,
StrFactory,
)
from schemey.factory.tuple_schema_factory import TupleSchemaFactory
from schemey.factory.uuid_factory import UuidFactory
from schemey.json_schema.ranges_validator import RangesValidator
from schemey.json_schema.schema_validator_abc import SchemaValidatorABC
from schemey.json_schema.timestamp_validator import TimestampValidator
from schemey.schema_marshaller import SchemaMarshaller

priority = 100


def configure(context: InjectyContext):
context.register_impl(MarshallerABC, SchemaMarshaller)
context.register_impls(
SchemaFactoryABC,
[
RefSchemaFactory,
BoolTypeFactory,
IntTypeFactory,
NoneTypeFactory,
FloatFactory,
StrFactory,
DatetimeFactory,
UuidFactory,
ArraySchemaFactory,
TupleSchemaFactory,
ExternalTypeFactory,
DataclassSchemaFactory,
EnumSchemaFactory,
FactorySchemaFactory,
ImplSchemaFactory,
AnyOfSchemaFactory,
],
)

context.register_impls(
SchemaValidatorABC,
[
RangesValidator,
TimestampValidator,
],
)
9 changes: 0 additions & 9 deletions marshy_config_schemey/__init__.py

This file was deleted.

27 changes: 3 additions & 24 deletions schemey/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import importlib
import pkgutil
from typing import Optional, Type
from typing import Type

from marshy import get_default_context
from marshy.marshaller_context import MarshallerContext
from marshy.types import ExternalItemType

from schemey.schema import Schema
from schemey.schema_context import SchemaContext
from schemey.schema_context import SchemaContext, create_schema_context

_default_context = None
CONFIG_MODULE_PREFIX = "schemey_config_"
Expand All @@ -17,27 +13,10 @@
def get_default_schema_context() -> SchemaContext:
global _default_context
if not _default_context:
_default_context = new_default_schema_context()
_default_context = create_schema_context()
return _default_context


def new_default_schema_context(
marshaller_context: Optional[MarshallerContext] = None,
) -> SchemaContext:
if marshaller_context is None:
marshaller_context = get_default_context()
context = SchemaContext(marshaller_context=marshaller_context)
# Set up context based on naming convention
module_info = (
m for m in pkgutil.iter_modules() if m.name.startswith(CONFIG_MODULE_PREFIX)
)
modules = [importlib.import_module(m.name) for m in module_info]
modules.sort(key=lambda m: m.priority, reverse=True)
for m in modules:
getattr(m, "configure")(context)
return context


def schema_from_type(type_: Type) -> Schema:
return get_default_schema_context().schema_from_type(type_)

Expand Down
6 changes: 2 additions & 4 deletions schemey/factory/dataclass_schema_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def from_type(
required.append(field.name)
if field.default is not dataclasses.MISSING:
field_schema = Schema({**field_schema.schema}, field_schema.python_type)
field_schema.schema["default"] = context.marshaller_context.dump(
field_schema.schema["default"] = context.marshy_context.dump(
field.default, types[field.name]
)
properties[field.name] = field_schema.schema
Expand Down Expand Up @@ -95,9 +95,7 @@ def from_json(
)
default = field_item.get("default", dataclasses.MISSING)
if default is not dataclasses.MISSING:
default = context.marshaller_context.load(
field_schema.python_type, default
)
default = context.marshy_context.load(field_schema.python_type, default)
p = params_with_default
else:
p = params
Expand Down
26 changes: 9 additions & 17 deletions schemey/factory/impl_schema_factory.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from dataclasses import dataclass
from typing import Type, Optional, Set, Dict
from typing import Type, Optional, Dict

from marshy.factory.impl_marshaller_factory import ImplMarshallerFactory
from marshy.types import ExternalItemType

from schemey.factory.schema_factory_abc import SchemaFactoryABC
Expand All @@ -27,7 +26,10 @@ def from_type(
path: str,
ref_schemas: Dict[Type, Schema],
) -> Optional[Schema]:
impls = self.get_impls(type_, context)
# noinspection PyTypeChecker
impls = context.marshy_context.injecty_context.get_impls(
type_, permit_no_impl=True
)
if impls:
impls = sorted(list(impls), key=lambda i: i.__name__)
any_of = []
Expand Down Expand Up @@ -55,17 +57,7 @@ def from_json(
name = item.get("name")
if not name or not item.get("anyOf"):
return
factories = context.marshaller_context.get_factories()
for factory in factories:
if isinstance(factory, ImplMarshallerFactory):
if factory.base.__name__ == name:
schema = self.from_type(factory.base, context, path, ref_schemas)
return schema

@staticmethod
def get_impls(type_: Type, context: SchemaContext) -> Optional[Set[Type]]:
factories = context.marshaller_context.get_factories()
for factory in factories:
if isinstance(factory, ImplMarshallerFactory):
if factory.base == type_:
return factory.impls
for base, _ in context.marshy_context.injecty_context.impls.items():
if base.__name__ == name:
schema = self.from_type(base, context, path, ref_schemas)
return schema
16 changes: 5 additions & 11 deletions schemey/factory/schema_factory_abc.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
from abc import abstractmethod, ABC
from functools import total_ordering
from typing import Type, Optional, Dict

from marshy.types import ExternalItemType

from schemey.schema import Schema
from schemey.schema_context import SchemaContext

_SchemaContext = "schemey.schema_context.SchemaContext"


@total_ordering
class SchemaFactoryABC(ABC):
@property
def priority(self):
return 100
priority: int = 100

@abstractmethod
def from_type(
self,
type_: Type,
context: SchemaContext,
context: _SchemaContext,
path: str,
ref_schemas: Dict[Type, Schema],
) -> Optional[Schema]:
Expand All @@ -30,13 +27,10 @@ def from_type(
def from_json(
self,
item: ExternalItemType,
context: SchemaContext,
context: _SchemaContext,
path: str,
ref_schemas: Dict[str, Schema],
) -> Optional[Schema]:
"""
Create a schema for the type given, or return None if that was not possible
"""

def __lt__(self, other):
return self.priority < getattr(other, "priority", None)
31 changes: 31 additions & 0 deletions schemey/factory/simple_type_factory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass
from types import NoneType
from typing import Optional, Type, Dict

from marshy.types import ExternalItemType
Expand Down Expand Up @@ -34,3 +35,33 @@ def from_json(
type_ = item.get("type")
if type_ == self.json_type:
return Schema(item, self.python_type)


@dataclass
class BoolTypeFactory(SimpleTypeFactory):
python_type: Type = bool
json_type: str = "boolean"


@dataclass
class IntTypeFactory(SimpleTypeFactory):
python_type: Type = int
json_type: str = "integer"


@dataclass
class NoneTypeFactory(SimpleTypeFactory):
python_type: Type = NoneType
json_type: str = "null"


@dataclass
class FloatFactory(SimpleTypeFactory):
python_type: Type = float
json_type: str = "number"


@dataclass
class StrFactory(SimpleTypeFactory):
python_type: Type = str
json_type: str = "string"
11 changes: 0 additions & 11 deletions schemey/json_schema/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +0,0 @@
from typing import Callable, Dict

_custom_validators = {}


def register_custom_json_schema_validator(property_name: str, validator: Callable):
_custom_validators[property_name] = validator


def get_custom_json_schema_validators() -> Dict[str, Callable]:
return _custom_validators
43 changes: 0 additions & 43 deletions schemey/json_schema/ranges.py

This file was deleted.

Loading

0 comments on commit d2dbc34

Please sign in to comment.