Don't know how to literal-quote value ~ : sqlalchemy에게 이 문자를 어떻게 컴파일 해야 하는지 알려줘

 

결론부터 말하자면 Sqlalchemy 1.3.6 이상에서는 일어나지 않는 문제이다.

🔗 sqlalchemy changelog 링크 

 

문제 원인

# ============== example ==============

query = db.session.query(m.Account).filter(m.Account.created_datetime == datetime.now())

query.statement.compile(
	dialect=postgresql.dialect(),
	compile_kwargs={'literal_binds': True},
)

statement를 컴파일 할 때 생기는 문제인데,

Sqlalchemy 1.3.6 버전 이하의 postgresql에서 DateTime을 처리하는 literal processor가 구현되어 있지 않아 생긴 문제로 보인다.

문제 해결

sqlalchemy 창시자 mike bayer 가 손수 해결해줬다.

모든 데이터베이스 엔진에 대해 Datetime객체를 ISO 8601 포멧으로 치환해주도록 변경한 듯 하다. > 🔗 commit log

 

https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/3744/6/lib/sqlalchemy/sql/sqltypes.py#703

 

gerrit.sqlalchemy.org

 

메타클래스에 대해 알아보기 이전에 파이썬의 데이터 모델에 대한 이해가 필요하다.

파이썬에서 모든 것은 데이터를 추상화 한 객체로 이루어져 있다.
또한, 파이썬의 객체는 아이덴티티, 값, 타입을 가지고 있다.

아이덴티티 (id)

id() 함수를 통해 얻을 수 있으며 객체의 수명동안 유일하고 불변함이 보장되는 정수다.

값 (value)

객체의 타입에 따라 불변할 수 있고 가변할 수도 있다. ex)tuple : 불변, list : 가변

타입 (type)

객체가 지원하는 연산들과 그 타입의 객체가 가질 수 있는 값(ex) int : 1, list : [1,2])들을 통해 객체의 특성을 정의한다. 객체의 타입은 type()을 통해 얻을 수 있으며, 불변하다.

여기서 말한 타입과 같이 파이썬의 모든 객체들은 어떠한 타입에 의해 정의된다.

파이썬의 type() 빌트인 함수를 사용하면 객체의 타입을 알 수 있다.

class Test:
    pass
t = Test()
type(t)
# <class '__main__.Test'>

def hello():
    pass
type(hello)
# <class 'function'>

type(1)
# <class 'int'>

위 예제를 보면 Test 클래스의 인스턴스인 tTest가 타입이고, hello 함수는 function이 타입이며, 정수 1은 int가 타입이다.

그렇다면, Test 클래스의 타입은 존재할까?

class Test:
    pass
type(Test)
# <class 'type'>

놀랍게도, Test 클래스 객체의 타입이 존재한다.
여기서 출력된 typeTest 클래스의 메타 클래스라고 하며, 인스턴스로 클래스를 가진다.

그러면 메타클래스는 무슨 용도로 사용하는 걸까?

그전에 몇가지 메타클래스의 매직 메소드에 대해 알아보자

class TestMeta(type):
    def __prepare__(mcs, *args, **kwarg): # 메타 클래스가 결정되었을 때 (mro가 구성된 후) 클래스 정의를 위해 호출된다.
        # mcs = metaclass
        print("__prepare__()")
        return super.__prepare__(mcs, *args, **kwarg)

    def __new__(mcs, *args, **kwargs):  # 클래스를 생성 할 때 호출됨
        # mcs = metaclass
        print("__new__()")
        return super().__new__(mcs, *args, **kwargs)

    def __init__(cls, *args, **kwargs):  # 클래스가 생성 된 후 호출됨
        print("__init__()")
        super().__init__(*args, **kwargs)

    def __call__(cls, *args, **kwargs):  # 클래스의 인스턴스를 생성할 때 호출됨
        print("__call__()")
        return super().__call__(*args, **kwargs)


class Test(metaclass=TestMeta):
    pass

# __prepare__()
# __new__()
# __init__()

t = Test()
# __call__()

위 코드를 보면 __prepare__, __new__, __init__, __call__ 메소드를 작성하고 사용하는 것을 볼 수 있다.

  • __prepare__ 메소드는 메타 클래스가 결정되었을 때 호출되며, 클래스의 네임 스페이스를 준비한다.
  • __new__ 메소드는 클래스 객체를 생성 할 때 호출된다.
  • __init__ 메소드는 클래스가 생성 된 후 호출되어 클래스를 초기화 한다.
  • __call__ 메소드는 클래스의 인스턴스를 생성 할 때 호출 된다.

이 매직 메서드를 가지고 무슨 일을 할 수 있을까?

싱글톤 패턴 구현

