diff --git a/changes/4470-samuelcolvin.md b/changes/4470-samuelcolvin.md new file mode 100644 index 0000000000..a22a152a1c --- /dev/null +++ b/changes/4470-samuelcolvin.md @@ -0,0 +1 @@ +**Revert Change:** Revert percent encoding of URL parts which was originally added in #4224. diff --git a/docs/examples/types_url_building.py b/docs/examples/types_url_building.py deleted file mode 100644 index 7d8d67b2ba..0000000000 --- a/docs/examples/types_url_building.py +++ /dev/null @@ -1,10 +0,0 @@ -from pydantic import AnyUrl, stricturl - -url = AnyUrl.build(scheme='https', host='example.com', query='query=my query') -print(url) - -my_url_builder = stricturl(quote_plus=True) -url = my_url_builder.build( - scheme='http', host='example.com', query='query=my query' -) -print(url) diff --git a/docs/usage/types.md b/docs/usage/types.md index df8bec6343..27109f281f 100644 --- a/docs/usage/types.md +++ b/docs/usage/types.md @@ -616,7 +616,11 @@ For URI/URL validation the following types are available: - `tld_required: bool = True` - `host_required: bool = True` - `allowed_schemes: Optional[Set[str]] = None` - - `quote_plus: bool = False` + +!!! warning + In V1.10.0 and v1.10.1 `stricturl` also took an optional `quote_plus` argument and URL components were percent + encoded in some cases. This feature was removed in v1.10.2, see + [#4470](https://github.com/pydantic/pydantic/pull/4470) for explanation and more details. The above types (which all inherit from `AnyUrl`) will attempt to give descriptive errors when invalid URLs are provided: @@ -679,20 +683,6 @@ If further validation is required, these properties can be used by validators to Also, Chrome, Firefox, and Safari all currently accept `http://exam_ple.com` as a URL, so we're in good (or at least big) company. -#### Building URLs - -You can build URLs from separate [URL Properties](#url-properties) using the `build` method in -[Pydantic URL types](#urls) or any type that inherits from them. - -By default, *pydantic* percent encodes the following URL properties: `user`, `password`, `path`, `query` -as per [RFC 3986](https://www.ietf.org/rfc/rfc3986.txt) without replacing spaces with `+` but this can -be changed using the `stricturl` method: - -!!! note - Percent encoding was added in V1.10 - -{!.tmp_examples/types_url_building.md!} - ### Color Type You can use the `Color` data type for storing colors as per diff --git a/pydantic/networks.py b/pydantic/networks.py index 725e4f8a27..c7d97186b9 100644 --- a/pydantic/networks.py +++ b/pydantic/networks.py @@ -26,7 +26,6 @@ cast, no_type_check, ) -from urllib.parse import quote, quote_plus from . import errors from .utils import Representation, update_not_none @@ -178,7 +177,6 @@ class AnyUrl(str): user_required: bool = False host_required: bool = True hidden_parts: Set[str] = set() - quote_plus: bool = False __slots__ = ('scheme', 'user', 'password', 'host', 'tld', 'host_type', 'port', 'path', 'query', 'fragment') @@ -241,19 +239,18 @@ def build( url = scheme + '://' if user: - url += cls.quote(user) + url += user if password: - url += ':' + cls.quote(password) + url += ':' + password if user or password: url += '@' url += host if port and ('port' not in cls.hidden_parts or cls.get_default_parts(parts).get('port') != port): url += ':' + port if path: - url += '/'.join(map(cls.quote, path.split('/'))) + url += path if query: - queries = query.split('&') - url += '?' + '&'.join(map(lambda s: '='.join(map(cls.quote, s.split('='))), queries)) + url += '?' + query if fragment: url += '#' + fragment return url @@ -394,10 +391,6 @@ def apply_default_parts(cls, parts: 'Parts') -> 'Parts': parts[key] = value # type: ignore[literal-required] return parts - @classmethod - def quote(cls, string: str, safe: str = '') -> str: - return quote_plus(string, safe) if cls.quote_plus else quote(string, safe) - def __repr__(self) -> str: extra = ', '.join(f'{n}={getattr(self, n)!r}' for n in self.__slots__ if getattr(self, n) is not None) return f'{self.__class__.__name__}({super().__repr__()}, {extra})' @@ -565,7 +558,6 @@ def stricturl( tld_required: bool = True, host_required: bool = True, allowed_schemes: Optional[Collection[str]] = None, - quote_plus: bool = False, ) -> Type[AnyUrl]: # use kwargs then define conf in a dict to aid with IDE type hinting namespace = dict( @@ -575,7 +567,6 @@ def stricturl( tld_required=tld_required, host_required=host_required, allowed_schemes=allowed_schemes, - quote_plus=quote_plus, ) return type('UrlValue', (AnyUrl,), namespace) diff --git a/tests/test_networks.py b/tests/test_networks.py index 95af277b35..2f4ed2dfe6 100644 --- a/tests/test_networks.py +++ b/tests/test_networks.py @@ -679,31 +679,12 @@ class Model2(BaseModel): (dict(scheme='ws', user='foo', password='x', host='example.net'), 'ws://foo:x@example.net'), (dict(scheme='ws', host='example.net', query='a=b', fragment='c=d'), 'ws://example.net?a=b#c=d'), (dict(scheme='http', host='example.net', port='1234'), 'http://example.net:1234'), - (dict(scheme='http', user='foo@bar', host='example.net'), 'http://foo%40bar@example.net'), - (dict(scheme='http', user='foo', password='a b', host='example.net'), 'http://foo:a%20b@example.net'), - (dict(scheme='http', host='example.net', query='q=foo bar'), 'http://example.net?q=foo%20bar'), - (dict(scheme='http', host='example.net', path="/m&m's"), 'http://example.net/m%26m%27s'), ], ) def test_build_url(kwargs, expected): assert AnyUrl(None, **kwargs) == expected -@pytest.mark.parametrize( - 'kwargs,expected', - [ - (dict(scheme='https', host='example.com', query='query=my query'), 'https://example.com?query=my+query'), - ( - dict(scheme='https', host='example.com', user='my name', password='a password'), - 'https://my+name:a+password@example.com', - ), - (dict(scheme='https', host='example.com', path='/this is a path'), 'https://example.com/this+is+a+path'), - ], -) -def test_build_url_quote_plus(kwargs, expected): - assert stricturl(quote_plus=True).build(**kwargs) == expected - - @pytest.mark.parametrize( 'kwargs,expected', [