-
Notifications
You must be signed in to change notification settings - Fork 0
Phase 34 os path plan
Part of the Amiga port design log.
Closes the os / os.path gap that showed up when comparing our
port to OoZe1911's. The C-side os module remains extensible
(via MP_REGISTER_EXTENSIBLE_MODULE), and a frozen os.py /
_ospath.py merges on top so the standard surface (chdir /
getcwd / listdir / mkdir / remove / rename / rmdir /
stat / statvfs / getenv / putenv / unsetenv) coexists
with the new additions.
import os
os.makedirs("Work:scripts/sub/dir", exist_ok=True)
for root, dirs, files in os.walk("Work:"):
print(root, len(files))
os.chmod("DH0:foo", 0) # 0 = no denials -- grant R, W, E, D
mask = os.getprotect("DH0:foo") # raw fib_Protection ULONG
print(mask & os.FIBF_DELETE) # nonzero ⇒ delete is denied
os.path.join("Work:", "scripts", "foo.py") # 'Work:scripts/foo.py'
os.path.abspath("foo.py") # uses cwd, volume aware
os.path.isabs("Sys:Prefs") # True (':' anywhere)
os.path.normpath("Work:scripts/../bin") # 'Work:bin'os.* additions:
| Call | Returns | Notes |
|---|---|---|
os.chmod(path, mask) |
None |
SetProtection wrapper. Suppresses AmigaDOS auto-requesters. OSError (translated AmigaDOS errno) on failure. |
os.getprotect(path) |
int |
Lock + Examine + read fib_Protection. Returns the raw 32-bit mask; AND with FIBF_* to test bits. |
os.makedirs(name, exist_ok=False) |
None |
Recursive mkdir, volume-aware. The volume prefix itself isn't created. EEXIST family swallowed when exist_ok=True. |
os.walk(top, topdown=True) |
generator | Yields (dirpath, dirnames, filenames) triples. Same shape as CPython; no onerror / followlinks (AmigaOS has no symlinks). |
os.FIBF_READ / WRITE / EXECUTE / DELETE |
int | RWED bits. Set bit ⇒ denied (AmigaDOS inverted convention). |
os.FIBF_ARCHIVE / PURE / SCRIPT / HOLD |
int | APSH bits. Set bit ⇒ asserted. |
os.path |
module | The frozen _ospath module (volume-aware path helpers). |
os.path (== _ospath) surface:
| Call | Returns | Notes |
|---|---|---|
path.join(*parts) |
str |
No separator after : (volume terminator); / between other components. A part containing : resets the join. |
path.split(p) |
(dir, base) |
Splits at the last / or :. : stays on the dirname side. |
path.splitext(p) |
(root, ext) |
Last . after the last separator. Leading . (dot-files) returns ("", p) semantics on basename. |
path.basename(p) / path.dirname(p)
|
str |
split(p)[1] / split(p)[0]. |
path.isabs(p) |
bool |
True if p contains any : (AmigaDOS treats Work:, :foo, Sys:Prefs all as absolute). |
path.abspath(p) |
str |
p if isabs(p); else join(getcwd(), p) then normpath. |
path.normpath(p) |
str |
Collapses . / ..; clamps at the volume boundary (Work:.. stays Work:). |
path.exists(p) / path.isfile(p) / path.isdir(p)
|
bool |
Wrap os.stat. False on any OSError. |
The four RWED bits in fib_Protection are inverted relative to
Unix:
| Bit | Set means | Clear means |
|---|---|---|
FIBF_READ |
read denied | read allowed |
FIBF_WRITE |
write denied | write allowed |
FIBF_EXECUTE |
execute denied | execute allowed |
FIBF_DELETE |
delete denied | delete allowed |
So os.chmod(path, 0) grants R, W, E, and D (nothing denied), and
os.chmod(path, os.FIBF_DELETE) denies only delete.
The APSH bits follow the conventional "set means yes":
| Bit | Set means |
|---|---|
FIBF_ARCHIVE |
file has been backed up since last change |
FIBF_PURE |
binary is reentrant (can be made resident) |
FIBF_SCRIPT |
file is a Shell script |
FIBF_HOLD |
keep a pure module resident on first use |
This matches the AmigaShell Protect command's encoding.
- Posix mode → AmigaDOS bit translation for
chmod.rwxandrweddon't map cleanly; callers wanting cross-platform code should branch onsys.platform. -
os.path.expanduser(no~concept),expandvars(would have to walkdos.library GetVar— separate phase if needed). -
os.walkfollow-symlinks /onerrorcallback. AmigaOS has no symlinks and the on-error case is rare enough to wrap in user code. - Full CPython
os.pathparity (commonpath,relpath,samefile,getmtime, ...) — easy to add later if a real call site needs them. -
os.environmapping interface — Phase 20'sgetenv/putenv/unsetenvare enough for now.
-
dos.librarySetProtection/Lock/Examine/UnLock(V36+).
The C-side os module (registered as
MP_REGISTER_EXTENSIBLE_MODULE) merges with a frozen os.py so the
existing C entries stay live and the frozen file adds the rest.
Step 1: os.chmod + os.getprotect (C entries) + FIBF_* constants
↓
Step 2: Frozen os.py + _ospath.py
↓
Step 3: Docs flip + smoke tests
| # | Step | Output | On-target smoke |
|---|---|---|---|
| 1 | New port-local modosamiga.c registering _osamiga with chmod(path, mask) (SetProtection), getprotect(path) (Lock+Examine+fib_Protection), and the FIBF_* constants. Surfaced via the frozen os.py from _osamiga import ... in Step 2 (rather than amending extmod/modos.c, which would violate the port-separation rule). |
New port-local C module. | From the REPL under Amiberry: os.getprotect("S:Startup-Sequence") returns a mask; os.chmod round-trips. |
| 2 | Frozen ports/amiga/modules/os.py with makedirs (recursive mkdir, AmigaOS volume-aware) and walk (recursive listdir + stat tree generator). Frozen ports/amiga/modules/_ospath.py with join / split / splitext / basename / dirname / exists / isfile / isdir / isabs / abspath / normpath. os.py does import _ospath as path so os.path Just Works. |
Two frozen modules. |
os.makedirs + os.walk against a temp tree. os.path.normpath collapses .. correctly across volume separators. |
| 3 | Docs flip, manifest verification, smoke test. |
docs/amiga.md Phase 34 → ✅. docs/amiga-testing.md gains a short os / os.path subsection. |
tests/ports/amiga/test_os_smoke.py covers the surface and the volume-separator edge cases under vamos. |
Each step is small: Step 1 is ~50 LOC C, Step 2 is ~150 LOC Python, Step 3 is paperwork.
- Two C functions in
ports/amiga/modosamiga.c:Registered as thestatic mp_obj_t mod_osamiga_chmod(mp_obj_t path_obj, mp_obj_t mask_obj); static mp_obj_t mod_osamiga_getprotect(mp_obj_t path_obj);
_osamigamodule viaMP_REGISTER_MODULE. The frozenos.py(Step 2) doesfrom _osamiga import chmod, getprotect, FIBF_*so callers see them onos. -
os.chmod(path, mask):- Suppress AmigaDOS auto-requesters (
pr_WindowPtr = -1) around the call so a path on an unmounted volume gets a clean OSError instead of a system dialog (matchesamiga.exists()/amiga.match()). -
SetProtection(name, mask); raises OSError on failure with the errno mappingamiga_dos_errno_from.
- Suppress AmigaDOS auto-requesters (
-
os.getprotect(path):-
Lock(path, SHARED_LOCK)→Examine(lock, &fib)→fib.fib_Protection. UnLock on the way out. - Same auto-requester suppression.
- Returns the raw
fib_ProtectionULONG.
-
-
FIBF_*constants exposed on_osamigaand pulled intoos.*via the frozenos.pystar-list import:-
os.FIBF_READ,os.FIBF_WRITE,os.FIBF_EXECUTE,os.FIBF_DELETE,os.FIBF_ARCHIVE,os.FIBF_PURE,os.FIBF_SCRIPT,os.FIBF_HOLD(the NDK shipsHOLD, notHIDDEN; the plan was wrong here).
-
AmigaDOS protection flags are inverted for the four classic
RWED bits — a set bit means "denied", a clear bit means
"allowed." The bits exposed by FIBF_READ / WRITE / EXECUTE /
DELETE follow the on-disk encoding, so chmod(path, 0) grants
all four (no bits set = nothing denied). ARCHIVE, PURE,
SCRIPT, HIDDEN follow the "set means yes" convention.
Document the inversion in the module docstring so callers aren't
surprised; this is the same convention Protect from the
AmigaShell uses.
REPL on target:
>>> import os
>>> oct(os.getprotect("S:Startup-Sequence")) # readable, writable
'0o15'
>>> os.chmod("Ram Disk:test", 0) # all allowed, no flags
>>> os.getprotect("Ram Disk:test")
0Vamos: dos.library's SetProtection / Examine work end-to-end
through mp: volumes, so the call paths are testable.
# Frozen extension for the C-side `os` module. The C entries
# (chdir, getcwd, listdir, mkdir, remove, rename, rmdir, stat,
# statvfs, getenv, putenv, unsetenv, chmod, getprotect, plus the
# FIBF_* constants) are already in this module's globals when
# this file is loaded -- the extensible-module mechanism merges
# them in.
import _ospath as path # exposes amiga.os.path
def makedirs(name, exist_ok=False):
"""mkdir -p semantics, AmigaOS volume aware."""
# walk Volume:dir1/dir2/dir3 component by component
def walk(top, topdown=True):
"""Recursive listdir + stat tree generator."""
# follow same shape as CPython os.walkmakedirs handles Work:scripts/sub/dir:
- Split off
Work:as the volume prefix (must exist). - For each subsequent component, accumulate the path with
/andmkdirit, toleratingEEXISTwhenexist_ok=True.
walk follows CPython's os.walk shape — (dirpath, dirnames, filenames) triples, topdown ordering preserved. AmigaOS paths
join correctly when the parent ends with : (no extra /) vs
when it ends with anything else.
Pure-Python helpers. Volume-aware policy: the first : in a path
is the volume terminator (Sys:Prefs/Workbench); subsequent /
characters separate components. A leading : is illegal in
AmigaOS so we don't need to worry about that edge case.
Functions:
-
join(*parts)— concatenate with the right separator:-
join("Work:", "scripts")→"Work:scripts"(no separator after:) -
join("Work:scripts", "foo.py")→"Work:scripts/foo.py" - A part containing
:resets the join (it's a fresh absolute path).
-
-
split(p)→(dirname, basename). Splits at the last/or:; the separator character is retained on the dirname side. -
splitext(p)→(root, ext). Last.after the last separator. -
basename(p)→ second element ofsplit. -
dirname(p)→ first element ofsplit. -
isabs(p)— has a:(volume reference) anywhere in the path, not just at index 0. AmigaOS treatsWork:and:fooandSys:Prefs/Workbenchall as absolute. -
abspath(p)—pifisabs(p), elsejoin(getcwd(), p). -
normpath(p)— collapse.and..components without crossing the volume boundary (Work:..is meaningless and stays as-is). -
exists,isfile,isdir— wrapstat().
>>> os.path.join("Work:", "scripts", "foo.py")
'Work:scripts/foo.py'
>>> os.path.normpath("Work:scripts/../bin/./tool")
'Work:bin/tool'
>>> os.path.isabs("Sys:")
True
>>> os.path.isabs("foo.py")
False
>>> os.path.split("Work:scripts/foo.py")
('Work:scripts', 'foo.py')
>>> os.path.split("Work:foo.py")
('Work:', 'foo.py')
>>> os.makedirs("Ram Disk:a/b/c", exist_ok=True)-
docs/amiga.mdPhase 34 status → ✅; section gains the full surface matrix (which calls / which module / what they return). -
docs/amiga-testing.mdshortos/os.pathsubsection with REPL examples and a note about the AmigaDOS inverted-bit semantics. -
tests/ports/amiga/test_os_smoke.py— vamos-runnable:-
os.path.isabs("Sys:")is True;isabs("foo.py")is False -
os.path.join/split/normpathcorner cases -
os.makedirsround-trip againstmp:tree -
os.chmod/os.getprotectround-trip on a temp file -
os.walkyields the expected(dirpath, dirs, files)shape
-
-
Volume-vs-directory separator policy.
:terminates the volume once per path;/separates directories after that. Pure-Python helpers in_ospath.pycarry that invariant; the C entries already do (they pass the string straight through todos.library). -
AmigaDOS protection-bit inversion. Document loudly in
_ospath.py/os.py/docs/amiga-testing.md. Set bit ⇒ denial for RWED; set bit ⇒ assertion for APSH. -
No CPython-compat translation layer for
chmod. Posix rwx → AmigaDOS rwed is ambiguous and lossy; we take the AmigaDOS mask directly. CPython interop scripts that need cross-platformchmodshould useif sys.platform == 'amiga'. -
Extensible-module attribute lookup. When the frozen
os.pyruns, the C-side globals are already populated, so functions defined in the frozen file shadow nothing — they extend. If a future C-side change adds e.g. amkdirwith a new signature, the frozen file'smakedirscontinues to call it through the merged namespace (no name change needed).
- Posix mode translation for
chmod— ambiguous, see above. -
os.path.expanduser/expandvars— AmigaOS usesdos.library GetVarand there's no~concept. -
os.walkfollow-symlinks / on-error callback — keep minimal shape; users wrap if needed. - Full CPython
os.pathparity (commonpath,relpath,samefile,getmtime, etc.) — add later if a real call site needs them. -
os.environmapping interface — Phase 20'sgetenv/putenv/unsetenvare enough for now.
ports/amiga/modosamiga.c — chmod + getprotect + FIBF_*
ports/amiga/modules/os.py — frozen extension (makedirs,
walk, _osamiga re-exports,
import _ospath as path)
ports/amiga/modules/_ospath.py — AmigaOS-aware path helpers
tests/ports/amiga/test_os_smoke.py — vamos smoke + RAM: round trip
docs/amiga.md — Phase 34 status + surface matrix
docs/amiga-testing.md — os / os.path subsection
Variants: all three. ~5 KB text per variant (1 KB C + 4 KB frozen Python bytecode).