Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion boltons/urlutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,8 @@ def navigate(self, dest):
if dest.path.startswith(u'/'): # absolute path
new_path_parts = list(dest.path_parts)
else: # relative path
new_path_parts = self.path_parts[:-1] + dest.path_parts
new_path_parts = list(self.path_parts[:-1]) \
+ list(dest.path_parts)
else:
new_path_parts = list(self.path_parts)
if not query_params:
Expand Down
17 changes: 17 additions & 0 deletions tests/test_urlutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,23 @@ def test_rel_navigate():
return


@pytest.mark.parametrize(
('expected', 'base', 'a', 'b'), [
('https://host/b', 'https://host', 'a', '/b'),
('https://host/a/b', 'https://host', 'a', 'b'),
Copy link
Contributor

Choose a reason for hiding this comment

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

Using

boltons.urlutils.URL(boltons.urlutils.URL('https://host').navigate('a')).navigate('b')

the expected result should be https://host/b

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I haven't looked at this in a long time, so pardon me if I'm completely backwards and no longer clear on what I thought behavior should be.

When you say, "the expected result should be", where are you basing that "should be" on? The scant documentation says, "Useful for navigating those relative links with ease." I consider this analogous to using cd on a Unix system; all references are relative unless the destination starts with /.

Actually, the vexing nature of the way things work now is more apparent if you have multiple path segments:

>>> boltons.urlutils.URL('https://host/a/b/c').navigate('d')
URL('https://host/a/b/d')
>>> boltons.urlutils.URL('https://host/a/b/c').navigate('../d')
URL('https://host/a/d')
>>> boltons.urlutils.URL('https://host/a/b/c/').navigate('../d')
URL('https://host/a/b/d')
>>> boltons.urlutils.URL('https://host/a/b/c').navigate('#d')
URL('https://host/a/b/c#d')

What I find most surprising is that, without a trailing slash on the base, navigate with a non-fragment argument assumes the last part of the path is (erm, not sure about the right terminology) a "leaf" instead of a "stem" (or "file" instead of a "directory"?).

To make a relative link from an URL without a trialing slash with the behavior you're describing, you have to: 1. to_text() 2. conditionally append a '/' (normalize() will not make // -> /) 3. recreate URL. Because the URL object is immutable, it's much easier to change the string argument to enable "upwards" navigation by pre-pending '../'. This is what I would find less surprising:

>>> boltons.urlutils.URL('https://host/a/b/c').navigate('d')
URL('https://host/a/b/c/d')
>>> boltons.urlutils.URL('https://host/a/b/c').navigate('../d')
URL('https://host/a/b/d')
>>> boltons.urlutils.URL('https://host/a/b/c/').navigate('../d')
URL('https://host/a/b/d')

Unfortunately, I think this ship has sailed; changing this behavior would be massively backwards-incompatible.

('https://host/a/b', 'https://host', 'a/', 'b'),
('https://host/a/b', 'https://host', '/a', 'b'),
Copy link
Contributor

Choose a reason for hiding this comment

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

Using

boltons.urlutils.URL(boltons.urlutils.URL('https://host').navigate('/a')).navigate('b')

the expected result should be https://host/b

('https://host/a/b', 'https://host/a/', None, 'b'),
('https://host/a/b', 'https://host/a', None, 'b'),
Copy link
Contributor

Choose a reason for hiding this comment

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

Using

boltons.urlutils.URL(boltons.urlutils.URL('https://host').navigate('/a')).navigate('b')

the expected result should be https://host/b

])
def test_chained_navigate(expected, base, a, b):
"""Chained :meth:`navigate` calls produces correct results."""
if a is not None:
assert expected == URL(base).navigate(a).navigate(b).to_text()
else:
assert expected == URL(base).navigate(b).to_text()


def test_navigate():
orig_text = u'http://a.b/c/d?e#f'
orig = URL(orig_text)
Expand Down