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

Introduce absolute cached property #1100

Merged
merged 6 commits into from
Sep 4, 2024
Merged
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
1 change: 1 addition & 0 deletions CHANGES/1100.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added :attr:`~yarl.URL.absolute` which is now preferred over ``URL.is_absolute()`` -- by :user:`bdraco`.
14 changes: 9 additions & 5 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ The module supports both absolute and relative URLs.
Absolute URL should start from either *scheme* or ``'//'``.


.. method:: URL.is_absolute()
.. attribute:: URL.absolute

A check for absolute URLs.

Expand All @@ -479,15 +479,19 @@ Absolute URL should start from either *scheme* or ``'//'``.

.. doctest::

>>> URL('http://example.com').is_absolute()
>>> URL('http://example.com').absolute
True
>>> URL('//example.com').is_absolute()
>>> URL('//example.com').absolute
True
>>> URL('/path/to').is_absolute()
>>> URL('/path/to').absolute
False
>>> URL('path').is_absolute()
>>> URL('path').absolute
False

.. versionchanged:: 1.9.10

The :attr:`~yarl.URL.absolute` property is preferred over the ``is_absolute()`` method.


New URL generation
------------------
Expand Down
6 changes: 6 additions & 0 deletions tests/test_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -1307,26 +1307,31 @@ def test_with_suffix_replace():
def test_is_absolute_for_relative_url():
url = URL("/path/to")
assert not url.is_absolute()
assert not url.absolute
Copy link
Member

Choose a reason for hiding this comment

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

@bdraco cached things are usually tested through their __wrapped__ attributes to prevent caching from influencing behavior in tests. Have you considered doing something like this?

Copy link
Member Author

Choose a reason for hiding this comment

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

Since the URL object is immutable and the state is already predefined we shouldn’t have much risk of the cached value being calculated incorrectly.

Was there a specific case you are concerned about?

Copy link
Member

Choose a reason for hiding this comment

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

No, just got surprised seeing this and thought I'd mention it.



def test_is_absolute_for_absolute_url():
url = URL("http://example.com")
assert url.is_absolute()
assert url.absolute


def test_is_non_absolute_for_empty_url():
url = URL()
assert not url.is_absolute()
assert not url.absolute


def test_is_non_absolute_for_empty_url2():
url = URL("")
assert not url.is_absolute()
assert not url.absolute


def test_is_absolute_path_starting_from_double_slash():
url = URL("//www.python.org")
assert url.is_absolute()
assert url.absolute


# is_default_port
Expand Down Expand Up @@ -1759,6 +1764,7 @@ def test_relative_is_relative():
url = URL("http://user:pass@example.com:8080/path?a=b#frag")
rel = url.relative()
assert not rel.is_absolute()
assert not rel.absolute


def test_relative_abs_parts_are_removed():
Expand Down
52 changes: 34 additions & 18 deletions yarl/_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ def __init_subclass__(cls):

def __str__(self) -> str:
val = self._val
if not val.path and self.is_absolute() and (val.query or val.fragment):
if not val.path and self.absolute and (val.query or val.fragment):
val = val._replace(path="/")
if (port := self._port_not_default) is None:
# port normalization - using None for default ports to remove from rendering
Expand All @@ -330,11 +330,11 @@ def __eq__(self, other: object) -> bool:
return NotImplemented

val1 = self._val
if not val1.path and self.is_absolute():
if not val1.path and self.absolute:
val1 = val1._replace(path="/")

val2 = other._val
if not val2.path and other.is_absolute():
if not val2.path and other.absolute:
val2 = val2._replace(path="/")

