Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# .coveragerc to control coverage.py
[run]
omit =
rest_client_gen/lazy.py

[report]
exclude_lines =
Expand Down
2 changes: 1 addition & 1 deletion rest_client_gen/dynamic_typing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
BaseType, ImportPathList, MetaData, NoneType, Unknown, UnknownType
)
from .complex import ComplexType, DList, DOptional, DTuple, DUnion, SingleType
from .models_meta import ModelMeta, ModelPtr
from .models_meta import AbsoluteModelRef, ModelMeta, ModelPtr
from .string_serializable import (
BooleanString, FloatString, IntString, StringSerializable, StringSerializableRegistry, registry
)
Expand Down
64 changes: 61 additions & 3 deletions rest_client_gen/dynamic_typing/models_meta.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import List, Optional, Set, Tuple
import threading
from typing import Dict, List, Optional, Set, Tuple, Union

import inflection

Expand Down Expand Up @@ -126,5 +127,62 @@ def replace_parent(self, t: ModelMeta, **kwargs) -> 'ModelPtr':
return self

def to_typing_code(self) -> Tuple[ImportPathList, str]:
imports, model = self.type.to_typing_code()
return imports, f"'{model}'"
return AbsoluteModelRef(self.type).to_typing_code()


ContextInjectionType = Dict[ModelMeta, Union[ModelMeta, str]]


class AbsoluteModelRef:
"""
Model forward absolute references. Using ContextManager to inject real models paths into typing code.
Forward reference is the typing string like ``List['MyModel']``.
If the model is defined as child model and is used by another nested model
than the reference to this model should be an absolute path:

class Model:
class GenericChildModel:
...

class NestedModel:
data: 'Model.GenericChildModel' # <--- this

This information is only available at the models code generation stage
while typing code is generated from raw metadata and passing this absolute path as argument
to each ModelPtr would be annoying.
"""

class Context:
data = threading.local()
data.context: ContextInjectionType = None

def __init__(self, patches: ContextInjectionType):
self.context: ContextInjectionType = patches
self._old: ContextInjectionType = None

def __enter__(self):
self._old = self.data.context
self.data.context = self.context

def __exit__(self, exc_type, exc_val, exc_tb):
self.data.context = self._old

@classmethod
def inject(cls, patches: ContextInjectionType):
context = cls.Context(patches)
return context

def __init__(self, model: ModelMeta):
self.model = model