싱글톤 패턴은 클래스의 인스턴스화를 항상 하나의 개체로만 제한하는 설계 패턴이다.
구현해 보자면

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]


class SingletonClass(metaclass=SingletonMeta):
    pass

sl1 = SingletonClass()
sl2 = SingletonClass()

print(id(sl1))
# 1877212284048
print(id(sl2))
# 1877212284048

인스턴스 생성에 관여하는 __call__()메소드를 오버라이딩 해서 클래스를 key로 두고 인스턴스를 value로 만들어 클래스당 하나의 인스턴스를 가지도록 했다.

애트리뷰트 검증

DRF의 ModelSerializer 에는 내부 Meta 클래스가 없다면 오류를 일으킨다. 이에 대한 오류 검증을 DRF에서는 get_fields() 메소드에 구현해 두었는데, 이를 메타 클래스로 검증 할 수 있을 것 같다.

def get_fields(self):
    ...

    assert hasattr(self, 'Meta'), (
        'Class {serializer_class} missing "Meta" attribute'.format(
            serializer_class=self.__class__.__name__
        )
    )
    ...

위 코드는 클래스 내부에 Meta 애트리뷰트가 있는지 확인하는 코드이다. 이를 메타클래스로 검증하는 코드를 짜보자

class ModelSerializerMetaclass(SerializerMetaclass):
    def __new__(mcs, *args, **kwargs):
        name, bases, namespace = args
        if name not in ("ModelSerializer","HyperlinkedModelSerializer"):
            mcs._check_meta(name,namespace)

        return super().__new__(mcs, *args, **kwargs)

    def _check_meta(name,namespace):
        if not namespace.get("Meta", None):
            raise Exception(f'Class {name} missing "Meta" attribute')
        return


class ModelSerializer(Serializer, metaclass=ModelSerializerMetaclass):
    pass
import peewee

class MySQLModel(peewee.Model):
    @property
    @classmethod
    def unique_fields(cls) -> list:
        for field_name in cls._meta.fields.keys():
            field = getattr(cls, field_name)
            if field.unique == True or field.primary_key == True:
                yield field

peewee 모델의 unique한 field의 이름을 iterable한 객체로 반환하는 property 함수를 만들었다.

key in [field.name for field in MySQLModel.unique_fields]

그런데 위와 같이 사용 할 때 TypeError: 'property' object is not iterable 에러가 발생했다.

해결방법을 찾아보니 metaclass에서 property 함수를 만드는 방법, class decorator를 직접 만들어서 처리하는 방법 등이 있었다.

class decorator를 직접 만들어서 처리하는 방법은 아래와 같은데, 이게 마음에 들었다.


class classproperty(object):
    def __init__(self, function):
        self.function = function

    def __get__(self, owner_self, owner_cls):  # classproperty 객체에 접근할 때 inner_func 결과값을 반환하도록
        return self.function(owner_cls)


class MySQLModel(peewee.Model):
    @classproperty
    def unique_fields(cls) -> list:
        for field_name in cls._meta.fields.keys():
            field = getattr(cls, field_name)
            if field.unique == True or field.primary_key == True:
                yield field

하위 디렉토리의 모든 테스트를 실행하고 싶을 때가 있다.

 

그럴때는

$ python -m unittest discover

를 입력하면 하위 디렉토리모든 테스트를 실행한다. (기본적으로 "test*.py" 포맷의 파일을 찾아 실행한다.)

 

