From: Thomas Walker Lynch Date: Tue, 2 Dec 2025 05:49:49 +0000 (+0000) Subject: . X-Git-Url: https://git.reasoningtechnology.com/style/static/git-favicon.png?a=commitdiff_plain;h=af5aa3e49399e512f1ab959302845931f09691bc;p=Harmony.git . --- diff --git a/shared/authored/dir-walk.zip b/shared/authored/dir-walk.zip new file mode 100644 index 0000000..e46a154 Binary files /dev/null and b/shared/authored/dir-walk.zip differ diff --git a/shared/authored/dir-walk/BreadthMachine.py b/shared/authored/dir-walk/BreadthMachine.py index 5f20fbc..6d489e1 100755 --- a/shared/authored/dir-walk/BreadthMachine.py +++ b/shared/authored/dir-walk/BreadthMachine.py @@ -113,16 +113,17 @@ class BreadthMachine: return # Still rightmost in new front context; keep popping. - # ------------------------------------------------------------------ - # Introspection helper - # ------------------------------------------------------------------ def has_node(self) -> bool: - """True if there is a current node to read.""" + """ + True if there is a current node to read. + """ return self._has_node and self._tree_machine.context_depth() > 0 def build_breadth_machine(root_dp: str) -> BreadthMachine: - """Worker: construct and return a BreadthMachine for root_dp.""" + """ + Worker: construct and return a BreadthMachine for root_dp. + """ return BreadthMachine(root_dp) diff --git a/shared/authored/dir-walk/DepthMachine.py b/shared/authored/dir-walk/DepthMachine.py index 5d30f9f..ab6700a 100755 --- a/shared/authored/dir-walk/DepthMachine.py +++ b/shared/authored/dir-walk/DepthMachine.py @@ -121,16 +121,17 @@ class DepthMachine: return # Still rightmost in new front context; keep popping. - # ------------------------------------------------------------------ - # Introspection helper - # ------------------------------------------------------------------ def has_node(self) -> bool: - """True if there is a current node to read.""" + """ + True if there is a current node to read. + """ return self._has_node and self._tree_machine.context_depth() > 0 def build_depth_machine(root_dp: str) -> DepthMachine: - """Worker: construct and return a DepthMachine for root_dp.""" + """ + Worker: construct and return a DepthMachine for root_dp. + """ return DepthMachine(root_dp) diff --git a/shared/authored/dir-walk/JSON.py b/shared/authored/dir-walk/JSON.py new file mode 100755 index 0000000..7a3336a --- /dev/null +++ b/shared/authored/dir-walk/JSON.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- + +from __future__ import annotations +from typing import Any +import json as pyjson +import duck +import builtins + +# ---------------------------------------------------------------------- +def to_JSON_TM( + tm: Any +) -> str: + """ + Convert a Tape Machine (TM_ND_A or compatible) to a JSON string. + + JSON contains only: + - "tape": list of cell values + - "head": integer head position + - "extent": last valid index + """ + + head = tm.address() + tm.cue_leftmost() + tape_data = [tm.read(n) for n in range(0 ,tm.extent + 1)] + + obj = { + "tape" : tape_data + ,"head" : tm.address() + ,"extent": tm.extent + } + + return pyjson.dumps(tm ,ensure_ascii=False) + + +# ---------------------------------------------------------------------- +def print_TM( + tm: Any + ,indent: int = 0 +) -> None: + """ + Print the JSON representation of the TM nicely indented. + """ + s = to_JSON_TM(tm) + parsed = pyjson.loads(s) + out = pyjson.dumps(parsed ,indent=2 ,ensure_ascii=False) + prefix = " " * indent + for line in out.split("\n"): + print(prefix + line) + + +# ---------------------------------------------------------------------- +def pretty_TM( + tm0: Any + ,indent: int = 0 +) -> None: + """ + Walk the tape cells, printing: + + + + mark:: + ' ' normal cell + '*' head is here + '␀' empty (None, non-head) + """ + prefix = " " * indent + + tm1 = tm0.entangle() + tm1.cue_leftmost() + + while True: + v = tm1.read() + + if tm1.address() == tm0.address(): + mark = "*" + elif v is None: + mark = "␀" + else: + mark = " " + + s = "" if v is None else str(v) + print(f"{prefix}{mark}{s}") + + if not tm1.can_step(): break + tm1.step() + + +# ---------------------------------------------------------------------- +def is_a_TM( + value: Any +) -> bool: + """ + A Tape Machine is anything providing the attributes: + + entangle + cue_leftmost + read + can_step + step + address + + These are exactly what pretty_TM() uses. + """ + return duck.has( + value + ,"entangle" + ,"cue_leftmost" + ,"read" + ,"can_step" + ,"step" + ,"address" + ) + +# ---------------------------------------------------------------------- +def print( + value: Any + ,indent: int = 0 +) -> None: + """ + Print logic: + + - If TM: call print_TM + - Else: print raw value using Python print + (JSON primitives remain one-line) + """ + if is_a_TM(value): + print_TM(value ,indent) + else: + prefix = " " * indent + print(prefix + str(value)) + + +# ---------------------------------------------------------------------- +def pretty( + value: Any + ,indent: int = 0 +) -> None: + """ + Pretty-print logic: + + - If TM: pretty_TM + - If JSON primitive: print raw + - If non-JSON type: wrap into { "value": } and pretty-print + - If JSON object/array: pretty-print with indentation + """ + prefix = " " * indent + + # TM case + if is_a_TM(value): + pretty_TM(value ,indent) + return + + # JSON primitive: str, int, float, bool, None + if isinstance(value ,(str ,int ,float ,bool)) or value is None: + print(prefix + str(value)) + return + + # Try to JSON-dump; if it fails, wrap in {"value": repr} + try: + js = pyjson.dumps(value ,ensure_ascii=False) + parsed = pyjson.loads(js) + except Exception: + wrapped = { "value": repr(value) } + parsed = wrapped + + # Pretty-print JSON + out = pyjson.dumps(parsed ,indent=2 ,ensure_ascii=False) + for line in out.split("\n"): + print(prefix + line) + + + +# ---------------------------------------------------------------------- +def CLI() -> None: + """ + Demo for json helpers. Prints examples for: + + - to_JSON_TM + - print_TM + - pretty_TM + - is_a_TM + - print + - pretty + """ + p = builtins.print + + class DemoTM: + def __init__(self) -> None: + self._tape = [1 ,None ,"three"] + self._head_position = 1 # on the None cell + + @property + def extent(self) -> int: return len(self._tape) - 1 + def address(self) -> int: return self._head_position + def cue_leftmost(self) -> None: self._head_position = 0 + def can_step(self ,n: int = 1) -> bool: + return 0 <= self._head_position + n <= self.extent + def step(self ,n: int = 1) -> None: + self._head_position += n + def read(self ,n: int = 0) -> Any: + return self._tape[self._head_position + n] + def write(self ,value: Any ,n: int = 0) -> None: + self._tape[self._head_position + n] = value + def entangle(self) -> "DemoTM": + tm = DemoTM() + tm._tape = self._tape + tm._head_position = self._head_position + return tm + + tm = DemoTM() + + p("json.is_a_TM examples:") + p(" is_a_TM(tm) ->" ,is_a_TM(tm)) + p(" is_a_TM(123) ->" ,is_a_TM(123)) + p() + + p("json.to_JSON_TM example:") + p(" to_JSON_TM(tm) ->") + p(" " + to_JSON_TM(tm)) + p() + + p("json.print_TM example:") + print_TM(tm ,indent=2) + p() + + p("json.pretty_TM example:") + pretty_TM(tm ,indent=2) + p() + + p("json.print() examples:") + print(tm ,indent=2) # TM case + print(42 ,indent=2) # primitive + print({"a": 1 ,"b": 2} ,indent=2) # dict, one-line + p() + + p("json.pretty() examples:") + pretty(tm ,indent=2) # TM + p("---") + pretty(42 ,indent=2) # primitive + p("---") + pretty({"a": 1 ,"b": [2 ,3]} ,indent=2) # JSON object + p("---") + pretty(object() ,indent=2) # non-JSON -> wrapped + + +# ---------------------------------------------------------------------- +if __name__ == "__main__": + CLI() + diff --git a/shared/authored/dir-walk/Queue.py b/shared/authored/dir-walk/Queue.py index da84999..5715169 100755 --- a/shared/authored/dir-walk/Queue.py +++ b/shared/authored/dir-walk/Queue.py @@ -3,306 +3,157 @@ from __future__ import annotations -from typing import Any, Generic, Iterable, List, Optional, TypeVar - import meta - - -T = TypeVar("T") - - -def _format_item_for_debug( - item: Any -) -> str: - """ - Convert a queued item to a human-readable string for debug output. - - If the item has a callable .print() method, we call it and use - the returned value (converted to str). Otherwise we fall back - to repr(item). - """ - if hasattr(item, "print"): - candidate = getattr(item, "print") - if callable(candidate): - try: - text = candidate() - if text is None: - return repr(item) - return str(text) - except Exception: - return repr(item) - return repr(item) +from typing import Any, List def _queue_debug( - queue: Any - ,op: str - ,item: Any | None = None + q: Any + ,msg: str ) -> None: """ - Internal helper: emit a queue-related debug message if - the meta.DEBUG set contains the tag "Queue". - - Includes the queue instance pointer in hex so we can - distinguish multiple Queue instances. + Emit a Queue-related debug message if meta.DEBUG has 'Queue'. """ - if "Queue" not in meta.DEBUG: - return - - qid = hex(id(queue)) - - if item is None: - print(f"[Queue {qid}] {op}") - else: - desc = _format_item_for_debug(item) - print(f"[Queue {qid}] {op}: {desc}") + if "Queue" not in meta.DEBUG: return + print(f"[Queue {hex(id(q))}] {msg}") -class Queue(Generic[T]): +class Queue: """ - Queue — ordered queue of items (e.g., Context objects). + Simple FIFO queue with tagged push operations. - Semantics: + Items are stored in a Python list, with index 0 as the "front". + """ - - The "front" of the queue is the current item. - - The "back" of the queue will eventually be visited. - - Popping an empty queue is an error. + def __init__(self) -> None: + self._items: List[Any] = [] - Interface: + def push( + self + ,where_tag: str + ,item: Any + ) -> None: + """ + Push an item into the queue. - front() -> T : current item (head), does not remove it. - back() -> T : last item in the queue, does not remove it. - top() -> T : alias for front(). - depth() -> int : number of entries in the queue. - pop() -> T : remove and return the front item. + where_tag: + - 'front' -> insert at front + - 'back' -> append at back + - 'throw-away' -> discard item (for debugging) + """ + if hasattr(item, "print"): + desc = item.print() + else: + desc = repr(item) - push(where_tag, item: T) -> None: + if where_tag == "throw-away": + _queue_debug(self, f"push throw-away (discard): {desc}") + return - where_tag: - "front" -> insert item at front of queue - "back" -> append item at back of queue - "throw-away" -> do not store, treated as intentionally ignored + if where_tag == "front": + self._items.insert(0, item) + _queue_debug(self, f"push front: {desc}") + return - clone() -> Queue[T] + if where_tag == "back": + self._items.append(item) + _queue_debug(self, f"push back: {desc}") + return - Creates a copy of the Queue. For each item: - - If item has a .clone() method, it is called. - - Otherwise the item reference is reused. - """ + raise ValueError(f"Queue.push: unknown where_tag {where_tag!r}") - def __init__( - self - ,initial_iter: Optional[Iterable[T]] = None - ) -> None: - if initial_iter is None: - self._queue_list: List[T] = [] + def pop(self) -> Any: + """ + Pop and return the front item. + """ + if not self._items: + raise IndexError("Queue.pop on empty queue.") + item = self._items.pop(0) + if hasattr(item, "print"): + desc = item.print() else: - self._queue_list = list(initial_iter) + desc = repr(item) + _queue_debug(self, f"pop: {desc}") + return item - def snapshot(self) -> list: + def front(self) -> Any: """ - Return a shallow copy of the items in queue order (front to back). - - This is intended for read-only traversal by higher-level logic - such as TreeMachine.can_step(). + Return the front item without removing it. """ - return list(self._items) - - # ---------------------------------------------------------------------- - # Basic properties - # ---------------------------------------------------------------------- - def is_empty(self) -> bool: - return len(self._queue_list) == 0 - - def depth(self) -> int: - return len(self._queue_list) + if not self._items: + raise IndexError("Queue.front on empty queue.") + return self._items[0] - # ---------------------------------------------------------------------- - # Cloning - # ---------------------------------------------------------------------- - def clone(self) -> "Queue[T]": + def back(self) -> Any: """ - Create a copy of this Queue. - - The new queue has its own internal list. If an item provides - a .clone() method, that is used to copy the item; otherwise - the item reference is reused. + Return the back item without removing it. """ - new_item_list: List[T] = [] - for item in self._queue_list: - if hasattr(item, "clone") and callable(getattr(item, "clone")): - new_item_list.append(item.clone()) # type: ignore[arg-type] - else: - new_item_list.append(item) - new_q = Queue[T](new_item_list) - _queue_debug(self, f"clone -> new {hex(id(new_q))}") - return new_q - - # ---------------------------------------------------------------------- - # Accessors - # ---------------------------------------------------------------------- - def front(self) -> T: - if self.is_empty(): - raise RuntimeError("Queue.front() on empty queue.") - return self._queue_list[0] - - def back(self) -> T: - if self.is_empty(): - raise RuntimeError("Queue.back() on empty queue.") - return self._queue_list[-1] + if not self._items: + raise IndexError("Queue.back on empty queue.") + return self._items[-1] - def top(self) -> T: + def depth(self) -> int: """ - Alias for front(). + Number of items in the queue. """ - return self.front() + return len(self._items) - # ---------------------------------------------------------------------- - # Mutating operations - # ---------------------------------------------------------------------- - def pop(self) -> T: + def is_empty(self) -> bool: """ - Remove and return the front item. - - Popping an empty queue is an error. + True if the queue is empty. """ - if self.is_empty(): - raise RuntimeError("Queue.pop() on empty queue.") - item = self._queue_list.pop(0) - _queue_debug(self, "pop", item) - return item + return not self._items - def push( - self - ,where_tag: str - ,item: T - ) -> None: + def snapshot(self) -> list: """ - Push an item according to where_tag: - - where_tag == "front": - insert item at front of the queue. - - where_tag == "back": - append item at back of the queue. + Return a shallow copy of items (front to back). - where_tag == "throw-away": - do not store item; treated as intentionally ignored. - - Any other where_tag is an error. + Intended for read-only inspection (e.g. TreeMachine.can_step()). """ - if where_tag == "front": - self._queue_list.insert(0, item) - _queue_debug(self, "push front", item) - return - - if where_tag == "back": - self._queue_list.append(item) - _queue_debug(self, "push back", item) - return - - if where_tag == "throw-away": - _queue_debug(self, "push throw-away (discard)", item) - return - - raise ValueError(f"Unknown Queue push tag: {where_tag!r}") + return list(self._items) - # ---------------------------------------------------------------------- - # Dispatcher - # ---------------------------------------------------------------------- - def dispatch( - self - ,op: str - ,*args - ,**kwargs - ): + def clone(self) -> "Queue": """ - Generic dispatcher for method-tag control. - - Supported op values: - - "front" -> front() - "back" -> back() - "top" -> top() - "depth" -> depth() - "pop" -> pop() + Clone the queue. - "push" -> push(where_tag, item) - - Any unknown op raises ValueError. + If an item has a .clone() method, use it. Otherwise copy the + reference directly. """ - if op == "front": - return self.front() - if op == "back": - return self.back() - if op == "top": - return self.top() - if op == "depth": - return self.depth() - if op == "pop": - return self.pop() - if op == "push": - if len(args) != 2: - raise ValueError("dispatch('push', where_tag, item) requires 2 arguments.") - where_tag = args[0] - item = args[1] - self.push(where_tag, item) - return None - - raise ValueError(f"Unknown Queue operation: {op!r}") - - -# ---------------------------------------------------------------------- -# Simple test harness -# ---------------------------------------------------------------------- -class TestItem: - """ - Minimal test item for the __main__ test(). - - It implements .print() so Queue can use it in debug messages. - """ - - def __init__( - self - ,name: str - ) -> None: - self.name = name - - def print(self) -> str: - return f"TestItem({self.name})" - - def clone(self) -> "TestItem": - # For the test, clone is just another instance with same name. - return TestItem(self.name) + new_q = Queue() + for item in self._items: + if hasattr(item, "clone"): + new_q._items.append(item.clone()) + else: + new_q._items.append(item) + return new_q -def test() -> None: +def CLI(argv: list[str]) -> int: """ - Simple test for Queue with debug enabled. + Simple self-test for Queue. """ - meta.DEBUG.add("Queue") - print("Queue test starting...") meta.version() + meta.DEBUG.add("Queue") - q = Queue[TestItem]() - q2 = q.clone() # exercise clone-debug once at the start - - q.push("front", TestItem("root")) - q.push("back", TestItem("child-A")) - q.push("back", TestItem("child-B")) - q.push("throw-away", TestItem("ignored-node")) + q = Queue() + q.push("back", "apple") + q.push("back", "banana") + q.push("front", "cherry") + q.push("throw-away", "ignored") print(f"depth = {q.depth()}") - print(f"front = {q.front().print()}") - print(f"back = {q.back().print()}") + print(f"front = {q.front()!r}") + print(f"back = {q.back()!r}") popped = q.pop() - print(f"popped = {popped.print()}") + print(f"popped = {popped!r}") print(f"depth = {q.depth()}") print("Queue test done.") + return 0 if __name__ == "__main__": - test() + import sys + raise SystemExit(CLI(sys.argv)) diff --git a/shared/authored/dir-walk/TapeMachine.py b/shared/authored/dir-walk/TapeMachine.py old mode 100755 new mode 100644 index 0ca1d21..abf2e9b --- a/shared/authored/dir-walk/TapeMachine.py +++ b/shared/authored/dir-walk/TapeMachine.py @@ -2,130 +2,50 @@ # -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- from __future__ import annotations +from typing import Any +import duck -from typing import Any, Optional - -import meta - +class TM_ND_A: + """ + TM_ND_A -def _tape_debug(tm: Any, msg: str) -> None: - """Emit a TapeMachine-related debug message if meta.DEBUG has 'TapeMachine'.""" - if "TapeMachine" not in meta.DEBUG: return - print(f"[TapeMachine {hex(id(tm))}] {msg}") + TM Tape Machine + ND Non Destructive, no cell deletion or insertion + A Address supports addressing of cells + Machine does not support status, so if the machine exists, the tape has + at least one cell. -class TapeMachine: + Head starts on cell 0 and by contract with the user is always on a valid cell. + I.e. it is the user's responsibility to check `can_step` before stepping. """ - TapeMachine — single-tape, single-head, first-rest pattern. - - Tape is non-empty. - - Head starts on cell 0 and is always on a valid cell. - - Movement contract: caller checks can_step(n) before step(n). - """ - - def __init__(self, tape: Optional[Any] = None) -> None: - if tape is None: tape = [] - for name in ("__len__", "__getitem__", "__setitem__"): - if not hasattr(tape, name): raise TypeError(f"TapeMachine tape must support {name}: {tape!r}") - self._length: int = len(tape) - if self._length == 0: raise ValueError("TapeMachine requires a non-empty tape (first-rest pattern).") + # ---------------------------------------------------------------------- + def __init__(self ,tape): + duck.require(duck.class_of(tape) ,"__len__" ,"__getitem__" ,"__setitem__") + self._extent: int = len(tape) - 1 self._tape = tape self._head_position: int = 0 - _tape_debug(self, f"init length={self._length}, head_position=0") + # ---------------------------------------------------------------------- @property - def length(self) -> int: - """Length of the tape (number of cells).""" - return self._length - - def cue(self, new_position: Optional[int] = None) -> int: - """Get or set head (cue) position.""" - if new_position is None: return self._head_position - if not 0 <= new_position < self._length: raise IndexError(f"TapeMachine.cue: out of range ({new_position}).") - self._head_position = new_position - return self._head_position - - def can_step(self, n: int = 1) -> bool: - """True if step(n) would keep the head on the tape.""" - proposed_position = self._head_position + n - return 0 <= proposed_position < self.length - - def rewind(self) -> None: - """Move head to the first cell (index 0).""" - self._head_position = 0 - - def cue_end(self) -> None: - """Move head to the last cell (index length-1).""" - self._head_position = self._length - 1 - - def step(self, n: int = 1) -> None: - """Move head by n cells (n>0 right, n<0 left, n=0 no-op); caller must ensure can_step(n).""" - self._head_position += n - - def read(self) -> Any: - """Read value under head.""" - return self._tape[self._head_position] - - def write(self, value: Any) -> None: - """Write a new value into the current cell.""" - self._tape[self._head_position] = value - - def print(self) -> str: - """ - Printable view of the tape. - - Two spaces then value per line; line with the head is prefixed '* '. - """ - line_list = [] - for idx in range(self._length): - prefix = "* " if idx == self._head_position else " " - line_list.append(f"{prefix}{self._tape[idx]}") - return "\n".join(line_list) - + def extent(self) -> int: return self._extent + def tape(self) -> Any: return self._tape + def where(self) -> int: return self._head_position + def cue_leftmost(self) -> None: self._head_position = 0 + def cue_rightmost(self) -> None: self._head_position = self._extent + def cue(self ,new_position: int) -> int: self._head_position = new_position + def can_step(self ,n: int = 1) -> bool: return 0 <= self._head_position + n <= self._extent + def step(self ,n: int = 1) -> None: self._head_position += n + def read(self ,n: int = 0) -> Any: return self._tape[self._head_position + n] + def write(self ,value: Any ,n: int = 0) -> None: self._tape[self._head_position + n] = value + + # because this is an ND machine it is safe to entangle heads def entangle(self) -> "TapeMachine": """Create an entangled copy: shared tape, independent head_position.""" new_tm = object.__new__(TapeMachine) new_tm._tape = self._tape new_tm._length = self._length new_tm._head_position = self._head_position - _tape_debug(self, f"entangle -> new {hex(id(new_tm))} head_position={self._head_position}") return new_tm - -def test() -> None: - """Simple TapeMachine test with first-rest semantics.""" - meta.DEBUG.add("TapeMachine") - print("TapeMachine test starting..."); meta.version() - - tm = TapeMachine(["apple", "orange", "tangerine"]) - print(f"initial: cue={tm.cue()}, value={tm.read()!r}") - - while tm.can_step(1): - tm.step(1) - print(f"step(+1): cue={tm.cue()}, value={tm.read()!r}") - - if tm.can_step(-1): - tm.step(-1) - print(f"step(-1): cue={tm.cue()}, value={tm.read()!r}") - - tm.rewind() - print(f"rewind: cue={tm.cue()}, value={tm.read()!r}") - - tm2 = tm.entangle() - if tm.can_step(2): - tm.step(2) - print(f"tm step(+2): cue={tm.cue()}, value={tm.read()!r}") - if tm2.can_step(1): - tm2.step(1) - print(f"tm2 step(+1): cue={tm2.cue()}, value={tm2.read()!r}") - - tm.cue_end() - print(f"cue_end: cue={tm.cue()}, value={tm.read()!r}") - - print("print(tm):") - print(tm.print()) - print("TapeMachine test done.") - - -if __name__ == "__main__": - test() diff --git a/shared/authored/dir-walk/duck.py b/shared/authored/dir-walk/duck.py new file mode 100755 index 0000000..cc086d6 --- /dev/null +++ b/shared/authored/dir-walk/duck.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- + +from typing import Any, Optional + + +# ---------------------------------------------------------------------- +def class_of( + value: Any +) -> Optional[type]: + """ + Return the runtime class for a value. + + If the value has a __class__ attribute that is a real type, return it. + Otherwise return None. + """ + cls = getattr(value ,"__class__" ,None) + return cls if isinstance(cls ,type) else None + + +# ---------------------------------------------------------------------- +def has( + inspected_class: Any + ,*attribute_names: str +) -> bool: + """ + True iff `inspected_class` has *all* listed attributes. + + Usage: + has(MyClass ,"x" ,"y" ,"z") + """ + for name in attribute_names: + if not hasattr(inspected_class ,name): + return False + return True + + +# ---------------------------------------------------------------------- +def require( + inspected_class: Any + ,*attribute_names: str +) -> None: + """ + Guard: ensure `inspected_class` has the required attributes. + + Usage: + require(MyClass ,"x" ,"y" ,"z") + + Raise TypeError if any required attribute is missing. + """ + if has(inspected_class ,*attribute_names): + return + + # Collect missing names for detailed error. + missing_names = [ + name + for name in attribute_names + if not hasattr(inspected_class ,name) + ] + + raise TypeError( + f"require() failed: {inspected_class!r} " + f"is missing attributes {missing_names!r}" + ) + +# ---------------------------------------------------------------------- +def CLI() -> None: + """ + Demo for duck helpers. Prints examples for: + + - class_of + - has + - require + """ + + class Demo: + demo_attr = 1 + def demo_method(self) -> None: # short-stuff + pass + + print("duck.class_of examples:") + print(" class_of(Demo) ->" ,class_of(Demo)) + print(" class_of(Demo()) ->" ,class_of(Demo())) + print(" class_of(None) ->" ,class_of(None)) + print() + + print("duck.has examples:") + print(" has(Demo ,'demo_attr') ->" + ,has(Demo ,"demo_attr")) + print(" has(Demo ,'demo_method') ->" + ,has(Demo ,"demo_method")) + print(" has(Demo ,'missing_attr') ->" + ,has(Demo ,"missing_attr")) + print(" has(Demo ,'demo_attr','missing_attr') ->" + ,has(Demo ,"demo_attr" ,"missing_attr")) + print() + + print("duck.require examples:") + print(" require(Demo ,'demo_attr') (no error)") + require(Demo ,"demo_attr") + + print(" require(Demo ,'demo_attr','demo_method') (no error)") + require(Demo ,"demo_attr" ,"demo_method") + + print(" require(Demo ,'missing_attr') (should raise)") + try: + require(Demo ,"missing_attr") + except TypeError as exc: + print(" caught TypeError:" ,exc) + + +# ---------------------------------------------------------------------- +if __name__ == "__main__": + CLI() diff --git a/shared/authored/dir-walk/temp.py b/shared/authored/dir-walk/temp.py new file mode 100644 index 0000000..c546377 --- /dev/null +++ b/shared/authored/dir-walk/temp.py @@ -0,0 +1,98 @@ + + to_JSON: + For each cell of the tape: + 1. If the value has a to_JSON() method, use value.to_JSON(). + 2. If the value is a JSON primitive (str,int,float,bool,None), output it directly. + 3. If the value is a Python primitive that is not a JSON primitive, stringify it + with JSON-safe escaping. + 4. Otherwise: error — cannot represent in JSON. + + # ---------------------------------------------------------------------- + # JSON representation + # ---------------------------------------------------------------------- + + @classmethod + def is_a_JSON_primitive( + cls + ,value: Any + ) -> bool: + return ( + isinstance(value ,(str ,int ,float ,bool)) + or value is None + ) + + @classmethod + def has_to_JSON( + cls + ,value: Any + ) -> bool: + return hasattr(value ,"to_JSON") and callable(value.to_JSON) + + @classmethod + def normalize_value( + cls + ,value: Any + ) -> Any: + """ + Normalize a tape cell value into JSON-compatible output. + + Rules: + 1. If the value has to_JSON, call it. + 2. If the value is a JSON primitive, return it. + 3. If the value is a Python primitive but not JSON-safe, stringify it with escaping. + 4. Else: raise TypeError. + """ + # Rule 1: to_JSON + if cls.has_to_JSON(value): + return value.to_JSON() + + # Rule 2: JSON primitive + if cls.is_a_JSON_primitive(value): + return value + + # Rule 3: Python primitive → stringify + # Note: repr() is safe because JSONPretty will escape the resulting string. + if isinstance(value ,(bytes ,bytearray)): + # represent bytes safely in hex + hexval = value.hex() + return f"" + + if isinstance(value ,(complex ,range)): + return repr(value) + + # Rule 4: No idea what this is + raise TypeError(f"TapeMachine cannot JSON-normalize value: {value!r} of type {type(value)}") + + def to_JSON( + self + ) -> dict[str ,Any]: + """ + JSON representation: + + { + "TapeMachine": "" + ,"head": + ,"Tape": [ , , ... ] + } + + Where each Value is JSON-normalized by normalize_value(). + """ + normalized = [] + for raw in self._tape: + normalized.append(self.normalize_value(raw)) + + return { + "TapeMachine": hex(id(self)) + ,"head": self._head_position + ,"Tape": normalized + } + +def _tape_debug( + tm: Any + ,msg: str +) -> None: + """Emit a TapeMachine-related debug message if meta.DEBUG has 'TapeMachine'.""" + if "TapeMachine" not in meta.DEBUG: return + print(f"[TapeMachine {hex(id(tm))}] {msg}") + + diff --git a/shared/authored/dir-walk/test_TapeMachine.py b/shared/authored/dir-walk/test_TapeMachine.py new file mode 100755 index 0000000..8701c64 --- /dev/null +++ b/shared/authored/dir-walk/test_TapeMachine.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2; -*- + +from TapeMachine import TapeMachine +from JSONPretty import print_JSON ,pretty_JSON +import meta + + +def test_TapeMachine() -> None: + """Unit test for TapeMachine using JSONPretty output.""" + meta.DEBUG.add("TapeMachine") + print("TapeMachine test starting..."); meta.version() + + print("----------------------------------------") + print("instantiating tape machine") + print("") + + tm = TapeMachine(["apple", "orange", "tangerine"]) + + print("----------------------------------------") + print("printing newly instantiated tape machine") + print("") + + print_JSON(tm.to_JSON()) + + + print("----------------------------------------") + print("pretty printing newly instantiated tape machine") + print("") + + pretty_JSON(tm.to_JSON()) + + print("----------------------------------------") + print("one rightward step at a time through the tape") + print("") + + while tm.can_step(1): + tm.step(1) + print_JSON(tm.to_JSON()) + + print("----------------------------------------") + print("one leftward step at a time through the tape") + print("") + + if tm.can_step(-1): + tm.step(-1) + print_JSON(tm.to_JSON()) + + print("----------------------------------------") + print("printing machine again") + print("") + + tm.rewind() # this won't do anything + print_JSON(tm.to_JSON()) + + print("----------------------------------------") + print("making entangled copy") + print("") + + tm2 = tm.entangle() + + print("----------------------------------------") + print("stepping original two steps") + print("") + if tm.can_step(2): + tm.step(2) + print_JSON(tm.to_JSON()) + + print("----------------------------------------") + print("stepping copy one step") + print("") + if tm2.can_step(1): + tm2.step(1) + print_JSON(tm2.to_JSON()) + + tm.cue_end() + print_JSON(tm.to_JSON()) + + print("TapeMachine test done.") + + +if __name__ == "__main__": + test_TapeMachine()