# -*- test-case-name: twisted.python.test.test_urlpath -*- # Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. """ L{URLPath}, a representation of a URL. """ from typing import cast from urllib.parse import quote as urlquote, unquote as urlunquote, urlunsplit from hyperlink import URL as _URL _allascii = b"".join([chr(x).encode("ascii") for x in range(1, 128)]) def _rereconstituter(name): """ Attriute declaration to preserve mutability on L{URLPath}. @param name: a public attribute name @type name: native L{str} @return: a descriptor which retrieves the private version of the attribute on get and calls rerealize on set. """ privateName = "_" + name return property( lambda self: getattr(self, privateName), lambda self, value: ( setattr( self, privateName, value if isinstance(value, bytes) else value.encode("charmap"), ) or self._reconstitute() ), ) class URLPath: """ A representation of a URL. @ivar scheme: The scheme of the URL (e.g. 'http'). @type scheme: L{bytes} @ivar netloc: The network location ("host"). @type netloc: L{bytes} @ivar path: The path on the network location. @type path: L{bytes} @ivar query: The query argument (the portion after ? in the URL). @type query: L{bytes} @ivar fragment: The page fragment (the portion after # in the URL). @type fragment: L{bytes} """ def __init__( self, scheme=b"", netloc=b"localhost", path=b"", query=b"", fragment=b"" ): self._scheme = scheme or b"http" self._netloc = netloc self._path = path or b"/" self._query = query self._fragment = fragment self._reconstitute() def _reconstitute(self): """ Reconstitute this L{URLPath} from all its given attributes. """ urltext = urlquote( urlunsplit( (self._scheme, self._netloc, self._path, self._query, self._fragment) ), safe=_allascii, ) self._url = _URL.fromText(urltext.encode("ascii").decode("ascii")) scheme = _rereconstituter("scheme") netloc = _rereconstituter("netloc") path = _rereconstituter("path") query = _rereconstituter("query") fragment = _rereconstituter("fragment") @classmethod def _fromURL(cls, urlInstance): """ Reconstruct all the public instance variables of this L{URLPath} from its underlying L{_URL}. @param urlInstance: the object to base this L{URLPath} on. @type urlInstance: L{_URL} @return: a new L{URLPath} """ self = cls.__new__(cls) self._url = urlInstance.replace(path=urlInstance.path or [""]) self._scheme = self._url.scheme.encode("ascii") self._netloc = self._url.authority().encode("ascii") self._path = ( _URL(path=self._url.path, rooted=True).asURI().asText().encode("ascii") ) self._query = (_URL(query=self._url.query).asURI().asText().encode("ascii"))[1:] self._fragment = self._url.fragment.encode("ascii") return self def pathList(self, unquote=False, copy=True): """ Split this URL's path into its components. @param unquote: whether to remove %-encoding from the returned strings. @param copy: (ignored, do not use) @return: The components of C{self.path} @rtype: L{list} of L{bytes} """ segments = self._url.path mapper = lambda x: x.encode("ascii") if unquote: mapper = lambda x, m=mapper: m(urlunquote(x)) return [b""] + [mapper(segment) for segment in segments] @classmethod def fromString(klass, url): """ Make a L{URLPath} from a L{str} or L{unicode}. @param url: A L{str} representation of a URL. @type url: L{str} or L{unicode}. @return: a new L{URLPath} derived from the given string. @rtype: L{URLPath} """ if not isinstance(url, str): raise ValueError("'url' must be a str") return klass._fromURL(_URL.fromText(url)) @classmethod def fromBytes(klass, url): """ Make a L{URLPath} from a L{bytes}. @param url: A L{bytes} representation of a URL. @type url: L{bytes} @return: a new L{URLPath} derived from the given L{bytes}. @rtype: L{URLPath} @since: 15.4 """ if not isinstance(url, bytes): raise ValueError("'url' must be bytes") quoted = urlquote(url, safe=_allascii) return klass.fromString(quoted) @classmethod def fromRequest(klass, request): """ Make a L{URLPath} from a L{twisted.web.http.Request}. @param request: A L{twisted.web.http.Request} to make the L{URLPath} from. @return: a new L{URLPath} derived from the given request. @rtype: L{URLPath} """ return klass.fromBytes(request.prePathURL()) def _mod(self, newURL, keepQuery): """ Return a modified copy of C{self} using C{newURL}, keeping the query string if C{keepQuery} is C{True}. @param newURL: a L{URL} to derive a new L{URLPath} from @type newURL: L{URL} @param keepQuery: if C{True}, preserve the query parameters from C{self} on the new L{URLPath}; if C{False}, give the new L{URLPath} no query parameters. @type keepQuery: L{bool} @return: a new L{URLPath} """ return self._fromURL( newURL.replace(fragment="", query=self._url.query if keepQuery else ()) ) def sibling(self, path, keepQuery=False): """ Get the sibling of the current L{URLPath}. A sibling is a file which is in the same directory as the current file. @param path: The path of the sibling. @type path: L{bytes} @param keepQuery: Whether to keep the query parameters on the returned L{URLPath}. @type keepQuery: L{bool} @return: a new L{URLPath} """ return self._mod(self._url.sibling(path.decode("ascii")), keepQuery) def child(self, path, keepQuery=False): """ Get the child of this L{URLPath}. @param path: The path of the child. @type path: L{bytes} @param keepQuery: Whether to keep the query parameters on the returned L{URLPath}. @type keepQuery: L{bool} @return: a new L{URLPath} """ return self._mod(self._url.child(path.decode("ascii")), keepQuery) def parent(self, keepQuery=False): """ Get the parent directory of this L{URLPath}. @param keepQuery: Whether to keep the query parameters on the returned L{URLPath}. @type keepQuery: L{bool} @return: a new L{URLPath} """ return self._mod(self._url.click(".."), keepQuery) def here(self, keepQuery=False): """ Get the current directory of this L{URLPath}. @param keepQuery: Whether to keep the query parameters on the returned L{URLPath}. @type keepQuery: L{bool} @return: a new L{URLPath} """ return self._mod(self._url.click("."), keepQuery) def click(self, st): """ Return a path which is the URL where a browser would presumably take you if you clicked on a link with an HREF as given. @param st: A relative URL, to be interpreted relative to C{self} as the base URL. @type st: L{bytes} @return: a new L{URLPath} """ return self._fromURL(self._url.click(st.decode("ascii"))) def __str__(self) -> str: """ The L{str} of a L{URLPath} is its URL text. """ return cast(str, self._url.asURI().asText()) def __repr__(self) -> str: """ The L{repr} of a L{URLPath} is an eval-able expression which will construct a similar L{URLPath}. """ return "URLPath(scheme={!r}, netloc={!r}, path={!r}, query={!r}, fragment={!r})".format( self.scheme, self.netloc, self.path, self.query, self.fragment, )