def to_typing_code(self) -> Tuple[ImportPathList, str]:
context_data = self.Context.data.context
if context_data:
model_path = context_data.get(self.model, "")
if isinstance(model_path, ModelMeta):
model_path = model_path.name
else:
model_path = ""
imports, model = self.model.to_typing_code()
s = ".".join(filter(None, (model_path, model)))
return imports, f"'{s}'"
192 changes: 192 additions & 0 deletions rest_client_gen/lazy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
"""
Fork of https://github.com/django/django/blob/2.1/django/utils/functional.py
Note: keep_lazy function is modified and returns lazy value always
even for functions without arguments.

Copyright (c) Django Software Foundation and individual contributors.
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

3. Neither the name of Django nor the names of its contributors may be used
to endorse or promote products derived from this software without
specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
from functools import total_ordering, wraps


class Promise:
"""
Base class for the proxy class created in the closure of the lazy function.
It's used to recognize promises in code.
"""
pass


def lazy(func, *resultclasses):
"""
Turn any callable into a lazy evaluated callable. result classes or types
is required -- at least one is needed so that the automatic forcing of
the lazy evaluation code is triggered. Results are not memoized; the
function is evaluated on every access.
"""

@total_ordering
class __proxy__(Promise):
"""
Encapsulate a function call and act as a proxy for methods that are
called on the result of that function. The function is not evaluated
until one of the methods on the result is called.
"""
__prepared = False

def __init__(self, args, kw):
self.__args = args
self.__kw = kw
if not self.__prepared:
self.__prepare_class__()
self.__prepared = True

def __reduce__(self):
return (
_lazy_proxy_unpickle,
(func, self.__args, self.__kw) + resultclasses
)

def __repr__(self):
return repr(self.__cast())

@classmethod
def __prepare_class__(cls):
for resultclass in resultclasses:
for type_ in resultclass.mro():
for method_name in type_.__dict__:
# All __promise__ return the same wrapper method, they
# look up the correct implementation when called.
if hasattr(cls, method_name):
continue
meth = cls.__promise__(method_name)
setattr(cls, method_name, meth)
cls._delegate_bytes = bytes in resultclasses
cls._delegate_text = str in resultclasses
assert not (cls._delegate_bytes and cls._delegate_text), (
"Cannot call lazy() with both bytes and text return types.")
if cls._delegate_text:
cls.__str__ = cls.__text_cast
elif cls._delegate_bytes:
cls.__bytes__ = cls.__bytes_cast

@classmethod
def __promise__(cls, method_name):
# Builds a wrapper around some magic method
def __wrapper__(self, *args, **kw):
# Automatically triggers the evaluation of a lazy value and
# applies the given magic method of the result type.
res = func(*self.__args, **self.__kw)
return getattr(res, method_name)(*args, **kw)

return __wrapper__

def __text_cast(self):
return func(*self.__args, **self.__kw)

def __bytes_cast(self):
return bytes(func(*self.__args, **self.__kw))

def __bytes_cast_encoded(self):
return func(*self.__args, **self.__kw).encode()

def __cast(self):
if self._delegate_bytes:
return self.__bytes_cast()
elif self._delegate_text:
return self.__text_cast()
else:
return func(*self.__args, **self.__kw)

def __str__(self):
# object defines __str__(), so __prepare_class__() won't overload
# a __str__() method from the proxied class.
return str(self.__cast())

def __eq__(self, other):
if isinstance(other, Promise):
other = other.__cast()
return self.__cast() == other

def __lt__(self, other):
if isinstance(other, Promise):
other = other.__cast()
return self.__cast() < other

def __hash__(self):
return hash(self.__cast())

def __mod__(self, rhs):
if self._delegate_text:
return str(self) % rhs
return self.__cast() % rhs

def __deepcopy__(self, memo):
# Instances of this class are effectively immutable. It's just a
# collection of functions. So we don't need to do anything
# complicated for copying.
memo[id(self)] = self
return self

@wraps(func)
def __wrapper__(*args, **kw):
# Creates the proxy object, instead of the actual value.
return __proxy__(args, kw)

return __wrapper__


def _lazy_proxy_unpickle(func, args, kwargs, *resultclasses):
return lazy(func, *resultclasses)(*args, **kwargs)


def keep_lazy(*resultclasses):
"""
A decorator that allows a function to be called with one or more lazy arguments.
Return a __proxy__ object that will evaluate the function when needed.
"""
if not resultclasses:
raise TypeError("You must pass at least one argument to keep_lazy().")

def decorator(func):
lazy_func = lazy(func, *resultclasses)

@wraps(func)
def wrapper(*args, **kwargs):
return lazy_func(*args, **kwargs)

return wrapper

return decorator


def keep_lazy_text(func):
"""
A decorator for functions that accept lazy arguments and return text.
"""
return keep_lazy(str)(func)
26 changes: 16 additions & 10 deletions rest_client_gen/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,26 @@ def extract_root(model: ModelMeta) -> Set[Index]:
return roots


def compose_models(models_map: Dict[str, ModelMeta]) -> List[dict]:
ModelsStructureType = Tuple[List[dict], Dict[ModelMeta, ModelMeta]]


def compose_models(models_map: Dict[str, ModelMeta]) -> ModelsStructureType:
"""
Generate nested sorted models structure for internal usage.