return val1 == val2
Expand All @@ -343,7 +343,7 @@ def __hash__(self) -> int:
ret = self._cache.get("hash")
if ret is None:
val = self._val
if not val.path and self.is_absolute():
if not val.path and self.absolute:
val = val._replace(path="/")
ret = self._cache["hash"] = hash(val)
return ret
Expand Down Expand Up @@ -398,8 +398,24 @@ def is_absolute(self) -> bool:
Return True for absolute ones (having scheme or starting
with //), False otherwise.

Is is preferred to call the .absolute property instead
as it is cached.
"""
return self.raw_host is not None
return self.absolute

@cached_property
def absolute(self) -> bool:
"""A check for absolute URLs.

Return True for absolute ones (having scheme or starting
with //), False otherwise.

"""
# `netloc`` is an empty string for relative URLs
# Checking `netloc` is faster than checking `hostname`
# because `hostname` is a property that does some extra work
# to parse the host from the `netloc`
return self._val.netloc != ""

def is_default_port(self) -> bool:
"""A check for default port.
Expand All @@ -426,7 +442,7 @@ def origin(self) -> "URL":

"""
# TODO: add a keyword-only option for keeping user/pass maybe?
if not self.is_absolute():
if not self.absolute:
raise ValueError("URL should be absolute")
if not self._val.scheme:
raise ValueError("URL should have scheme")
Expand All @@ -441,7 +457,7 @@ def relative(self) -> "URL":
scheme, user, password, host and port are removed.

"""
if not self.is_absolute():
if not self.absolute:
raise ValueError("URL should be absolute")
val = self._val._replace(scheme="", netloc="")
return URL(val, encoded=True)
Expand Down Expand Up @@ -589,7 +605,7 @@ def raw_path(self) -> str:

"""
ret = self._val.path
if not ret and self.is_absolute():
if not ret and self.absolute:
ret = "/"
return ret

Expand Down Expand Up @@ -671,7 +687,7 @@ def raw_parts(self) -> Tuple[str, ...]:

"""
path = self._val.path
if self.is_absolute():
if self.absolute:
if not path:
parts = ["/"]
else:
Expand Down Expand Up @@ -711,7 +727,7 @@ def parent(self) -> "URL":
def raw_name(self) -> str:
"""The last part of raw_parts."""
parts = self.raw_parts
if self.is_absolute():
if self.absolute:
parts = parts[1:]
if not parts:
return ""
Expand Down Expand Up @@ -789,7 +805,7 @@ def _make_child(self, paths: "Sequence[str]", encoded: bool = False) -> "URL":
old_path_cutoff = -1 if old_path_segments[-1] == "" else None
parsed = [*old_path_segments[:old_path_cutoff], *parsed]

if self.is_absolute():
if self.absolute:
parsed = _normalize_path_segments(parsed)
if parsed and parsed[0] != "":
# inject a leading slash when adding a path to an absolute URL
Expand Down Expand Up @@ -879,7 +895,7 @@ def with_scheme(self, scheme: str) -> "URL":
# N.B. doesn't cleanup query/fragment
if not isinstance(scheme, str):
raise TypeError("Invalid scheme type")
if not self.is_absolute():
if not self.absolute:
raise ValueError("scheme replacement is not allowed for relative URLs")
return URL(self._val._replace(scheme=scheme.lower()), encoded=True)

Expand All @@ -900,7 +916,7 @@ def with_user(self, user: Optional[str]) -> "URL":
password = val.password
else:
raise TypeError("Invalid user type")
if not self.is_absolute():
if not self.absolute:
raise ValueError("user replacement is not allowed for relative URLs")
return URL(
self._val._replace(
Expand All @@ -924,7 +940,7 @@ def with_password(self, password: Optional[str]) -> "URL":
password = self._QUOTER(password)
else:
raise TypeError("Invalid password type")
if not self.is_absolute():
if not self.absolute:
raise ValueError("password replacement is not allowed for relative URLs")
val = self._val
return URL(
Expand All @@ -946,7 +962,7 @@ def with_host(self, host: str) -> "URL":
# N.B. doesn't cleanup query/fragment
if not isinstance(host, str):
raise TypeError("Invalid host type")
if not self.is_absolute():
if not self.absolute:
raise ValueError("host replacement is not allowed for relative URLs")
if not host:
raise ValueError("host removing is not allowed")
Expand All @@ -970,7 +986,7 @@ def with_port(self, port: Optional[int]) -> "URL":
raise TypeError(f"port should be int or None, got {type(port)}")
if port < 0 or port > 65535:
raise ValueError(f"port must be between 0 and 65535, got {port}")
if not self.is_absolute():
if not self.absolute:
raise ValueError("port replacement is not allowed for relative URLs")
val = self._val
return URL(
Expand All @@ -984,7 +1000,7 @@ def with_path(self, path: str, *, encoded: bool = False) -> "URL":
"""Return a new URL with path replaced."""
if not encoded:
path = self._PATH_QUOTER(path)
if self.is_absolute():
if self.absolute:
path = self._normalize_path(path)
if len(path) > 0 and path[0] != "/":
path = "/" + path
Expand Down Expand Up @@ -1148,7 +1164,7 @@ def with_name(self, name: str) -> "URL":
if name in (".", ".."):
raise ValueError(". and .. values are forbidden")
parts = list(self.raw_parts)
if self.is_absolute():
if self.absolute:
if len(parts) == 1:
parts.append(name)
else:
Expand Down
Loading