Skip to content

Commit

Permalink
Add autogenerated dunder methods to Tag
Browse files Browse the repository at this point in the history
Rename original Tag to TagRaw
Fix all of testing stuff for it
Use ruff on staged changes, ignore unstaged ones
  • Loading branch information
dhvcc committed May 28, 2023
1 parent c88388c commit c436ce4
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 59 deletions.
4 changes: 1 addition & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ repos:
- run
- ruff
- check
- .
language: system
always_run: true
pass_filenames: false
types: [ python ]
stages: [ commit, push ]
4 changes: 0 additions & 4 deletions rss_parser/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
# TODO: Possibly bundle as deb/rpm/exe

# >>>> MVP
# FIXME: doesn't parse items on https://rss.art19.com/apology-line
# Related. Provide a way to auto populate nested models. May be a custom class, like TagList or a custom validator
#
# TODO: Arithmetic operators
# TODO: class based approach, use classmethods and class attributes
# TODO: Also add dynamic class generator with config.
# Parser.with_config which returns new class and also supports context managers
Expand Down
6 changes: 5 additions & 1 deletion rss_parser/models/types/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
from email.utils import parsedate_to_datetime
from typing import Union

from rss_parser.models.types.tag import Tag

DatetimeOrStr = Union[str, datetime]


def validate_dt_or_str(value: str) -> DatetimeOrStr:
def validate_dt_or_str(value: Union[str, Tag]) -> DatetimeOrStr:
if hasattr(value, "content"):
value = value.content
# Try to parse standard (RFC 822)
try:
return parsedate_to_datetime(value)
Expand Down
112 changes: 64 additions & 48 deletions rss_parser/models/types/tag.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import warnings
from copy import deepcopy
from typing import TYPE_CHECKING, Generic, TypeVar, Union
from math import ceil, floor, trunc
from operator import add, eq, floordiv, ge, gt, index, invert, le, lt, mod, mul, ne, neg, pos, pow, sub, truediv
from typing import TYPE_CHECKING, Generic, Type, TypeVar, Union

from pydantic import validator
from pydantic import create_model, validator
from pydantic.generics import GenericModel

T = TypeVar("T")
Expand All @@ -12,7 +15,7 @@ class TagData(GenericModel, Generic[T]):
attributes: dict


class Tag(GenericModel, Generic[T]):
class TagRaw(GenericModel, Generic[T]):
"""
Class to represent XML tag.
Expand Down Expand Up @@ -58,48 +61,61 @@ def validate_attributes(cls, v: Union[T, dict], values, **kwargs): # noqa
return {"content": v, "attributes": {}}

def __getattr__(self, item):
"""Forward attribute lookup to the actual element which is stored in self.__root__."""
# Not using super, since there's no getattr in any of the parents
try:
return getattr(self.__root__, item)
except AttributeError:
return getattr(self.__root__.content, item)

# Conversions

def __str__(self):
return str(self.content)

def __int__(self):
return int(self.content)

def __float__(self):
return float(self.content)

def __bool__(self):
return bool(self.content)

# Comparion operators

def __eq__(self, other):
return self.content == other

def __ne__(self, other) -> bool:
return self.content != other

def __gt__(self, other):
return self.content > other

def __ge__(self, other):
return self.content >= other

def __lt__(self, other):
return self.content < other

def __le__(self, other):
return self.content <= other

# Arithmetic operators

def __add__(self, other):
return self.content + other
"""Optionally forward attribute lookup to the actual element which is stored in self.__root__."""
return getattr(self.__root__, item)


_OPERATOR_MAPPING = {
# Unary
"__pos__": pos,
"__neg__": neg,
"__abs__": abs,
"__invert__": invert,
"__round__": round,
"__floor__": floor,
"__ceil__": ceil,
# Conversion
"__str__": str,
"__int__": int,
"__float__": float,
"__bool__": bool,
"__complex__": complex,
"__oct__": oct,
"__hex__": hex,
"__index__": index,
"__trunc__": trunc,
# Comparison
"__lt__": lt,
"__gt__": gt,
"__le__": le,
"__eq__": eq,
"__ne__": ne,
"__ge__": ge,
# Arithmetic
"__add__": add,
"__sub__": sub,
"__mul__": mul,
"__truediv__": truediv,
"__floordiv__": floordiv,
"__mod__": mod,
"__pow__": pow,
}


def _make_proxy_operator(operator):
def f(self, *args):
return operator(self.content, *args)

f.__name__ = operator.__name__

return f


with warnings.catch_warnings():
# Ignoring pydantic's warnings when inserting dunder methods (this is not a field so we don't care)
warnings.filterwarnings("ignore", message="fields may not start with an underscore")
Tag: Type[TagRaw] = create_model(
"Tag",
__base__=(TagRaw, Generic[T]),
**{method: _make_proxy_operator(operator) for method, operator in _OPERATOR_MAPPING.items()},
)
54 changes: 51 additions & 3 deletions tests/test_tag.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Unit tests in addition to doctests present."""
from datetime import datetime, timedelta
from operator import eq, ge, gt, le, lt, ne
from operator import add, eq, floordiv, ge, gt, le, lt, mod, mul, ne, pow, sub, truediv
from random import randint
from typing import Optional

Expand All @@ -20,6 +20,14 @@ def rand_str():
return "string"


def rand_dt():
return datetime(randint(1, 2023), randint(1, 12), randint(1, 28))


def rand_td():
return timedelta(randint(1, 1000))


def test_comparison_operators_number():
number = randint(0, 2**32)
operator_result_list = [
Expand Down Expand Up @@ -137,8 +145,8 @@ def add_to_last_char(s, n):


def test_comparison_operators_datetime():
dt = datetime(randint(1, 2023), randint(1, 12), randint(1, 28))
delta = timedelta(randint(1, 1000))
dt = rand_dt()
delta = rand_td()
operator_result_list = [
[eq, dt, True],
[eq, dt + delta, False],
Expand Down Expand Up @@ -168,3 +176,43 @@ def test_comparison_operators_datetime():

for operator, b_operand, expected in operator_result_list:
assert operator(obj.datetime, b_operand) is expected


def test_arithmetic_operators_number():
number = randint(0, 2**32)
b_operand = randint(0, 2**16)
operator_list = [add, sub, mul, truediv, floordiv, mod, pow]
obj = Model(number=number)

for operator in operator_list:
assert operator(obj.number, b_operand) == operator(number, b_operand)


def test_arithmetic_operators_float():
number = randint(0, 2**8) / 100
b_operand = randint(0, 2**8) / 100
operator_list = [add, sub, mul, truediv, floordiv, mod, pow]
obj = Model(floatNumber=number)

for operator in operator_list:
assert operator(obj.float_number, b_operand) == operator(number, b_operand)


def test_arithmetic_operators_string_mul():
string = rand_str()
int_operand = randint(0, 2**16)
string_operand = rand_str()
obj = Model(string=string)

assert mul(obj.string, int_operand) == mul(string, int_operand)
assert add(obj.string, string_operand) == add(string, string_operand)


def test_arithmetic_operators_datetime():
dt = rand_dt()
td_operand = rand_td()
operator_list = [add, sub]
obj = Model(datetime=dt)

for operator in operator_list:
assert operator(obj.datetime, td_operand) == operator(dt, td_operand)

0 comments on commit c436ce4

Please sign in to comment.