diff --git a/CHANGES/1100.feature.rst b/CHANGES/1100.feature.rst new file mode 100644 index 00000000..08b3d00c --- /dev/null +++ b/CHANGES/1100.feature.rst @@ -0,0 +1 @@ +Added :attr:`~yarl.URL.absolute` which is now preferred over ``URL.is_absolute()`` -- by :user:`bdraco`. diff --git a/docs/api.rst b/docs/api.rst index 35e75eb3..84c009dd 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -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. @@ -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 ------------------ diff --git a/tests/test_url.py b/tests/test_url.py index 213338ef..3d622c3a 100644 --- a/tests/test_url.py +++ b/tests/test_url.py @@ -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 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 @@ -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(): diff --git a/yarl/_url.py b/yarl/_url.py index 0153c81b..f303f634 100644 --- a/yarl/_url.py +++ b/yarl/_url.py @@ -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 @@ -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 @@ -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 @@ -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. @@ -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") @@ -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) @@ -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 @@ -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: @@ -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 "" @@ -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 @@ -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) @@ -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( @@ -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( @@ -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") @@ -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( @@ -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 @@ -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: