Skip to content

tftp.path

tftp.path

pathlib-style access to TFTP files (path extra: pip install tftp[path]).

  • :class:TFTPPath -- a :mod:pathapi Path bound to a :class:~tftp.TFTPClient: client.path("boot/pxelinux.0").read_bytes().
  • :class:TFTPURIPath -- the tftp:// scheme for uripath.URIPath: URIPath("tftp://192.0.2.1/boot/x").copy("s3://bucket/x").

TFTP moves whole files and nothing else, so reading, writing, stat() (a size probe that transfers nothing), exists() and copy() work, and listing, deleting, renaming, move() from TFTP and directories raise NotImplementedError. Reads and writes stream through a bounded buffer; a with block over a write that raises, and a write never closed, abandon the upload.

TFTPPath(*segments, client=None, mode='octet')

Bases: Path

A file on a TFTP server, addressed like a PurePosixPath.

TFTPPath("boot/pxelinux.0", client=tftp.TFTPClient("192.0.2.1")), or client.path("boot", "pxelinux.0"). The path text is the filename sent to the server, so /boot/x and boot/x stay distinct (some servers resolve them differently). mode="netascii" selects the transfer mode.

TFTP can read and write whole files: open("r"), open("w")/"x", read_bytes/write_bytes/read_text/ write_text, stat() (a probe), exists(), is_file(), and copy() to and from any pathapi path and move() to one. Against a server speaking pytftp's x-list/x-mtime extensions, iterdir(), is_dir(), walk(), glob() and st_mtime work too (one listing per directory); other servers report directories as missing. Deleting, renaming, creating directories and permissions are not implemented: pathapi.protocols.supports() says so, and calling them raises NotImplementedError.

Source code in src/tftp/path/_local.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def __init__(self, *segments: Any, client: Optional[TFTPClient] = None, mode: str = "octet") -> None:
    text = ""
    inherited = None
    for segment in segments:
        if isinstance(segment, TFTPPath):
            piece = segment.as_posix()
            inherited = segment
        elif isinstance(segment, Pathname):
            piece = "/".join(segment.segments)
        elif isinstance(segment, str):
            piece = segment
        else:
            raise TypeError("argument should be a str or a Pathname, not %r" % type(segment).__name__)
        piece = piece.replace("\\", "/")
        if piece.startswith("/") or not text:
            text = piece
        elif piece:
            text = "%s/%s" % (text, piece)
    probe_key = None
    if client is None:
        if inherited is None:
            raise TypeError("TFTPPath needs client= (or a TFTPPath to join onto)")
        client, mode, probe_key = inherited._client, inherited._mode, inherited._probe_key
    if not isinstance(mode, str):
        raise TypeError("a transfer mode is text, not %s" % type(mode).__name__)
    self._client = check_client(client)
    self._mode = _mode(mode)
    self._probe_key = probe_key
    names = [name for name in text.split("/") if name and name != "."]
    if text.startswith("/"):
        self._segments = ["", *names] if names else ["", ""]
    else:
        self._segments = names

stat(*, follow_symlinks=True)

A FileStat from :meth:TFTPClient.stat: one probe, nothing transferred.

st_size is 0 with size_known False when the server reports no size, and st_mtime is 0 when it reports no time; a directory is recognised only by a server speaking x-list.

Source code in src/tftp/path/_local.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def stat(self, *, follow_symlinks: bool = True) -> FileStat:
    """A ``FileStat`` from :meth:`TFTPClient.stat`: one probe, nothing transferred.

    ``st_size`` is 0 with ``size_known`` False when the server reports no size,
    and ``st_mtime`` is 0 when it reports no time; a directory is recognised only
    by a server speaking ``x-list``.
    """
    try:
        info = self._client.stat(self.as_posix(), mode=self._mode)
    except TFTPError as exc:
        raise os_error(exc, self) from None
    return FileStat(
        st_size=info.size or 0,
        st_mtime=info.mtime or 0,
        is_dir=info.is_dir,
        size_known=info.is_dir or info.size is not None,
    )

TFTPURIPath

Bases: DelegatingURIPath

A file named by a tftp://host[:port]/path[;mode=netascii] URI (RFC 3617).

The filename sent is the path after the authority's /; a ;mode=netascii suffix on the last segment selects the mode. Transfer options follow the grammar of :class:tftp.TFTPURL: ;name=value parameters on the last segment, or a ?name=value&name=value query. The path is decoded before it is read, so a ; in a file name or in a parameter's value cannot be told from a delimiter: spell such a value in the query. Options built with with_options win over the URI's own, and a client given to with_client is used as it is. The operations are those of :class:TFTPPath, which does them: whole-file reads and writes, stat() by probe, exists(), and listing against a server speaking x-list; no deleting or renaming.

with_options(**client_options)

This path with a TFTPClient built from client_options (blksize, windowsize...).

A keyword given here wins over the option of the same name in the URI.

Source code in src/tftp/path/_uri.py
65
66
67
68
69
70
71
72
def with_options(self, **client_options: Any) -> "TFTPURIPath":
    """This path with a ``TFTPClient`` built from ``client_options`` (blksize, windowsize...).

    A keyword given here wins over the option of the same name in the URI.
    """
    source = self.source
    client = TFTPClient(source.host or "", source.port or 69, **client_options)
    return self.with_backend(_TFTPBackend(client, client_options))