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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@ target/

# extension stub files
src/gino/ext/*.pyi

# ignore virtualenv
.venv

# ignore sqlalchemy local folder
sqlalchemy
18 changes: 18 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[mypy]
plugins = sqlalchemy.ext.mypy.plugin

# --strict
disallow_any_generics = True
disallow_subclassing_any = True
disallow_untyped_calls = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_return_any = True
implicit_reexport = False
strict_equality = True
# --strict end
27 changes: 14 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@ sqlalchemy = {path = "./sqlalchemy"}
importlib_metadata = {version = "^1.7.0", python = "<3.8"}

# extensions
gino-starlette = { version = "^0.1.1", optional = true }
gino-aiohttp = { version = "^0.1.0", optional = true }
gino-tornado = { version = "^0.1.0", optional = true }
gino-sanic = { version = "^0.1.0", optional = true }
gino-quart = { version = "^0.1.0", optional = true }

[tool.poetry.extras]
starlette = ["gino-starlette"]
aiohttp = ["gino-aiohttp"]
tornado = ["gino-tornado"]
sanic = ["gino-sanic"]
quart = ["gino-quart"]
#gino-starlette = { version = "^0.1.1", optional = true }
#gino-aiohttp = { version = "^0.1.0", optional = true }
#gino-tornado = { version = "^0.1.0", optional = true }
#gino-sanic = { version = "^0.1.0", optional = true }
#gino-quart = { version = "^0.1.0", optional = true }
#
#[tool.poetry.extras]
#starlette = ["gino-starlette"]
#aiohttp = ["gino-aiohttp"]
#tornado = ["gino-tornado"]
#sanic = ["gino-sanic"]
#quart = ["gino-quart"]

[tool.poetry.dev-dependencies]
psycopg2-binary = "^2.8.5"
Expand All @@ -52,7 +52,8 @@ pytest-asyncio = "^0.14.0"
pytest-mock = "^3.3.0"
pytest-cov = "^2.10.1"
black = "^19.10b0"
mypy = "^0.782"
mypy = "^0.812"
sqlalchemy2-stubs = "^0.0.1a11"

# docs
sphinx = "^3.2.1"
Expand Down
70 changes: 40 additions & 30 deletions src/gino/json_support.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,46 @@
from __future__ import annotations

from datetime import datetime

import sqlalchemy as sa

from .exceptions import UnknownJSONPropertyError
from typing import Callable, Any, Optional, \
TypeVar, Dict, Union, \
Hashable, Generic, cast

DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"
NONE = object()

J = TypeVar('J', bound='JSONProperty')
T = TypeVar('T', Callable[[J, Any], Any], Optional[Callable[[J, Any], Any]])

class Hook:
def __init__(self, parent):
def __init__(self, parent: J, method: Optional[T] = None) -> None:
self.parent = parent
self.method = None
self.method = method

def __call__(self, method):
def __call__(self, method: T) -> JSONProperty:
self.method = method
return self.parent

def call(self, instance, val):
def call(self, instance: J, val: Optional[Any]) -> Any:
if self.method is not None:
val = self.method(instance, val)
return val


class JSONProperty:
def __init__(self, default=None, prop_name="profile"):
def __init__(self, default: Optional[Callable[[J], Any]] = None, prop_name: str = "profile") -> None:
self.name = None
self.default = default
self.prop_name = prop_name
self.expression = Hook(self)
self.after_get = Hook(self)
self.before_set = Hook(self)
self.__profile__: Optional[Dict[Any, Any]] = None

def __get__(self, instance, owner):
def __get__(self, instance: J, owner: J) -> Any:
if instance is None:
exp = self.make_expression(getattr(owner, self.prop_name)[self.name])
return self.expression.call(owner, exp)
Expand All @@ -44,17 +52,17 @@ def __get__(self, instance, owner):
val = self.default
return self.after_get.call(instance, val)

def __set__(self, instance, value):
def __set__(self, instance: J, value: Any) -> None:
self.get_profile(instance)[self.name] = self.before_set.call(instance, value)

def __delete__(self, instance):
def __delete__(self, instance: J) -> None:
self.get_profile(instance).pop(self.name, None)

def get_profile(self, instance):
def get_profile(self, instance: J) -> Union[Any, Dict[Any, Any]]:
if instance.__profile__ is None:
props = type(instance).__dict__
instance.__profile__ = {}
profiles = {}
profiles: Dict[Any, Any] = {}
for prop_name in getattr(instance, "__json_prop_names__", set()):
profiles.update(getattr(instance, prop_name, None) or {})
for key, value in profiles.items():
Expand All @@ -77,19 +85,20 @@ def get_profile(self, instance):

return instance.__profile__

def save(self, instance, value=NONE):
def save(self, instance: J, value: Union[sa.sql.ClauseElement, object] = NONE) -> Any:
profile = getattr(instance, self.prop_name, None)
if profile is None:
profile = {}
setattr(instance, self.prop_name, profile)
if value is NONE:
instance.__profile__ = cast(Dict[Any, Any], instance.__profile__) # mypy: it is for sure dict
value = instance.__profile__[self.name]
if not isinstance(value, sa.sql.ClauseElement):
value = self.encode(value)
rv = profile[self.name] = value
return rv

def reload(self, instance):
def reload(self, instance: J) -> None:
if instance.__profile__ is None:
return
profile = getattr(instance, self.prop_name, None) or {}
Expand All @@ -99,88 +108,88 @@ def reload(self, instance):
else:
instance.__profile__[self.name] = self.decode(value)

def make_expression(self, base_exp):
def make_expression(self, base_exp: Any) -> Any:
return base_exp

def decode(self, val):
def decode(self, val: Any) -> Any:
return val

def encode(self, val):
def encode(self, val: Any) -> Any:
return val

def __hash__(self):
def __hash__(self) -> int:
return hash(self.name)


class StringProperty(JSONProperty):
def make_expression(self, base_exp):
def make_expression(self, base_exp: Any) -> Any:
return base_exp.astext


class DateTimeProperty(JSONProperty):
def make_expression(self, base_exp):
def make_expression(self, base_exp: Any) -> Any:
return base_exp.astext.cast(sa.DateTime)

def decode(self, val):
def decode(self, val: Any) -> Any:
if val:
val = datetime.strptime(val, DATETIME_FORMAT)
return val

def encode(self, val):
def encode(self, val: Any) -> Any:
if isinstance(val, datetime):
val = val.strftime(DATETIME_FORMAT)
return val


class IntegerProperty(JSONProperty):
def make_expression(self, base_exp):
def make_expression(self, base_exp: Any) -> Any:
return base_exp.astext.cast(sa.Integer)

def decode(self, val):
def decode(self, val: Any) -> Any:
if val is not None:
val = int(val)
return val

def encode(self, val):
def encode(self, val: Any) -> Any:
if val is not None:
val = int(val)
return val


class BooleanProperty(JSONProperty):
def make_expression(self, base_exp):
def make_expression(self, base_exp: Any) -> Any:
return base_exp.astext.cast(sa.Boolean)

def decode(self, val):
def decode(self, val: Any) -> Any:
if val is not None:
val = bool(val)
return val

def encode(self, val):
def encode(self, val: Any) -> Any:
if val is not None:
val = bool(val)
return val


class ObjectProperty(JSONProperty):
def decode(self, val):
def decode(self, val: Any) -> Any:
if val is not None:
val = dict(val)
return val

def encode(self, val):
def encode(self, val: Any) -> Any:
if val is not None:
val = dict(val)
return val


class ArrayProperty(JSONProperty):
def decode(self, val):
def decode(self, val: Any) -> Any:
if val is not None:
val = list(val)
return val

def encode(self, val):
def encode(self, val: Any) -> Any:
if val is not None:
val = list(val)
return val
Expand All @@ -194,4 +203,5 @@ def encode(self, val):
"BooleanProperty",
"ObjectProperty",
"ArrayProperty",
"DATETIME_FORMAT",
]