# nobodd.path

Defines the `FatPath` class, a Path-like class for interacting with directories and sub-directories in a `FatFileSystem` instance. You should never need to construct this class directly; instead it should be derived from the `root` attribute which is itself a `FatPath` instance.

    >>> from nobodd.disk import DiskImage
    >>> from nobodd.fs import FatFileSystem
    >>> img = DiskImage('test.img')
    >>> fs = FatFileSystem(img.partitions[1].data)
    >>> for p in fs.root.iterdir():
    ...     print(repr(p))
    ...
    FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/foo')
    FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/bar.txt')
    FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/setup.cfg')
    FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/baz')
    FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/adir')
    FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/BDIR')

## FatPath

class nobodd.path.FatPath(fs, *pathsegments)

  A `Path`-like object representing a filepath within an associated `FatFileSystem`.

  There is rarely a need to construct this class directly. Instead, instances should be obtained via the `root` property of a `FatFileSystem`. If constructed directly, fs is a `FatFileSystem` instance, and pathsegments is the sequence of strings to be joined with a path separator into the path.

  Instances provide almost all the facilities of the `pathlib.Path` class they are modeled after, including the crucial `open()` method, `iterdir()`, `glob()`, and `rglob()` for enumerating directories, `stat()`, `is_dir()`, and `is_file()` for querying information about files, division for construction of new paths, and all the usual `name`, `parent`, `stem`, and `suffix` attributes. When the `FatFileSystem` is writable, then `unlink()`, `touch()`, `mkdir()`, `rmdir()`, and `rename()` may also be used.

  Instances are also comparable for the purposes of sorting, but only within the same `FatFileSystem` instance (comparisons across file-system instances raise `TypeError`).

  exists()

    Whether the path points to an existing file or directory:

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> (fs.root / 'foo').exists()
        True
        >>> (fs.root / 'fooo').exists()
        False

  glob(pattern)

    Glob the given relative pattern in the directory represented by this path, yielding matching files (of any kind):

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> sorted((fs.root / 'nobodd').glob('*.py'))
        [FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/nobodd/__init__.py'),
         FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/nobodd/disk.py'),
         FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/nobodd/fat.py'),
         FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/nobodd/fs.py'),
         FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/nobodd/gpt.py'),
         FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/nobodd/main.py'),
         FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/nobodd/mbr.py'),
         FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/nobodd/tftp.py'),
         FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/nobodd/tools.py')]

    Patterns are the same as for `fnmatch()`, with the addition of “`**`” which means “this directory and all subdirectories, recursively”. In other words, it enables recurisve globbing.

    Warning

    Using the “`**`” pattern in large directory trees may consume an inordinate amount of time.

  is_absolute()

    Return whether the path is absolute or not. A path is considered absolute if it has a “/” prefix.

  is_dir()

    Return a `bool` indicating whether the path points to a directory. `False` is also returned if the path doesn’t exist.

  is_file()

    Returns a `bool` indicating whether the path points to a regular file. `False` is also returned if the path doesn’t exist.

  is_mount()

    Returns a `bool` indicating whether the path is a mount point. In this implementation, this is only `True` for the root path.

  is_relative_to(*other)

    Return whether or not this path is relative to the other path.

  iterdir()

    When the path points to a directory, yield path objects of the directory contents:

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> for child in fs.root.iterdir(): child
        ...
        FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/foo')
        FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/bar.txt')
        FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/setup.cfg')
        FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/baz')
        FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/adir')
        FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/BDIR')

    The children are yielded in arbitrary order (the order they are found in the file-system), and the special entries `'.'` and `'..'` are not included.

  joinpath(*other)

    Calling this method is equivalent to combining the path with each of the other arguments in turn:

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> fs.root
        FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/')
        >>> fs.root.joinpath('nobodd')
        FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/nobodd')
        >>> fs.root.joinpath('nobodd', 'main.py')
        FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/nobodd/main.py')

  match(pattern)

    Match this path against the provided glob-style pattern. Returns a `bool` indicating if the match is successful.

    If pattern is relative, the path may be either relative or absolute, and matching is done from the right:

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> f = fs / 'nobodd' / 'mbr.py'
        >>> f
        FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/nobodd/mbr.py')
        >>> f.match('*.py')
        True
        >>> f.match('nobodd/*.py')
        True
        >>> f.match('/*.py')
        False

    As FAT file-systems are case-insensitive, all matches are likewise case-insensitive.

  mkdir(mode=511, parents=False, exist_ok=False)

    Create a new directory at this given path. The mode parameter exists only for compatibility with `pathlib.Path` and is otherwise ignored. If the path already exists, `FileExistsError` is raised.

    If parents is true, any missing parents of this path are created as needed.

    If parents is false (the default), a missing parent raises `FileNotFoundError`.

    If exist_ok is false (the default), `FileExistsError` is raised if the target directory already exists.

    If exist_ok is true, `FileExistsError` exceptions will be ignored (same behavior as the POSIX `mkdir -p` command), but only if the last path component is not an existing non-directory file.

  open(mode='r', buffering=-1, encoding=None, errors=None, newline=None)

    Open the file pointed to by the path, like the built-in `open()` function does. The mode, buffering, encoding, errors and newline options are as for the `open()` function. If successful, a `FatFile` instance is returned.

  read_bytes()

    Return the binary contents of the pointed-to file as a bytes object:

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> (fs.root / 'foo').read_text()
        b'foo\n'

  read_text(encoding=None, errors=None)

    Return the decoded contents of the pointed-to file as a string:

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> (fs.root / 'foo').read_text()
        'foo\n'

  relative_to(*other)

    Compute a version of this path relative to the path represented by other. If it’s impossible, `ValueError` is raised.

  rename(target)

    Rename this file or directory to the given target, and return a new `FatPath` instance pointing to target. If target exists and is a file, it will be replaced silently. target can be either a string or another path object:

        >>> p = fs.root / 'foo'
        >>> p.open('w').write('some text')
        9
        >>> target = fs.root / 'bar'
        >>> p.rename(target)
        FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/bar')
        >>> target.read_text()
        'some text'

    The target path must be absolute. There are no guarantees of atomic behaviour (in contrast to `os.rename()`).

    Note

    `pathlib.Path.rename()` permits relative paths, but interprets them relative to the working directory which is a concept `FatPath` does not support.

  resolve(strict=False)

    Make the path absolute, resolving any symlinks. A new `FatPath` object is returned.

    `".."` components are also eliminated (this is the only method to do so). If the path doesn’t exist and strict is `True`, `FileNotFoundError` is raised. If strict is `False`, the path is resolved as far as possible and any remainder is appended without checking whether it exists.

    Note that as there is no concept of the “current” directory within `FatFileSystem`, relative paths cannot be resolved by this function, only absolute.

  rglob(pattern)

    This is like calling `glob()` with a prefix of “`**/`” to the specified pattern.

  rmdir()

    Remove this directory. The directory must be empty.

  stat(*, follow_symlinks=True)

    Return a `os.stat_result` object containing information about this path:

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> p = (fs.root / 'nobodd' / 'main.py')
        >>> p.stat().st_size
        388
        >>> p.stat().st_ctime
        1696606672.02

    Note

    In a FAT file-system, `atime` has day resolution, `mtime` has 2-second resolution, and `ctime` has either 2-second or millisecond resolution depending on the driver that created it. Directories have no timestamp information.

    The follow_symlinks parameter is included purely for compatibility with `pathlib.Path.stat()`; it is ignored as symlinks are not supported.

  touch(mode=438, exist_ok=True)

    Create a file at this given path. The mode parameter is only present for compatibility with `pathlib.Path` and is otherwise ignored. If the file already exists, the function succeeds if exist_ok is `True` (and its modification time is updated to the current time), otherwise `FileExistsError` is raised.

  unlink(missing_ok=False)

    Remove this file. If the path points to a directory, use `rmdir()` instead.

    If missing_ok is `False` (the default), `FileNotFoundError` is raised if the path does not exist. If missing_ok is `True`, `FileNotFoundError` exceptions will be ignored (same behaviour as the POSIX `rm -f` command).

  with_name(name)

    Return a new path with the `name` changed. If the original path doesn’t have a name, `ValueError` is raised.

  with_stem(stem)

    Return a new path with the `stem` changed. If the original path doesn’t have a name, `ValueError` is raised.

  with_suffix(suffix)

    Return a new path with the `suffix` changed. If the original path doesn’t have a suffix, the new suffix is appended instead. If the suffix is an empty string, the original suffix is removed.

  write_bytes(data)

    Open the file pointed to in bytes mode, write data to it, and close the file:

        >>> p = fs.root / 'my_binary_file'
        >>> p.write_bytes(b'Binary file contents')
        20
        >>> p.read_bytes()
        b'Binary file contents'

    An existing file of the same name is overwritten.

  write_text(data, encoding=None, errors=None, newline=None)

    Open the file pointed to in text mode, write data to it, and close the file:

        >>> p = fs.root / 'my_text_file'
        >>> p.write_text('Text file contents')
        18
        >>> p.read_text()
        'Text file contents'

    An existing file of the same name is overwritten. The optional parameters have the same meaning as in `open()`.

  property anchor

    Returns the concatenation of the drive and root. This is always “/”.

  property fs

    Returns the `FatFileSystem` instance that this instance was constructed with.

  property name

    A string representing the final path component, excluding the root:

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> p = (fs.root / 'nobodd' / 'main.py')
        >>> p.name
        'main.py'

  property parent

    The logical parent of the path:

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> p = (fs.root / 'nobodd' / 'main.py')
        >>> p.parent
        FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/nobodd')

    You cannot go past an anchor:

        >>> p = (fs.root / 'nobodd' / 'main.py')
        >>> p.parent.parent.parent
        FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/')

  property parents

    An immutable sequence providing access to the logical ancestors of the path:

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> p = (fs.root / 'nobodd' / 'main.py')
        >>> p.parents
        (FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/nobodd'),
         FatPath(<FatFileSystem label='TEST' fat_type='fat16'>, '/'))

  property parts

    A tuple giving access to the path’s various components:

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> p = (fs.root / 'nobodd' / 'main.py')
        >>> p.parts
        ['/', 'nobodd', 'main.py']

  property root

    Returns a string representing the root. This is always “/”.

  property stem

    The final path component, without its suffix:

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> p = (fs.root / 'nobodd' / 'main.py')
        >>> p.stem
        'main'

  property suffix

    The file extension of the final component, if any:

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> p = (fs.root / 'nobodd' / 'main.py')
        >>> p.suffix
        '.py'

  property suffixes

    A list of the path’s file extensions:

        >>> fs
        <FatFileSystem label='TEST' fat_type='fat16'>
        >>> p = (fs.root / 'nobodd.tar.gz')
        >>> p.suffixes
        ['.tar', '.gz']

## Support functions

nobodd.path.get_cluster(entry, fat_type)

  Given entry, a `DirectoryEntry`, and the fat_type indicating the size of FAT clusters, return the first cluster of the file associated with the directory entry.