$ python -m unittest discover 
F.
======================================================================
FAIL: test_failure (test2.HelloTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\workspace\study\TIL\test2.py", line 9, in test_failure
    self.assertEqual(0,1)
AssertionError: 0 != 1

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

Django 로 개발을 하던 중 auth 라는 이름을 가진 Custom app을 INSTALLED_APP에 추가하니 오류가 발생했다.

기존의 "django.contrib.auth" 앱과 중복되었기 때문이다.

 

 

https://code.djangoproject.com/ticket/21562

 

#21562 (Bad things happen if you name your custom user app "auth") – Django

Bad things happen if you name your custom user app "auth" Reported by: Charlie DeTar Owned by: nobody Component: Documentation Version: dev Severity: Normal Keywords: Cc: Triage Stage: Accepted Has patch: yes Needs documentation: no Needs tests: no Patch n

code.djangoproject.com

새로운 이름으로는 accounts가 적절 할 것 같다.

이름 변경 후에 users 앱과 통합시켜야 겠다.

 

깃허브 링크

https://github.com/Doran-Doran-development/DoranDoran-Server-2

 

Doran-Doran-development/DoranDoran-Server-2

🧮 교내 실습실 예약 및 관리 웹 서비스 '도란도란'의 서버를 개발한 레포지토리 입니다. Contribute to Doran-Doran-development/DoranDoran-Server-2 development by creating an account on GitHub.

github.com

 

Django로 이메일 전송을 구현하고 있는데 send_mail 함수에서 위와 같은 오류가 났다.

 

위 오류의 내용은 send_mail의 매개변수 from_email을 안 넘겨 줬다는 소리다.

from django.core.mail import send_mail


send_mail(
	subject="Activate your DoranDoran account.",
	message="Please Activate your account http://localhost:8000",
	recipient_list=[user_instance.user.email],
	fail_silently=False,
)

코드는 위와 같았는데, from_email 이 없다고 오류가 난다.

그런데 django.core.mail.send_mail 의 소스코드를 보면 아래와 같이 설명되어 있다.

If from_email is None, user the DEFAULT_FROM_EMAIL setting.

45번째 줄을 보면 from_email 이 None이면 DEFAULT_FROM_EMAIL을 사용한다고 되어있다. 하지만 같이 써있는 auth_user는 기본값 None이 있는 반면, from_email을 기본값이 설정되어 있지 않다. 그래서 적어주지 않으니 오류가 발생하는 것이다.

 

결국 해결방법은 from_email을 적어주면 된다.

from django.core.mail import send_mail


send_mail(
	subject="Activate your DoranDoran account.",
	message="Please Activate your account http://localhost:8000",
	from_email=None,
	recipient_list=[user_instance.user.email],
	fail_silently=False,
)

이렇게 하면 해결되긴 하지만 django 라이브러리에 일관성이 없다고 생각이 든다... PR 보내봐야 겠다.

 

========================================

 

PR 보내고 봤더니 이미 이전에 있었던 이슈였다.

https://code.djangoproject.com/ticket/32633#comment:3

 

#32633 (send_mail must make `from_email` argument optional) – Django

the django docs state that, while sending mail, if no from_email is passed, it will use the DEFAULT_FROM_EMAIL. source: ​https://docs.djangoproject.com/en/3.1/topics/email/ from_email: A string. If None, Django will use the value of the DEFAULT_FROM_EMAI

code.djangoproject.com

네임스페이스란?

네임스페이스란 프로그래밍 언어에서 특정한 객체를 이름에 따라 구분할 수 있는 공간을 의미한다.

my_string = "asdf"



my_integer = 12



my_list = [1,2,3]

my_list2 = my_list

위 예시에서는 my_string"asdf"객체를 가리키고 있다.

위와 같이 이름과 객체를 연결한 것을네임스페이스 라고 한다.

왜 필요한데?

프로그래밍을 하다보면 모든 변수와 함수명을 겹치지 않도록 하는 것은 불가능 하다.

그렇기 때문에 특정한 이름의 변수 혹은 함수가 통용될 수 있는 범위를 제한하기 위해 네임스페이스가 등장한 것이다.

아래의 코드를 보자

class TestA:

    a = 1



class TestB:

    a = 2

a라는 변수이름이 중복되어 사용되고 있다. 만약 네임스페이스라는 개념이 없다면 원하는 a를 호출하기 힘들 것이다.

TestA 라는 로컬 네임스페이스에서 a를 호출하면 1의 값을 가진 변수 a를 얻을 수 있고,

TestB 라는 로컬 네임스페이스에서 a를 호출하면 2의 값을 가진 변수 a를 얻을 수 있게 된다.

Local, Global, Built-in Namespace

앞서 말했듯이 네임스페이스는 변수 혹은 함수가 통용될 수 있는 범위를 제한하기 위해 등장했다.

이런 네임스페이스는 Local, Global, Built-in 3가지로 분류할 수 있다.

  • Build-in
    기존 내장 함수 들의 이름이 소속된다. 파이썬으로 작성 된 모든 범위가 포함된다.

  • Global
    모듈별로 존재하며, 모듈 전체에서 통용될 수 있는 이름들이 소속된다.

  • Local
    함수 및 메서드 별로 존재하며, 함수 내의 지역 변수들의 이름들이 소속된다.

메타클래스에 대해 알아보기 이전에 파이썬의 데이터 모델에 대한 이해가 필요하다.

파이썬에서 모든 것은 데이터를 추상화 한 객체로 이루어져 있다.
또한, 파이썬의 객체는 아이덴티티, 값, 타입을 가지고 있다.

아이덴티티 (id)

id() 함수를 통해 얻을 수 있으며 객체의 수명동안 유일하고 불변함이 보장되는 정수다.

값 (value)

객체의 타입에 따라 불변할 수 있고 가변할 수도 있다. ex)tuple : 불변, list : 가변

타입 (type)

