Skip to content

Commit

Permalink
Url Type (#317)
Browse files Browse the repository at this point in the history
* working on Url type

* Url type

* tweak Cargo

* allow Url as input to Url

* international_domain -> punycode_domain

* add unicode_host and unicode_string
  • Loading branch information
samuelcolvin committed Nov 2, 2022
1 parent 9028b4f commit f23c6a7
Show file tree
Hide file tree
Showing 17 changed files with 756 additions and 15 deletions.
70 changes: 69 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pydantic-core"
version = "0.6.0"
version = "0.6.1"
edition = "2021"
license = "MIT"
homepage = "https://github.com/pydantic/pydantic-core"
Expand Down Expand Up @@ -38,6 +38,9 @@ mimalloc = { version = "0.1.30", default-features = false, optional = true }
speedate = "0.7.0"
ahash = "0.8.0"
nohash-hasher = "0.2.0"
url = "2.3.1"
# idna is already required by url, added here to be explicit
idna = "0.3.0"

[lib]
name = "_pydantic_core"
Expand Down
2 changes: 2 additions & 0 deletions pydantic_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
PydanticOmit,
SchemaError,
SchemaValidator,
Url,
ValidationError,
__version__,
)
Expand All @@ -14,6 +15,7 @@
'CoreConfig',
'CoreSchema',
'SchemaValidator',
'Url',
'SchemaError',
'ValidationError',
'PydanticCustomError',
Expand Down
21 changes: 20 additions & 1 deletion pydantic_core/_pydantic_core.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import decimal
import sys
from typing import Any, TypedDict
from typing import Any, Literal, TypedDict

from pydantic_core.core_schema import CoreConfig, CoreSchema, ErrorType

Expand All @@ -13,6 +13,7 @@ __all__ = (
'__version__',
'build_profile',
'SchemaValidator',
'Url',
'SchemaError',
'ValidationError',
'PydanticCustomError',
Expand All @@ -38,6 +39,24 @@ class SchemaValidator:
self, field: str, input: Any, data: 'dict[str, Any]', strict: 'bool | None' = None, context: Any = None
) -> 'dict[str, Any]': ...

class Url:
scheme: str
username: 'str | None'
password: 'str | None'
host: 'str | None'
host_type: Literal['domain', 'punycode_domain', 'ipv4', 'ipv6', None]
port: 'int | None'
path: 'str | None'
query: 'str | None'
fragment: 'str | None'

def __init__(self, raw_url: str) -> None: ...
def unicode_host(self) -> 'str | None': ...
def query_params(self) -> 'list[tuple[str, str]]': ...
def unicode_string(self) -> str: ...
def __str__(self) -> str: ...
def __repr__(self) -> str: ...

class SchemaError(Exception):
pass

