Path objects¶
Each filing system exposes a path object with a uniform interface
modelled on pathlib.Path. The three concrete classes
inherit from oaknut.file.AcornPath, which carries the
shared surface — slash-join, iterdir, stat, read_bytes,
write_bytes, walk, touch, copy_to, and the rest — and delegates
the filesystem-specific primitives to each subclass:
oaknut.file.AcornPath(concrete base, parallel topathlib.PurePath/pathlib.Path)
Callers wanting “a path on any Acorn filesystem” can type-hint
with AcornPath directly:
from oaknut.file import AcornPath
def summarise(p: AcornPath) -> None:
print(p.path, p.stat().length)
A path is obtained from an open filesystem handle:
with ADFS.from_file("disc.adl") as adfs:
elite = adfs.root / "Games" / "Elite"
elite.read_bytes()
Paths bind to a filesystem handle and become stale when the handle
closes — never escape a path out of its with block and reach
for it later.
Where the path models diverge¶
The same surface hides a structural difference that bites if you assume Unix or ADFS shape when reading DFS code.
ADFS and AFS — hierarchical trees.
adfs.root and afs.root are the actual top-level directory
$. Walking with / descends into named subdirectories:
adfs.root # represents $
adfs.root / "Games" # $.Games (a directory)
adfs.root / "Games" / "Elite" # $.Games.Elite (a file)
mkdir() works on ADFSPath / AFSPath. parent walks up
the tree as far as the root (the root’s parent is itself).
iterdir() on a subdirectory yields its immediate children.
The Acorn shell’s ^ parent-token works as a slash-join
component too — stored literally, exactly the way
pathlib.PurePath stores ... Dots between consecutive
hats are optional (^^ and ^.^ parse to one component and
two components respectively but resolve identically), matching the
syntax the Acorn *DIR command accepts. AcornPath.resolve()
collapses any carets present:
elite = adfs.root / "Games" / "Elite"
(elite / "^").path # "$.Games.Elite.^" (literal)
(elite / "^").resolve().path # "$.Games" (collapsed)
(elite / "^^").parts # ("$", "Games", "Elite", "^^")
(elite / "^^.Docs.ReadMe").resolve().path
# "$.Docs.ReadMe" (up two, into Docs)
I/O methods (read_bytes(), stat(), iterdir(),
and the rest) call resolve() automatically before touching
the disc, so caret-bearing paths read and write correctly without
the caller having to remember to resolve. Pure path operations
(parts, path, name, parent,
slash-join, equality) preserve the literal form, just as pathlib
does.
DFS — single-character directories under a nameless root.
A DFS catalogue holds up to 31 file entries (62 on Watford DFS).
Each lives in one of 27 directories — $ and A–Z — and
all 27 are children of a nameless root. $ is the default
directory DFS assumes when a path omits one (per the Acorn DFS
User Guide); it is not a parent of A–Z, just a sibling
with one extra job:
dfs.root # the nameless root
dfs.root / "$" / "HELLO" # $.HELLO in the default directory
dfs.root / "A" / "GAME" # A.GAME, a sibling of $.HELLO
Empty directories cannot exist on DFS — a directory comes into
being the first time you write a file under it and disappears
again when its last file is deleted. There is no
DFSPath.mkdir(). parent does walk up: a file’s
parent is its single-character directory, whose parent is the
nameless root, whose parent is itself — so you can climb to the
root and slash back down into a sibling directory the same way
you would on ADFS. iterdir() on the nameless root yields one
entry per populated directory letter; iterdir() on a
single-letter directory yields the files filed under it.
See Paths and compound paths for the CLI-side companion to this explanation.
Binding to a filesystem handle¶
Path objects split into two layers of operation:
Pure path manipulation — slash-join, parent,
name, parts, path, equality, str(),
repr() — only inspects the path string. These work whether or
not the underlying image is open, the same way
pathlib.PurePath operations work on a string that doesn’t
correspond to any file on disc:
with ADFS.from_file("disc.adl") as adfs:
elite = adfs.root / "Games" / "Elite"
# adfs is closed; pure ops still work.
print(elite.path) # '$.Games.Elite'
sibling = elite.parent / "Manic" # ADFSPath at $.Games.Manic
I/O — read_bytes(), write_bytes(), stat(),
iterdir(), walk(), exists(), and friends — needs
the filesystem handle to be open. Calling them after the
with block has exited raises a clear error:
with ADFS.from_file("disc.adl") as adfs:
elite = adfs.root / "Games" / "Elite"
elite.read_bytes() # raises: filesystem closed
Pull the bytes you need before exiting if you want to use them later. Pure path objects are safe to keep around indefinitely.