Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add file/image field #127

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
46 changes: 46 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
DateTime,
Decimal,
Email,
File,
Float,
Image,
Integer,
IPAddress,
Number,
Expand Down Expand Up @@ -889,3 +891,47 @@ 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") 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")

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)
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="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(
text="Did not match the image_types.", code="image_types"
)
50 changes: 48 additions & 2 deletions typesystem/fields.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import decimal
import imghdr
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typesystem support version <= 3.10

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3.11 releases just in one week. The typesystem will support it without any doubt.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional dependency lib is not cost-effective. Or do not check the image type.

such as : filetype OR puremagic in pypi

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be an optional dependency for those, who wants to use File fields.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe Image is unnecessary. Just define the File, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if File() can validate the file type. So we can specify that we want image, audio, video, etc.
Something like

File(accept=['image/jpeg', 'image/png'])

This should answer your question. But IMO, a dedicated Image() field is a nice utility.

import io
import re
import typing
from math import isfinite
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}'."}
Expand Down Expand Up @@ -790,3 +792,47 @@ 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.",
}
value_types = (
io.BufferedReader,
io.TextIOWrapper,
io.BufferedRandom,
io.BufferedWriter,
)

def __init__(self, **kwargs: typing.Any) -> None:
super().__init__(**kwargs)

def validate(self, value: typing.Any) -> typing.Any:
if not isinstance(value, self.value_types):
raise self.validation_error("type")
return value


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")
3 changes: 1 addition & 2 deletions typesystem/json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,15 +408,14 @@ 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

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)

Expand Down