Expand Down
32 changes: 32 additions & 0 deletions pydantic_core/core_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,33 @@ def json_schema(schema: CoreSchema | None = None, *, ref: str | None = None, ext
return dict_not_none(type='json', schema=schema, ref=ref, extra=extra)


class UrlSchema(TypedDict, total=False):
type: Required[Literal['url']]
host_required: bool # default False
max_length: int
allowed_schemes: List[str]
ref: str
extra: Any


def url_schema(
*,
host_required: bool | None = None,
max_length: int | None = None,
allowed_schemes: list[str] | None = None,
ref: str | None = None,
extra: Any = None,
) -> UrlSchema:
return dict_not_none(
type='url',
host_required=host_required,
max_length=max_length,
allowed_schemes=allowed_schemes,
ref=ref,
extra=extra,
)


CoreSchema = Union[
AnySchema,
NoneSchema,
Expand Down Expand Up @@ -1059,6 +1086,7 @@ def json_schema(schema: CoreSchema | None = None, *, ref: str | None = None, ext
RecursiveReferenceSchema,
CustomErrorSchema,
JsonSchema,
UrlSchema,
]

# used in _pydantic_core.pyi::PydanticKnownError
Expand Down Expand Up @@ -1139,4 +1167,8 @@ def json_schema(schema: CoreSchema | None = None, *, ref: str | None = None, ext
'unexpected_positional_argument',
'missing_positional_argument',
'multiple_argument_values',
'url_error',
'url_too_long',
'url_schema',
'url_host_required',
]
25 changes: 25 additions & 0 deletions src/errors/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,22 @@ pub enum ErrorType {
MissingPositionalArgument,
#[strum(message = "Got multiple values for argument")]
MultipleArgumentValues,
// ---------------------
// URL errors
#[strum(message = "Invalid URL, {error}")]
UrlError {
error: String,
},
#[strum(message = "URL should have at most {max_length} characters")]
UrlTooLong {
max_length: usize,
},
#[strum(message = "URL schema should be {expected_schemas}")]
UrlSchema {
expected_schemas: String,
},
#[strum(message = "URL host required")]
UrlHostRequired,
}

macro_rules! render {
Expand Down Expand Up @@ -449,6 +465,9 @@ impl ErrorType {
expected_tags: String
),
Self::UnionTagNotFound { .. } => extract_context!(UnionTagNotFound, ctx, discriminator: String),
Self::UrlError { .. } => extract_context!(UrlError, ctx, error: String),
Self::UrlTooLong { .. } => extract_context!(UrlTooLong, ctx, max_length: usize),
Self::UrlSchema { .. } => extract_context!(UrlSchema, ctx, expected_schemas: String),
_ => {
if ctx.is_some() {
py_err!(PyTypeError; "'{}' errors do not require context", value)
Expand Down Expand Up @@ -536,6 +555,9 @@ impl ErrorType {
expected_tags,
} => render!(self, discriminator, tag, expected_tags),
Self::UnionTagNotFound { discriminator } => render!(self, discriminator),
Self::UrlError { error } => render!(self, error),
Self::UrlTooLong { max_length } => to_string_render!(self, max_length),
Self::UrlSchema { expected_schemas } => render!(self, expected_schemas),
_ => Ok(self.message_template().to_string()),
}
}
Expand Down Expand Up @@ -585,6 +607,9 @@ impl ErrorType {
expected_tags,
} => py_dict!(py, discriminator, tag, expected_tags),
Self::UnionTagNotFound { discriminator } => py_dict!(py, discriminator),
Self::UrlError { error } => py_dict!(py, error),
Self::UrlTooLong { max_length } => py_dict!(py, max_length),
Self::UrlSchema { expected_schemas } => py_dict!(py, expected_schemas),
_ => Ok(None),
}
}
Expand Down
1 change: 1 addition & 0 deletions src/errors/validation_exception.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ impl ValidationError {
}

fn errors(&self, py: Python, include_context: Option<bool>) -> PyResult<PyObject> {
// TODO remove `collect` when we have https://github.com/PyO3/pyo3/pull/2676
Ok(self
.line_errors
.iter()
Expand Down
5 changes: 5 additions & 0 deletions src/input/input_abstract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use pyo3::prelude::*;
use pyo3::types::{PyString, PyType};

use crate::errors::{InputValue, LocItem, ValResult};
use crate::PyUrl;

use super::datetime::{EitherDate, EitherDateTime, EitherTime, EitherTimedelta};
use super::return_enums::{EitherBytes, EitherString};
Expand Down Expand Up @@ -54,6 +55,10 @@ pub trait Input<'a>: fmt::Debug + ToPyObject {
Ok(false)
}

fn input_as_url(&self) -> Option<PyUrl> {
None
}

fn callable(&self) -> bool {
false
}
Expand Down
16 changes: 10 additions & 6 deletions src/input/input_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use pyo3::types::{PyDictItems, PyDictKeys, PyDictValues};
use pyo3::{ffi, intern, AsPyPointer, PyTypeInfo};

use crate::errors::{py_err_string, ErrorType, InputValue, LocItem, ValError, ValLineError, ValResult};
use crate::PyUrl;

use super::datetime::{
bytes_as_date, bytes_as_datetime, bytes_as_time, bytes_as_timedelta, date_as_datetime, float_as_datetime,
Expand Down Expand Up @@ -102,13 +103,16 @@ impl<'a> Input<'a> for PyAny {
}

fn input_is_subclass(&self, class: &PyType) -> PyResult<bool> {
if let Ok(py_type) = self.cast_as::<PyType>() {
py_type.is_subclass(class)
} else {
Ok(false)
match self.cast_as::<PyType>() {
Ok(py_type) => py_type.is_subclass(class),
Err(_) => Ok(false),
}
}

fn input_as_url(&self) -> Option<PyUrl> {
self.extract::<PyUrl>().ok()
}

fn callable(&self) -> bool {
self.is_callable()
}
Expand All @@ -124,7 +128,7 @@ impl<'a> Input<'a> for PyAny {
} else if args.is_none() {
Ok(None)
} else if let Ok(list) = args.cast_as::<PyList>() {
// remove `collect` when we have https://github.com/PyO3/pyo3/pull/2676
// TODO remove `collect` when we have https://github.com/PyO3/pyo3/pull/2676
Ok(Some(PyTuple::new(self.py(), list.iter().collect::<Vec<_>>())))
} else {
Err(ValLineError::new_with_loc(
Expand Down Expand Up @@ -161,7 +165,7 @@ impl<'a> Input<'a> for PyAny {
} else if let Ok(tuple) = self.cast_as::<PyTuple>() {
Ok(PyArgs::new(Some(tuple), None).into())
} else if let Ok(list) = self.cast_as::<PyList>() {
// remove `collect` when we have https://github.com/PyO3/pyo3/pull/2676
// TODO remove `collect` when we have https://github.com/PyO3/pyo3/pull/2676
let tuple = PyTuple::new(self.py(), list.iter().collect::<Vec<_>>());
Ok(PyArgs::new(Some(tuple), None).into())
} else {
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ mod input;
mod lookup_key;
mod questions;
mod recursion_guard;
mod url;
mod validators;

// required for benchmarks
pub use self::url::PyUrl;
pub use build_tools::SchemaError;
pub use errors::{list_all_errors, PydanticCustomError, PydanticKnownError, PydanticOmit, ValidationError};
pub use validators::SchemaValidator;
Expand All @@ -41,6 +43,7 @@ fn _pydantic_core(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<PydanticCustomError>()?;
m.add_class::<PydanticKnownError>()?;
m.add_class::<PydanticOmit>()?;
m.add_class::<self::url::PyUrl>()?;
m.add_function(wrap_pyfunction!(list_all_errors, m)?)?;
Ok(())
}

0 comments on commit f23c6a7

Please sign in to comment.