From 7696b7f25277a8d203162ff09f90aea116fdbea7 Mon Sep 17 00:00:00 2001 From: huangsong Date: Thu, 3 Mar 2022 17:40:24 +0800 Subject: [PATCH 1/6] add file/image field --- tests/test_fields.py | 38 ++++++++++++++++++++++++++++++++ typesystem/fields.py | 52 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 3b0fb49..8cd7a61 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -16,7 +16,9 @@ DateTime, Decimal, Email, + File, Float, + Image, Integer, IPAddress, Number, @@ -889,3 +891,39 @@ def test_url(): validator = URL() value, error = validator.validate_or_error("example") assert error == ValidationError(text="Must be a real URL.", code="invalid") + + +def test_file(): + validator = File() + with open("test.txt", "w") as f: + f.write("123") + with open("test.txt", "r") as f: + value, error = validator.validate_or_error(f) + assert value == f + + validator = File() + value, error = validator.validate_or_error(None) + assert error == ValidationError(text="Must be a file descriptor.", code="type") + + +def test_image(): + validator = Image(image_types=["png"]) + with open("test.png", "wb") as f: + f.write(b"\211PNG\r\n\032\nxxxxxxxxxxxxxxxxxxxxxxxy") + + f = open("test.png", "rb") + value, error = validator.validate_or_error(f) + assert value == f + + validator = Image(image_types=["png"]) + value, error = validator.validate_or_error(None) + assert error == ValidationError(text="Must be a file descriptor.", code="type") + + with open("test.png", "wb") as f: + f.write(b"123") + + with open("test.png", "rb") as f: + value, error = validator.validate_or_error(f) + assert error == ValidationError( + text="Did not match the image_types.", code="image_types" + ) diff --git a/typesystem/fields.py b/typesystem/fields.py index 2d47d5a..61453ce 100644 --- a/typesystem/fields.py +++ b/typesystem/fields.py @@ -1,4 +1,6 @@ import decimal +import imghdr +import io import re import typing from math import isfinite @@ -385,7 +387,7 @@ def validate(self, value: typing.Any) -> typing.Any: return None elif value is None: raise self.validation_error("null") - elif value not in Uniqueness([key for key, value in self.choices]): + elif value not in Uniqueness([key for key, val in self.choices]): if value == "": if self.allow_null and self.coerce_types: return None @@ -749,7 +751,7 @@ def validate(self, value: typing.Any) -> typing.Any: class Const(Field): """ - Only ever matches the given given value. + Only ever matches the given value. """ errors = {"only_null": "Must be null.", "const": "Must be the value '{const}'."} @@ -790,3 +792,49 @@ def __init__(self, **kwargs: typing.Any) -> None: class URL(String): def __init__(self, **kwargs: typing.Any) -> None: super().__init__(format="url", **kwargs) + + +class File(Field): + errors = { + "type": "Must be a file descriptor.", + } + + def __init__( + self, *, serialize_func: typing.Callable = None, **kwargs: typing.Any + ) -> None: + super().__init__(**kwargs) + self.serialize_func = serialize_func + + def validate(self, value: typing.Any) -> typing.Any: + if not isinstance(value, io.BufferedReader): + raise self.validation_error("type") + return value + + def serialize(self, obj: typing.Any) -> typing.Any: + if self.serialize_func is not None and callable(self.serialize_func): + return self.serialize_func(obj) + return obj + + +class Image(File): + errors = { + "type": "Must be a file descriptor.", + "image_types": "Did not match the image_types", + "file_type": "Must be a image type.", + } + + def __init__( + self, image_types: typing.List[str] = None, **kwargs: typing.Any + ) -> None: + + super().__init__(**kwargs) + self.image_types = image_types + + def validate(self, value: typing.Any) -> typing.Any: + value = super().validate(value) + image_type: typing.Optional[str] = imghdr.what(value) + if image_type is None: + raise self.validation_error("file_type") + if self.image_types is not None and image_type in self.image_types: + return value + raise self.validation_error("image_types") From 0c54f98f1d44f145baa34cc4b1e5883fa483207a Mon Sep 17 00:00:00 2001 From: huangsong Date: Thu, 3 Mar 2022 17:57:21 +0800 Subject: [PATCH 2/6] fix test --- tests/test_fields.py | 16 ++++++++++++---- typesystem/fields.py | 10 ++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 8cd7a61..777d865 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -897,7 +897,7 @@ def test_file(): validator = File() with open("test.txt", "w") as f: f.write("123") - with open("test.txt", "r") as f: + with open("test.txt") as f: value, error = validator.validate_or_error(f) assert value == f @@ -911,9 +911,9 @@ def test_image(): with open("test.png", "wb") as f: f.write(b"\211PNG\r\n\032\nxxxxxxxxxxxxxxxxxxxxxxxy") - f = open("test.png", "rb") - value, error = validator.validate_or_error(f) - assert value == f + with open("test.png", "rb") as f: + value, error = validator.validate_or_error(f) + assert value == f validator = Image(image_types=["png"]) value, error = validator.validate_or_error(None) @@ -922,6 +922,14 @@ def test_image(): with open("test.png", "wb") as f: f.write(b"123") + with open("test.png", "rb") as f: + value, error = validator.validate_or_error(f) + assert error == ValidationError(text="Must be a image type.", code="file_type") + + validator = Image(image_types=["jpg"]) + with open("test.png", "wb") as f: + f.write(b"\211PNG\r\n\032\nxxxxxxxxxxxxxxxxxxxxxxxy") + with open("test.png", "rb") as f: value, error = validator.validate_or_error(f) assert error == ValidationError( diff --git a/typesystem/fields.py b/typesystem/fields.py index 61453ce..9584a9e 100644 --- a/typesystem/fields.py +++ b/typesystem/fields.py @@ -798,6 +798,12 @@ class File(Field): errors = { "type": "Must be a file descriptor.", } + value_types = ( + io.BufferedReader, + io.TextIOWrapper, + io.BufferedRandom, + io.BufferedWriter, + ) def __init__( self, *, serialize_func: typing.Callable = None, **kwargs: typing.Any @@ -806,7 +812,7 @@ def __init__( self.serialize_func = serialize_func def validate(self, value: typing.Any) -> typing.Any: - if not isinstance(value, io.BufferedReader): + if not isinstance(value, self.value_types): raise self.validation_error("type") return value @@ -819,7 +825,7 @@ def serialize(self, obj: typing.Any) -> typing.Any: class Image(File): errors = { "type": "Must be a file descriptor.", - "image_types": "Did not match the image_types", + "image_types": "Did not match the image_types.", "file_type": "Must be a image type.", } From a61d59e7bf9526a463a75f72d8f3ca42fe4e2b2b Mon Sep 17 00:00:00 2001 From: huangsong Date: Thu, 3 Mar 2022 18:02:01 +0800 Subject: [PATCH 3/6] fix coverage --- typesystem/fields.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/typesystem/fields.py b/typesystem/fields.py index 9584a9e..926d77a 100644 --- a/typesystem/fields.py +++ b/typesystem/fields.py @@ -805,22 +805,14 @@ class File(Field): io.BufferedWriter, ) - def __init__( - self, *, serialize_func: typing.Callable = None, **kwargs: typing.Any - ) -> None: + def __init__(self, **kwargs: typing.Any) -> None: super().__init__(**kwargs) - self.serialize_func = serialize_func def validate(self, value: typing.Any) -> typing.Any: if not isinstance(value, self.value_types): raise self.validation_error("type") return value - def serialize(self, obj: typing.Any) -> typing.Any: - if self.serialize_func is not None and callable(self.serialize_func): - return self.serialize_func(obj) - return obj - class Image(File): errors = { From b53e26e6e99b27e703326907afdf5caa7b2568cb Mon Sep 17 00:00:00 2001 From: huangsong Date: Wed, 28 Sep 2022 11:11:59 +0800 Subject: [PATCH 4/6] bugfix for local variable --- typesystem/json_schema.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/typesystem/json_schema.py b/typesystem/json_schema.py index b03135e..321b20f 100644 --- a/typesystem/json_schema.py +++ b/typesystem/json_schema.py @@ -408,7 +408,7 @@ def to_json_schema( elif isinstance(arg, NeverMatch): return False - field: typing.Optional[Field] + field: typing.Optional[Field] = None data: dict = {} is_root = _definitions is None definitions = {} if _definitions is None else _definitions @@ -416,7 +416,6 @@ def to_json_schema( if isinstance(arg, Field): field = arg elif isinstance(arg, Definitions): - field = None for key, value in arg.items(): definitions[key] = to_json_schema(value, _definitions=definitions) From c326ff7d18e60b3703b3aaac3be3be4797afa394 Mon Sep 17 00:00:00 2001 From: huangsong Date: Sun, 16 Oct 2022 19:06:24 +0800 Subject: [PATCH 5/6] use puremagic to check the image type --- typesystem/fields.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/typesystem/fields.py b/typesystem/fields.py index 926d77a..e86e4e8 100644 --- a/typesystem/fields.py +++ b/typesystem/fields.py @@ -1,5 +1,4 @@ import decimal -import imghdr import io import re import typing @@ -9,6 +8,13 @@ from typesystem.base import Message, ValidationError, ValidationResult from typesystem.unique import Uniqueness +try: + # check the image type. + import puremagic +except ImportError: + puremagic = None + + NO_DEFAULT = object() FORMATS = { @@ -830,7 +836,9 @@ def __init__( def validate(self, value: typing.Any) -> typing.Any: value = super().validate(value) - image_type: typing.Optional[str] = imghdr.what(value) + if puremagic is None: + return value + image_type: typing.Optional[str] = puremagic.from_stream(value) if image_type is None: raise self.validation_error("file_type") if self.image_types is not None and image_type in self.image_types: From 00fc837b909b26cfe90da0ed309e190b48837212 Mon Sep 17 00:00:00 2001 From: huangsong Date: Sun, 16 Oct 2022 20:13:05 +0800 Subject: [PATCH 6/6] add puremagic to check image type --- requirements.txt | 1 + setup.py | 1 + tests/image/head.jpeg | Bin 0 -> 17467 bytes tests/test_fields.py | 27 ++++++++++----------------- typesystem/fields.py | 22 +++++++++++++--------- 5 files changed, 25 insertions(+), 26 deletions(-) create mode 100644 tests/image/head.jpeg diff --git a/requirements.txt b/requirements.txt index 290cd98..62be83b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ # Optionals jinja2 pyyaml +puremagic # Packaging twine diff --git a/setup.py b/setup.py index ebc4b01..a852a74 100755 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ def get_packages(package): extras_require={ "jinja2": ["jinja2"], "pyyaml": ["pyyaml"], + "puremagic": ["puremagic"], }, python_requires='>=3.6', classifiers=[ diff --git a/tests/image/head.jpeg b/tests/image/head.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..aa8d72c0c4079b551476457a47a36dbb84e2bb97 GIT binary patch literal 17467 zcmeHuc|26#`}nmh6;i1zV+oa_lC3aEBq2*AF=YwaWesy%lq7p7Vk}9Pl6^O&Y$4gQ zjHT@RGGm6>?)bIyIX^E~G~=UhfV<2%4}URzfi zU}6G*b8rS2KY+_;{he+AfWAI(7ytlHfSGA0zyg<;;0%O10jxjE03Zop0e~fq=`Uy+ z^B+)VbQ;T_huI`q>Odg_)I!4PfWs+_IGm zuJDluU}Axvv9PkUa$*DyM<>$;~n$6b*a&E4t7a)k$M zj_vv2)p;iNRo@WD7QJ}=`x zKEQWt``#JXF}qQ+W$V8E2M--qQa*J~_q_RKiz^O}PB;Dh18zSIeH8XMIwA4d^Q5$_ zSJ|&~a^IDfl~+_&z5mqE*woze`D;(_x4!;?!S553Q`0lEbA&Y_X?=rC*`yNY?r%iG z|3B&6B!ZRgFDiKfW@ea97G@SUW;SM4)=(yxM=U%$MOj6TYw)oh6FXtFOPyU#{JQ3D zPvet!FCK_VKU27e!$#g@rBmYC2dy7otNizD6+9%*n#M|HbivHq`;!hum=J9pn3PLq zo5a}J-`J$*mbZ`ZCQWzlhdlU=uDGZ1zmb;t@^|8jic5Z{um0m76n6hX;@J4_MB@LX z5(k_C*ng(2?Ce{2Y~8wb$Icya+R49jGw}=Z|4f3vC$XPNYBL@AGcn*!m^e5%xVCcf zZQaTzgV>Ld`5!07Ah3<)C;NC|? z=6c^u`M3Sp1mP6k#k8*=$A(6TGzrws{yR`cTvRh9VG789JS<$pU{9IrPiKuEpu zDs{gW9|RiUyy|df|0J8B$&!;ZDbwE3@oY`B{WgJu7%Ty&|m8*u

001KI^-Y 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 typing.Any: class Image(File): errors = { "type": "Must be a file descriptor.", - "image_types": "Did not match the image_types.", - "file_type": "Must be a image type.", + "image_types": "Do not support this image type.", } def __init__( @@ -836,11 +835,16 @@ def __init__( def validate(self, value: typing.Any) -> typing.Any: value = super().validate(value) - if puremagic is None: + if self.image_types is None: return value - image_type: typing.Optional[str] = puremagic.from_stream(value) - if image_type is None: - raise self.validation_error("file_type") - if self.image_types is not None and image_type in self.image_types: - return value - raise self.validation_error("image_types") + + assert puremagic is not None, "'puremagic' must be installed." + try: + image_type: typing.Optional[str] = puremagic.from_stream(value) + except Exception: + image_type = None + + image_type = (image_type or "").strip(".") + if not image_type or image_type not in self.image_types: + raise self.validation_error("image_types") + return value