-
-
Notifications
You must be signed in to change notification settings - Fork 35.3k
bpo-46337: Urllib.parse scheme-specific behavior without reliance on URL scheme #30520
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
Changes from all commits
ee890ed
53c6ccc
41d3b58
eee880c
f9b59dd
1691a1e
c7ae936
4fc9059
c07600c
07a8576
2c4aa3a
5f81d16
226bbe9
ff88881
bf64df0
6a09c38
9d7cfb5
81d3414
677ed1a
0ec4a4e
b25e0e8
2123ad7
9f50dfb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -213,12 +213,12 @@ def _encode(t): | |
| split = (scheme,) + split | ||
| self.checkRoundtrips(url, parsed, split) | ||
|
|
||
| def checkJoin(self, base, relurl, expected): | ||
| def checkJoin(self, base, relurl, expected, flags=urllib.parse.SchemeFlag(0)): | ||
| str_components = (base, relurl, expected) | ||
| self.assertEqual(urllib.parse.urljoin(base, relurl), expected) | ||
| self.assertEqual(urllib.parse.urljoin(base, relurl, flags=flags), expected) | ||
| bytes_components = baseb, relurlb, expectedb = [ | ||
| x.encode('ascii') for x in str_components] | ||
| self.assertEqual(urllib.parse.urljoin(baseb, relurlb), expectedb) | ||
| self.assertEqual(urllib.parse.urljoin(baseb, relurlb, flags=flags), expectedb) | ||
|
|
||
| def test_unparse_parse(self): | ||
| str_cases = ['Python', './Python','x-newscheme://foo.com/stuff','x://y','x:/y','x:/','/',] | ||
|
|
@@ -417,6 +417,11 @@ def test_urljoins(self): | |
| self.checkJoin('svn+ssh://pathtorepo/dir1', 'dir2', 'svn+ssh://pathtorepo/dir2') | ||
| self.checkJoin('ws://a/b','g','ws://a/g') | ||
| self.checkJoin('wss://a/b','g','wss://a/g') | ||
| self.checkJoin( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make these new SchemeFlag specific test methods instead of appending to an existing long one. |
||
| 'nonsensebase://net.loc/url/', '..', | ||
| 'nonsensebase://net.loc/', | ||
| flags=(urllib.parse.SchemeFlag.RELATIVE | urllib.parse.SchemeFlag.NETLOC), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove the For those that tend to forget, "global" means "module" in Python.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add more test cases that explicitly cover PARAMS and UNIVERSAL behaviors. |
||
| ) | ||
|
|
||
| # XXX: The following tests are no longer compatible with RFC3986 | ||
| # self.checkJoin(SIMPLE_BASE, '../../../g','http://a/../g') | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,7 @@ | |
| """ | ||
|
|
||
| from collections import namedtuple | ||
| import enum | ||
| import functools | ||
| import re | ||
| import sys | ||
|
|
@@ -39,12 +40,35 @@ | |
| "parse_qsl", "quote", "quote_plus", "quote_from_bytes", | ||
| "unquote", "unquote_plus", "unquote_to_bytes", | ||
| "DefragResult", "ParseResult", "SplitResult", | ||
| "SchemeFlag", "RELATIVE", "NETLOC", "PARAMS", "UNIVERSAL", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lets not pollute
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ethanfurman thoughts? I remember you suggested that these should be exported as such for code like urlparse(uri_string, flags=UNIVERSAL)or similar. I'm fine either way, but do agree that the namespace would be cleaner were the flags not exported individually.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Putting them in |
||
| "DefragResultBytes", "ParseResultBytes", "SplitResultBytes"] | ||
|
|
||
| # A classification of schemes. | ||
| # The empty string classifies URLs with no scheme specified, | ||
| # being the default value returned by “urlsplit” and “urlparse”. | ||
|
|
||
| class SchemeFlag(enum.Flag): | ||
| """SchemeFlag is an enum with the members RELATIVE, NETLOC, and | ||
| PARAMS. These describe methods for URL resolution, usually by | ||
| scheme. These resolution classes determine, namely, whether a | ||
| scheme supports, respectively, relative addressing, preserving the | ||
| netloc (domain name), and preserving the parameters. | ||
| """ | ||
| RELATIVE = enum.auto() | ||
| NETLOC = enum.auto() | ||
| PARAMS = enum.auto() | ||
| UNIVERSAL = RELATIVE | NETLOC | PARAMS | ||
|
|
||
| def __repr__(self): | ||
| return f'{self.__module__}.{self._name_}' | ||
|
|
||
| __str__ = __repr__ | ||
|
|
||
| RELATIVE, NETLOC, PARAMS = SchemeFlag | ||
| # UNIVERSAL must be assigned separately as it's a combination of other variants. | ||
| UNIVERSAL = SchemeFlag.UNIVERSAL | ||
|
|
||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. change to: |
||
| uses_relative = ['', 'ftp', 'http', 'gopher', 'nntp', 'imap', | ||
| 'wais', 'file', 'https', 'shttp', 'mms', | ||
| 'prospero', 'rtsp', 'rtspu', 'sftp', | ||
|
|
@@ -60,6 +84,30 @@ | |
| 'https', 'shttp', 'rtsp', 'rtspu', 'sip', 'sips', | ||
| 'mms', 'sftp', 'tel'] | ||
|
|
||
|
|
||
| def _scheme_classes(scheme, overrides=None): | ||
| """Find out what scheme classes a given scheme fits in. | ||
|
|
||
| This consults the variables uses_relative, uses_netloc, and | ||
| uses_params. It returns a set of all the classes that apply, with | ||
| at least the unique classes specified by the optional overrides | ||
| parameter. | ||
| """ | ||
| if overrides is None: | ||
| overrides = SchemeFlag(0) | ||
|
|
||
| if scheme in uses_relative: | ||
| overrides |= RELATIVE | ||
|
|
||
| if scheme in uses_netloc: | ||
| overrides |= NETLOC | ||
|
|
||
| if scheme in uses_params: | ||
| overrides |= PARAMS | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove the three |
||
| return overrides | ||
|
|
||
|
|
||
| # These are not actually used anymore, but should stay for backwards | ||
| # compatibility. (They are undocumented, but have a public-looking name.) | ||
|
|
||
|
|
@@ -363,7 +411,7 @@ def _fix_result_transcoding(): | |
| _fix_result_transcoding() | ||
| del _fix_result_transcoding | ||
|
|
||
| def urlparse(url, scheme='', allow_fragments=True): | ||
| def urlparse(url, scheme='', allow_fragments=True, *, flags=None): | ||
| """Parse a URL into 6 components: | ||
| <scheme>://<netloc>/<path>;<params>?<query>#<fragment> | ||
|
|
||
|
|
@@ -386,7 +434,8 @@ def urlparse(url, scheme='', allow_fragments=True): | |
| url, scheme, _coerce_result = _coerce_args(url, scheme) | ||
| splitresult = urlsplit(url, scheme, allow_fragments) | ||
| scheme, netloc, url, query, fragment = splitresult | ||
| if scheme in uses_params and ';' in url: | ||
| scheme_classes = _scheme_classes(scheme, overrides=flags) | ||
| if PARAMS in scheme_classes and ';' in url: | ||
| url, params = _splitparams(url) | ||
| else: | ||
| params = '' | ||
|
|
@@ -500,7 +549,9 @@ def urlunsplit(components): | |
| empty query; the RFC states that these are equivalent).""" | ||
| scheme, netloc, url, query, fragment, _coerce_result = ( | ||
| _coerce_args(*components)) | ||
| if netloc or (scheme and scheme in uses_netloc and url[:2] != '//'): | ||
|
|
||
| scheme_classes = _scheme_classes(scheme) | ||
| if netloc or (scheme and (NETLOC in scheme_classes) and url[:2] != '//'): | ||
| if url and url[:1] != '/': url = '/' + url | ||
| url = '//' + (netloc or '') + url | ||
| if scheme: | ||
|
|
@@ -511,23 +562,26 @@ def urlunsplit(components): | |
| url = url + '#' + fragment | ||
| return _coerce_result(url) | ||
|
|
||
| def urljoin(base, url, allow_fragments=True): | ||
| def urljoin(base, url, allow_fragments=True, *, flags=None): | ||
| """Join a base URL and a possibly relative URL to form an absolute | ||
| interpretation of the latter.""" | ||
| interpretation of the latter. Some logic may be enabled by setting | ||
| the flags variable.""" | ||
|
ethanfurman marked this conversation as resolved.
|
||
| if not base: | ||
| return url | ||
| if not url: | ||
| return base | ||
|
|
||
| base, url, _coerce_result = _coerce_args(base, url) | ||
| bscheme, bnetloc, bpath, bparams, bquery, bfragment = \ | ||
| urlparse(base, '', allow_fragments) | ||
| urlparse(base, '', allow_fragments, flags=flags) | ||
| scheme, netloc, path, params, query, fragment = \ | ||
| urlparse(url, bscheme, allow_fragments) | ||
| urlparse(url, bscheme, allow_fragments, flags=flags) | ||
|
|
||
| if scheme != bscheme or scheme not in uses_relative: | ||
| scheme_classes = _scheme_classes(scheme, overrides=flags) | ||
|
|
||
| if scheme != bscheme or RELATIVE not in scheme_classes: | ||
| return _coerce_result(url) | ||
| if scheme in uses_netloc: | ||
| if NETLOC in scheme_classes: | ||
| if netloc: | ||
| return _coerce_result(urlunparse((scheme, netloc, path, | ||
| params, query, fragment))) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Expose scheme-specific URL options in ``urllib.parse.urljoin`` and ``urllib.parse.urlparse`` via the enum ``urllib.parse.SchemeFlag``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do not reformat doctest examples. these were formatted to be narrow to avoid horizontal scrollbars in documentation on most common displays and to keep the .rst itself <80 columns when possible.
reformatting is unrelated to the change at hand and distracts from the actual change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restoring the original formatting causes the doctest to fail, I should've broken that out into a separate and clear commit... I'm doctesting with
.python -m doctest. Is that wrong, or is there some other way I can keep the old linebreaks?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gpshead re
behavior_overridesvsflags: aren't flags usually behavior overrides?ssl,socket,_pydecimal,_osx_support, andreall useflags, whiledoctestusescompileflags,_pyiousedec_flags, andsubprocessusescreationflags.My first choice here would be a simple
flags, and it should be easily understood that the flags given will modify the parsing behavior ofurlparse. Would it be more precise to call ituri_flags? At any rate,behavior_overridesis no less generic and much more verbose thanflags.