객체가 지원하는 연산들과 그 타입의 객체가 가질 수 있는 값(ex) int : 1, list : [1,2])들을 통해 객체의 특성을 정의한다. 객체의 타입은 type()을 통해 얻을 수 있으며, 불변하다.

여기서 말한 타입과 같이 파이썬의 모든 객체들은 어떠한 타입에 의해 정의된다.

파이썬의 type() 빌트인 함수를 사용하면 객체의 타입을 알 수 있다.

class Test:
    pass
t = Test()
type(t)
# <class '__main__.Test'>

def hello():
    pass
type(hello)
# <class 'function'>

type(1)
# <class 'int'>

위 예제를 보면 Test 클래스의 인스턴스인 tTest가 타입이고, hello 함수는 function이 타입이며, 정수 1은 int가 타입이다.

그렇다면, Test 클래스의 타입은 존재할까?

class Test:
    pass
type(Test)
# <class 'type'>

놀랍게도, Test 클래스 객체의 타입이 존재한다.
여기서 출력된 typeTest 클래스의 메타 클래스라고 하며, 인스턴스로 클래스를 가진다.

그러면 메타클래스는 무슨 용도로 사용하는 걸까?

그전에 몇가지 메타클래스의 매직 메소드에 대해 알아보자

class TestMeta(type):
    def __prepare__(mcs, *args, **kwarg): # 메타 클래스가 결정되었을 때 (mro가 구성된 후) 클래스 정의를 위해 호출된다.
        # mcs = metaclass
        print("__prepare__()")
        return super.__prepare__(mcs, *args, **kwarg)

    def __new__(mcs, *args, **kwargs):  # 클래스를 생성 할 때 호출됨
        # mcs = metaclass
        print("__new__()")
        return super().__new__(mcs, *args, **kwargs)

    def __init__(cls, *args, **kwargs):  # 클래스가 생성 된 후 호출됨
        print("__init__()")
        super().__init__(*args, **kwargs)

    def __call__(cls, *args, **kwargs):  # 클래스의 인스턴스를 생성할 때 호출됨
        print("__call__()")
        return super().__call__(*args, **kwargs)


class Test(metaclass=TestMeta):
    pass

# __prepare__()
# __new__()
# __init__()

t = Test()
# __call__()

위 코드를 보면 __prepare__, __new__, __init__, __call__ 메소드를 작성하고 사용하는 것을 볼 수 있다.

  • __prepare__ 메소드는 메타 클래스가 결정되었을 때 호출되며, 클래스의 네임 스페이스를 준비한다.
  • __new__ 메소드는 클래스 객체를 생성 할 때 호출된다.
  • __init__ 메소드는 클래스가 생성 된 후 호출되어 클래스를 초기화 한다.
  • __call__ 메소드는 클래스의 인스턴스를 생성 할 때 호출 된다.

이 매직 메서드를 가지고 무슨 일을 할 수 있을까?

싱글톤 패턴 구현

싱글톤 패턴은 클래스의 인스턴스화를 항상 하나의 개체로만 제한하는 설계 패턴이다.
구현해 보자면

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]


class SingletonClass(metaclass=SingletonMeta):
    pass

sl1 = SingletonClass()
sl2 = SingletonClass()

print(id(sl1))
# 1877212284048
print(id(sl2))
# 1877212284048

인스턴스 생성에 관여하는 __call__()메소드를 오버라이딩 해서 클래스를 key로 두고 인스턴스를 value로 만들어 클래스당 하나의 인스턴스를 가지도록 했다.

애트리뷰트 검증

DRF의 ModelSerializer 에는 내부 Meta 클래스가 없다면 오류를 일으킨다. 이에 대한 오류 검증을 DRF에서는 get_fields() 메소드에 구현해 두었는데, 이를 메타 클래스로 검증 할 수 있을 것 같다.

def get_fields(self):
    ...

    assert hasattr(self, 'Meta'), (
        'Class {serializer_class} missing "Meta" attribute'.format(
            serializer_class=self.__class__.__name__
        )
    )
    ...

위 코드는 클래스 내부에 Meta 애트리뷰트가 있는지 확인하는 코드이다. 이를 메타클래스로 검증하는 코드를 짜보자

class ModelSerializerMetaclass(SerializerMetaclass):
    def __new__(mcs, *args, **kwargs):
        name, bases, namespace = args
        if name not in ("ModelSerializer","HyperlinkedModelSerializer"):
            mcs._check_meta(name,namespace)

        return super().__new__(mcs, *args, **kwargs)

    def _check_meta(name,namespace):
        if not namespace.get("Meta", None):
            raise Exception(f'Class {name} missing "Meta" attribute')
        return


class ModelSerializer(Serializer, metaclass=ModelSerializerMetaclass):
    pass

https://github.com/encode/django-rest-framework/pull/7970

+ Recent posts