:return: List of root models data, Map(child model -> root model) for absolute ref generation
"""
root_models = ListEx()
root_nested_ix = 0
structure_hash_table: Dict[Index, dict] = {
key: {
"model": model,
"nested": ListEx(),
"roots": list(extract_root(model))
"roots": list(extract_root(model)), # Indexes of root level models
} for key, model in models_map.items()
}
# TODO: Test path_injections
path_injections: Dict[ModelMeta, ModelMeta] = {}

for key, model in models_map.items():
pointers = list(filter_pointers(model))
Expand All @@ -85,10 +92,8 @@ def compose_models(models_map: Dict[str, ModelMeta]) -> List[dict]:
else:
parents = {ptr.parent.index for ptr in pointers}
struct = structure_hash_table[key]
# FIXME: "Model is using by single root model" case for the time being will be disabled
# until solution to make typing ref such as 'Parent.Child' will be found
# Model is using by other models
if has_root_pointers or len(parents) > 1: # and len(struct["roots"]) > 1
if has_root_pointers or len(parents) > 1 and len(struct["roots"]) > 1:
# Model is using by different root models
try:
root_models.insert_before(
Expand All @@ -98,17 +103,18 @@ def compose_models(models_map: Dict[str, ModelMeta]) -> List[dict]:
except ValueError:
root_models.insert(root_nested_ix, struct)
root_nested_ix += 1
# elif len(parents) > 1 and len(struct["roots"]) == 1:
# # Model is using by single root model
# parent = structure_hash_table[struct["roots"][0]]
# parent["nested"].insert(0, struct)
elif len(parents) > 1 and len(struct["roots"]) == 1:
# Model is using by single root model
parent = structure_hash_table[struct["roots"][0]]
parent["nested"].insert(0, struct)
path_injections[struct["model"]] = parent["model"]
else:
# Model is using by only one model
parent = structure_hash_table[next(iter(parents))]
struct = structure_hash_table[key]
parent["nested"].append(struct)

return root_models
return root_models, path_injections


def sort_fields(model_meta: ModelMeta) -> Tuple[List[str], List[str]]:
Expand Down
11 changes: 6 additions & 5 deletions rest_client_gen/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from jinja2 import Template

from rest_client_gen.dynamic_typing import compile_imports
from rest_client_gen.models import INDENT, OBJECTS_DELIMITER
from rest_client_gen.dynamic_typing import AbsoluteModelRef, compile_imports
from rest_client_gen.models import INDENT, ModelsStructureType, OBJECTS_DELIMITER
from . import indent, sort_fields
from ..dynamic_typing import ImportPathList, MetaData, ModelMeta, metadata_to_typing

Expand Down Expand Up @@ -49,7 +49,6 @@ class {{ name }}:
def __init__(self, model: ModelMeta, **kwargs):
self.model = model


def generate(self, nested_classes: List[str] = None) -> Tuple[ImportPathList, str]:
"""
:param nested_classes: list of strings that contains classes code
Expand Down Expand Up @@ -138,7 +137,7 @@ def _generate_code(
return imports, classes


def generate_code(structure: List[dict], class_generator: Type[GenericModelCodeGenerator],
def generate_code(structure: ModelsStructureType, class_generator: Type[GenericModelCodeGenerator],
class_generator_kwargs: dict = None, objects_delimiter: str = OBJECTS_DELIMITER) -> str:
"""
Generate ready-to-use code
Expand All @@ -149,7 +148,9 @@ def generate_code(structure: List[dict], class_generator: Type[GenericModelCodeG
:param objects_delimiter: Delimiter between root level classes
:return: Generated code
"""
imports, classes = _generate_code(structure, class_generator, class_generator_kwargs or {})
root, mapping = structure
with AbsoluteModelRef.inject(mapping):
imports, classes = _generate_code(root, class_generator, class_generator_kwargs or {})
if imports:
imports_str = compile_imports(imports) + objects_delimiter
else:
Expand Down
Loading