From: Thomas Walker Lynch Date: Wed, 19 Nov 2025 03:27:46 +0000 (+0000) Subject: . X-Git-Url: https://git.reasoningtechnology.com/style/rt_dark_doc.css?a=commitdiff_plain;h=f4aa3cda299574643f37e51985f616e143928478;p=Harmony.git . --- diff --git a/nohup.out b/nohup.out new file mode 100644 index 0000000..e69de29 diff --git a/tool/skeleton/CLI.py b/tool/skeleton/CLI.py index 18467cd..b62d3a8 100755 --- a/tool/skeleton/CLI.py +++ b/tool/skeleton/CLI.py @@ -34,11 +34,10 @@ is invoked to print the environment. from __future__ import annotations +import meta import sys from typing import Sequence -import meta import doc -import skeleton meta.debug_set("print_command_lists") @@ -157,13 +156,13 @@ def CLI(argv: Sequence[str] | None = None) -> int: doc.print_help() return 1 - status,Harmony_root = skeleton.where_is_Harmony() - if status == 'different': - print("Seems we are not running in the Harmony project, will exit.") - return 2 - if status == 'not-found': - print("Harmony project not found, normally this command is run from with Harmony.") - return 3 +# status,Harmony_root = skeleton.where_is_Harmony() +# if status == 'different': +# print("Seems we are not running in the Harmony project, will exit.") +# return 2 +# if status == 'not-found': +# print("Harmony project not found, normally this command is run from with Harmony.") +# return 3 ret_val = 0 if "environment" in no_other_list: diff --git a/tool/skeleton/GitIgnore.py b/tool/skeleton/GitIgnore.py index 52f3f34..70c6509 100755 --- a/tool/skeleton/GitIgnore.py +++ b/tool/skeleton/GitIgnore.py @@ -2,35 +2,60 @@ # -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- """ -gitignore.py - minimal GitIgnore-style helper for Harmony projects +GitIgnore.py - minimal .gitignore-based helper for Harmony projects -GitIgnore instances track which parts of a project tree are governed by -per-directory '.gitignore' files (excluding the top-level one). - -Heuristic: +Behavior: 1. During initialization, traverse the project tree rooted at . - 2. If the top-level directory (the project root) has a '.gitignore' - file, ignore it for the purposes of this helper. + 2. Whenever a directory contains a '.gitignore' file, record: + - its relative directory path from the project root + - a list of regular expressions compiled from the patterns + in that '.gitignore' file + + These are stored in: + + self.rules: Dict[str, List[Pattern]] + + where the key is the directory RELATIVE to the project root: + "" -> project root (top-level .gitignore) + "src" -> src/.gitignore + "src/module" -> src/module/.gitignore + + 3. check() -> token: + + - is a path relative to the project root. + + - We compute all prefix directories of , including the + root (""), for example: + + path = "a/b/c.py" + prefixes = ["", "a", "a/b"] + + - For each prefix, if there are regexes stored for that directory, + we collect them. + + - We then test ALL collected regexes against the basename of + (the last component only). - 3. For any other directory in the tree that contains a file literally - named '.gitignore', record that directory's relative path (from the - project root) into an internal set — but only if none of its - prefixes are already present in the set. + - If ANY regex matches, return 'Ignore'. + Otherwise return 'Accept'. - 4. check() -> token: - - If ANY prefix of is found in the set, return 'Ignore'. - - Otherwise, return 'Accept'. +Notes: + + * We implement a simplified subset of .gitignore semantics suitable + for your current patterns and add a small base ignore set for + always-ignored names such as '.git'. """ from __future__ import annotations +import fnmatch import os -from typing import Set - -import skeleton +import re +from typing import Dict, List +import Harmony class GitIgnore: @@ -40,9 +65,19 @@ class GitIgnore: Attributes: project_path: Absolute path to the project root. - prefixes: - A set of relative directory paths (from project root) where a - '.gitignore' file exists, excluding the top-level project root. + + rules: + Mapping from relative directory path -> list of compiled regex + patterns derived from that directory's '.gitignore' file. + + Example: + rules[""] -> patterns from /.gitignore + rules["developer"] -> patterns from developer/.gitignore + + base_patterns: + List of compiled regex patterns applied to the basename of every + checked path, independent of any .gitignore file. Currently used + to always ignore '.git' directories. """ def __init__( @@ -51,106 +86,160 @@ class GitIgnore: ) -> None: """ Initialize a GitIgnore instance with a path to a project and - scan for per-directory '.gitignore' files (excluding the root). + scan for '.gitignore' files. """ self.project_path: str = os.path.abspath(project_path) - self.prefixes: Set[str] = set() + self.rules: Dict[str, List[re.Pattern]] = {} + + # Base patterns: always applied, regardless of .gitignore contents. + # These are matched against basenames only. + self.base_patterns: List[re.Pattern] = [ + re.compile(r"^\.git$") # ignore any basename == ".git" + ] self._scan_project() def _scan_project(self) -> None: """ - Traverse the project tree and populate self.prefixes with the - relative paths of directories (excluding the project root) - that contain a '.gitignore' file. + Traverse the project tree and populate self.rules with entries of + the form: - Only add a directory if none of its prefixes are already in - self.prefixes. This avoids redundant entries such as: + -> [Pattern, Pattern, ...] - tool_shared/third_party - tool_shared/third_party/upstream + where is the directory containing '.gitignore', relative + to the project root ("" for root). """ root = self.project_path for dirpath, dirnames, filenames in os.walk(root, topdown=True): - rel_dir = os.path.relpath(dirpath, root) + if ".gitignore" not in filenames: + continue - # Normalize '.' to empty for clarity + rel_dir = os.path.relpath(dirpath, root) if rel_dir == ".": rel_dir = "" - if ".gitignore" not in filenames: - continue + gitignore_path = os.path.join(dirpath, ".gitignore") + patterns = self._parse_gitignore_file(gitignore_path) - # Skip the top-level '.gitignore' (if any) - if rel_dir == "": - continue + if patterns: + if rel_dir not in self.rules: + self.rules[rel_dir] = [] + self.rules[rel_dir].extend(patterns) + + def _parse_gitignore_file( + self + ,gitignore_path: str + ) -> List[re.Pattern]: + """ + Parse a single '.gitignore' file into a list of compiled regex patterns. + + Simplified rules: + - Blank lines and lines starting with '#' are ignored. + - Lines containing '/' in the MIDDLE are currently ignored + (future extension). + - Lines ending with '/' are treated as directory name patterns: + '__pycache__/' -> pattern on basename '__pycache__' + - All patterns are treated as name globs and compiled via + fnmatch.translate(), to be matched against basenames only. + """ + patterns: List[re.Pattern] = [] - # Only add this directory if none of its prefixes are - # already present in self.prefixes. - parts = rel_dir.split(os.sep) - prefix = None - skip = False + try: + with open(gitignore_path, "r", encoding="utf-8") as f: + for raw_line in f: + line = raw_line.strip() - for part in parts: - if prefix is None: - prefix = part - else: - prefix = os.path.join(prefix, part) + # Skip comments and blank lines + if not line or line.startswith("#"): + continue - if prefix in self.prefixes: - skip = True - break + # Remove trailing '/' for directory patterns (e.g. '__pycache__/') + if line.endswith("/"): + line = line[:-1].strip() + if not line: + continue - if skip: - continue + # If there is still a '/' in the line, we do not support this + # pattern in this minimal implementation. + if "/" in line: + continue + + # Compile as a name glob -> regex + regex_text = fnmatch.translate(line) + patterns.append(re.compile(regex_text)) - self.prefixes.add(rel_dir) + except OSError: + # If the .gitignore cannot be read, just skip it. + return patterns + + return patterns def check( self ,path: str ) -> str: """ - Check a path against the recorded '.gitignore' directories. + Check a path against the collected .gitignore patterns. path: A path relative to the project root. Returns: - 'Ignore' if any prefix of is in self.prefixes, otherwise - 'Accept'. + 'Ignore' if any applicable pattern matches the basename of the path, + otherwise 'Accept'. """ - # Normalize the incoming path to a consistent form + # Normalize the incoming path norm = os.path.normpath(path) - # If the path is '.' or empty, treat as root-relative with no prefix + # If the path is '.' or empty, we accept it if norm in ("", "."): return "Accept" + basename = os.path.basename(norm) + + # First, apply base patterns (always applied). + for pat in self.base_patterns: + if pat.match(basename): + return "Ignore" + + # Build the list of directories that may contribute .gitignore rules. + # + # For path "a/b/c": + # prefixes: ["", "a", "a/b"] parts = norm.split(os.sep) - # Build incremental prefixes: 'a', 'a/b', 'a/b/c', ... + prefixes: List[str] = [""] prefix = None - for part in parts: + for part in parts[:-1]: if prefix is None: prefix = part else: prefix = os.path.join(prefix, part) + prefixes.append(prefix) - if prefix in self.prefixes: - return "Ignore" + # Collect all patterns from the applicable .gitignore directories + for rel_dir in prefixes: + dir_patterns = self.rules.get(rel_dir) + if not dir_patterns: + continue + + for pat in dir_patterns: + if pat.match(basename): + return "Ignore" return "Accept" def test_GitIgnore() -> int: """ - 1. Locate the Harmony project root using skeleton.where_is_Harmony(). + 1. Locate the Harmony project root using Harmony.where(). 2. Create a GitIgnore instance rooted at that path. - 3. Print a list of all directories (relative) that would be ignored. + 3. Print: + - directories that have .gitignore rules + - directories (relative) that would be ignored by check() """ - status, Harmony_root = skeleton.where_is_Harmony() + status, Harmony_root = Harmony.where() if status == "not-found": print("Harmony project not found; cannot test GitIgnore.") @@ -161,9 +250,18 @@ def test_GitIgnore() -> int: gi = GitIgnore(Harmony_root) - print("GitIgnore directories (relative to Harmony root):") - for rel in sorted(gi.prefixes): - print(rel) + print(".gitignore rule directories (relative to Harmony root):") + for rel_dir in sorted(gi.rules.keys()): + print(f" {rel_dir if rel_dir else '.'}") + + print("\nDirectories that would be ignored (relative to Harmony root):") + for dirpath, dirnames, filenames in os.walk(Harmony_root, topdown=True): + rel_dir = os.path.relpath(dirpath, Harmony_root) + if rel_dir == ".": + rel_dir = "" + + if gi.check(rel_dir) == "Ignore": + print(f" {rel_dir if rel_dir else '.'}") return 0 diff --git a/tool/skeleton/Harmony.py b/tool/skeleton/Harmony.py new file mode 100644 index 0000000..9385507 --- /dev/null +++ b/tool/skeleton/Harmony.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- + +""" +locate the project root +""" + +from __future__ import annotations + +import meta +import os +import sys +from typing import Any, Callable, Dict + +# where +# +# Context / assumptions: +# 1. This module lives somewhere under the Harmony tree, for example: +# /.../Harmony/tool/skeleton/skeleton.py +# 2. CLI.py is run from somewhere inside the same tree (or a clone). +# +# Search behavior: +# 1. Start from the directory containing this file. +# 2. Walk upward towards the filesystem root, with limits: +# a) Do not move up more than 5 levels. +# b) Stop immediately if the current directory contains a +# '.git' subdirectory. +# +# Result classification: +# status is one of: +# 'found' -> we found a directory whose basename is 'Harmony' +# 'different' -> we stopped at a directory that has a '.git' +# subdirectory, but its basename is not 'Harmony' +# 'not-found' -> we hit the 5-level limit or filesystem root +# without finding 'Harmony' or a '.git' directory +# +# Path: +# - In all cases, the returned path is the last directory inspected: +# * the 'Harmony' directory (status 'found'), or +# * the directory with '.git' (status 'different'), or +# * the directory at the 5-level limit / filesystem root +# (status 'not-found'). +# +# Debug printing: +# - If meta.debug_has("print_Harmony_root") is true, print: +# * "The Harmony project root found at: {path}" +# when status == 'found' +# * "Harmony not found, but found: {path}" +# when status == 'different' +# * "Harmony not found." +# when status == 'not-found' +def where() -> tuple[str, str]: + """ + Locate the Harmony root (or best guess). + + Returns: + (status, path) + """ + here = os.path.abspath(__file__) + d = os.path.dirname(here) + + harmony_root = None + status = "not-found" + + max_up = 5 + steps = 0 + + while True: + base = os.path.basename(d) + + # Case 1: exact 'Harmony' directory name + if base == "Harmony": + harmony_root = d + status = "found" + break + + # Case 2: stop at a directory that has a '.git' subdirectory + git_dir = os.path.join(d, ".git") + if os.path.isdir(git_dir): + harmony_root = d + if base == "Harmony": + status = "found" + else: + status = "different" + break + + parent = os.path.dirname(d) + + # Stop if we hit filesystem root + if parent == d: + harmony_root = d + status = "not-found" + break + + steps += 1 + if steps > max_up: + # Reached search depth limit; last inspected directory is d + harmony_root = d + status = "not-found" + break + + d = parent + + if harmony_root is None: + # Extremely defensive; in practice harmony_root will be set above. + harmony_root = d + + root_base = os.path.basename(harmony_root) + + # Warning to stderr if we are not literally in a 'Harmony' directory + if root_base != "Harmony": + sys.stderr.write( + f"WARNING: Harmony root basename is '{root_base}', expected 'Harmony'.\n" + ) + + if meta.debug_has("print_Harmony_root"): + if status == "found": + print(f"The Harmony project root found at: {harmony_root}") + elif status == "different": + print(f"Harmony not found, but found: {harmony_root}") + else: + print("Harmony not found.") + + return status, harmony_root + +def test_where() -> int: + """ + Simple test that prints the Harmony root using the debug flag. + """ + meta.debug_set("print_Harmony_root") + status, _root = where() + return 0 if status != "not-found" else 1 + diff --git a/tool/skeleton/doc.py b/tool/skeleton/doc.py index 4fee15a..d822a02 100644 --- a/tool/skeleton/doc.py +++ b/tool/skeleton/doc.py @@ -17,6 +17,7 @@ Grammar (informal): from __future__ import annotations +import meta import os import sys from typing import TextIO diff --git a/tool/skeleton/skeleton.py b/tool/skeleton/skeleton.py index 2c3b12f..d777a88 100755 --- a/tool/skeleton/skeleton.py +++ b/tool/skeleton/skeleton.py @@ -7,125 +7,410 @@ skeleton.py - helpers for working with the Harmony skeleton tree from __future__ import annotations +import meta import os import sys -import meta +from typing import Any, Callable, Dict, List, Set +from GitIgnore import GitIgnore +import Harmony - -# where_is_Harmony +# tree_dict_make / tree_dict_print +# +# Build a dictionary describing a project tree, respecting GitIgnore. # -# Context / assumptions: -# 1. This module lives somewhere under the Harmony tree, for example: -# /.../Harmony/tool/skeleton/skeleton.py -# 2. CLI.py is run from somewhere inside the same tree (or a clone). +# tree_dict_make(, ) -> tree_dict # -# Search behavior: -# 1. Start from the directory containing this file. -# 2. Walk upward towards the filesystem root, with limits: -# a) Do not move up more than 5 levels. -# b) Stop immediately if the current directory contains a -# '.git' subdirectory. +# () -> bignum | None # -# Result classification: -# status is one of: -# 'found' -> we found a directory whose basename is 'Harmony' -# 'different' -> we stopped at a directory that has a '.git' -# subdirectory, but its basename is not 'Harmony' -# 'not-found' -> we hit the 5-level limit or filesystem root -# without finding 'Harmony' or a '.git' directory +# Keys of tree_dict: +# - Relative paths from ; the root itself is stored under "". # -# Path: -# - In all cases, the returned path is the last directory inspected: -# * the 'Harmony' directory (status 'found'), or -# * the directory with '.git' (status 'different'), or -# * the directory at the 5-level limit / filesystem root -# (status 'not-found'). +# Values are dicts with: +# 1. 'mtime' : last modification time (float seconds) +# 2. 'node_type' : 'file', 'directory', or 'other' +# 3. 'dir_info' : 'not-a-directory', 'leaf', 'project-root' +# 4. 'checksum' : present only for file nodes when checksum_fn is +# not None # -# Debug printing: -# - If meta.debug_has("print_Harmony_root") is true, print: -# * "The Harmony project root found at: {path}" -# when status == 'found' -# * "Harmony not found, but found: {path}" -# when status == 'different' -# * "Harmony not found." -# when status == 'not-found' -def where_is_Harmony() -> tuple[str, str]: +# Traversal: +# - Directories whose relative path GitIgnore.check() marks as +# 'Ignore' are included in tree_dict but not traversed further. +def tree_dict_make( + path: str + ,checksum_fn: Callable[[str], int] | None +) -> Dict[str, Dict[str, Any]]: """ - Locate the Harmony root (or best guess). + Build a tree_dict for the subtree rooted at , respecting GitIgnore. + + Semantics (current): + * Any path (directory or file) for which GitIgnore.check() + returns 'Ignore' is completely omitted from the tree_dict. + * The root directory ('') is always included. + """ + root = os.path.abspath(path) + gi = GitIgnore(root) + + tree_dict: Dict[str, Dict[str, Any]] = {} + + for dirpath, dirnames, filenames in os.walk(root, topdown=True): + rel_dir = os.path.relpath(dirpath, root) + if rel_dir == ".": + rel_dir = "" + + # Skip ignored directories (except the root). + # We do this BEFORE recording the directory, so ignored dirs + # like '.git' or '__pycache__' never appear in tree_dict. + if rel_dir != "" and gi.check(rel_dir) == "Ignore": + dirnames[:] = [] # do not descend + continue + + # Record the directory node itself + dir_abs = dirpath + dir_mtime = os.path.getmtime(dir_abs) + dir_node_type = "directory" + + if rel_dir == "": + dir_info = "project-root" + else: + dir_info = "leaf" + + tree_dict[rel_dir] = { + "mtime": dir_mtime + ,"node_type": dir_node_type + ,"dir_info": dir_info + } + + # For non-ignored directories, record files within + for name in filenames: + abs_path = os.path.join(dirpath, name) + if rel_dir == "": + rel_path = name + else: + rel_path = os.path.join(rel_dir, name) + + # Filter files via GitIgnore as well. + if gi.check(rel_path) == "Ignore": + continue + + if os.path.isfile(abs_path): + node_type = "file" + elif os.path.isdir(abs_path): + node_type = "directory" + else: + node_type = "other" + + mtime = os.path.getmtime(abs_path) + + if node_type == "directory": + if rel_path == "": + dir_info_f = "project-root" + else: + dir_info_f = "leaf" + else: + dir_info_f = "not-a-directory" + + info: Dict[str, Any] = { + "mtime": mtime + ,"node_type": node_type + ,"dir_info": dir_info_f + } + + if node_type == "file" and checksum_fn is not None: + info["checksum"] = checksum_fn(abs_path) + + tree_dict[rel_path] = info + + if meta.debug_has("tree_dict_print"): + tree_dict_print(tree_dict) + + return tree_dict + +def tree_dict_print( + tree_dict: Dict[str, Dict[str, Any]] +) -> None: + """ + Pretty-print a tree_dict produced by tree_dict_make(). + """ + print("Tree dictionary contents:") + for rel_path in sorted(tree_dict.keys()): + info = tree_dict[rel_path] + display_path = rel_path if rel_path != "" else "." + + parts = [ + f"path={display_path}" + ,f"type={info.get('node_type')}" + ,f"mtime={info.get('mtime')}" + ,f"dir={info.get('dir_info')}" + ] + + if "checksum" in info: + parts.append(f"checksum={info['checksum']}") + + print(" " + ", ".join(parts)) + + +def tree_dict_A_minus_B( + A: Dict[str, Dict[str, Any]] + ,B: Dict[str, Dict[str, Any]] +) -> Dict[str, Dict[str, Any]]: + """ + Compute the set difference of two tree_dicts at the key level: + + Result = A \\ B + + That is, return a new tree_dict containing only those entries whose + keys are present in A but NOT present in B. + + Notes: + * We treat the key sets as abstract sets; values are copied from A. + * A and B are assumed to have relative-path keys built from + independent scans (possibly with different GitIgnore instances). + The contract that the underlying filesystem trees do not overlap + is irrelevant here because we compare only the relative keys. + """ + result: Dict[str, Dict[str, Any]] = {} + + B_keys = set(B.keys()) + + for key, info in A.items(): + if key not in B_keys: + result[key] = info + + if meta.debug_has("tree_dict_A_minus_B"): + tree_dict_print(result) + + return result + +def tree_dict_in_between_and_below( + A: Dict[str, Dict[str, Any]] + ,B: Dict[str, Dict[str, Any]] +) -> tuple[Dict[str, Dict[str, Any]], Dict[str, Dict[str, Any]]]: + """ + Partition nodes of B into two topology-based sets relative to A: + + 1. in_between: + Nodes in B that lie under at least one directory node in A, + but do NOT lie under any leaf directory of A. + + 2. below: + Nodes in B that lie under at least one leaf directory of A. + + Definitions (relative to A's directory topology): + + - A directory node in A is any key whose info['node_type'] == 'directory'. + + - A leaf directory in A is a directory that has no *other* directory + in A as a proper descendant. The project root ('') is therefore + never a leaf (it always has descendant directories if the tree is + non-trivial). + + - “Lies under”: + * For a path p in B, we look at the chain of directory ancestors + (including the root "") and, if p itself is a directory, p + itself. Any of those that appear as directory keys in A are + considered directory ancestors in A. + + * If any of those ancestors is a leaf in A, p goes to 'below'. + Otherwise, if there is at least one directory ancestor in A, + p goes to 'in_between'. + + - Nodes in B that do not lie under any directory in A are ignored. Returns: - (status, path) + (in_between_dict, below_dict), both keyed like B and containing + copies of the info dicts from B. """ - here = os.path.abspath(__file__) - d = os.path.dirname(here) - - harmony_root = None - status = "not-found" - - max_up = 5 - steps = 0 - - while True: - base = os.path.basename(d) - - # Case 1: exact 'Harmony' directory name - if base == "Harmony": - harmony_root = d - status = "found" - break - - # Case 2: stop at a directory that has a .git subdirectory - git_dir = os.path.join(d, ".git") - if os.path.isdir(git_dir): - harmony_root = d - if base == "Harmony": - status = "found" + # 1. Collect all directory keys from A + A_dir_keys: Set[str] = set( + key for key, info in A.items() + if info.get("node_type") == "directory" + ) + + # 2. Compute leaf directories in A: + # leaf_dir = directory key with no other directory key as a proper descendant + leaf_dirs: Set[str] = set() + + for d in A_dir_keys: + # The root "" is never treated as a leaf; it always has descendants + if d == "": + continue + + has_child_dir = False + prefix = d + os.sep + + for other in A_dir_keys: + if other == d: + continue + if other.startswith(prefix): + has_child_dir = True + break + + if not has_child_dir: + leaf_dirs.add(d) + + in_between: Dict[str, Dict[str, Any]] = {} + below: Dict[str, Dict[str, Any]] = {} + + for key, info in B.items(): + # Skip B's root; it is typically not useful in this partitioning. + if key in ("", "."): + continue + + parts = key.split(os.sep) + + # Build directory ancestor chain for this B node. + # If the node itself is a directory, include its full path; + # otherwise include directories up to its parent. + node_is_dir = (info.get("node_type") == "directory") + + ancestors: List[str] = [""] + prefix = None + + if node_is_dir: + upto = parts # include the last component + else: + upto = parts[:-1] # only directories above the basename + + for part in upto: + if prefix is None: + prefix = part else: - status = "different" - break - - parent = os.path.dirname(d) - - # Stop if we hit filesystem root - if parent == d: - harmony_root = d - status = "not-found" - break - - steps += 1 - if steps > max_up: - # Reached search depth limit; last inspected directory is d - harmony_root = d - status = "not-found" - break - - d = parent - - if harmony_root is None: - # Extremely defensive; in practice harmony_root will be set above. - harmony_root = d - - root_base = os.path.basename(harmony_root) - - # Warning to stderr if we are not literally in a 'Harmony' directory - if root_base != "Harmony": - sys.stderr.write( - f"WARNING: Harmony root basename is '{root_base}', expected 'Harmony'.\n" - ) - - if meta.debug_has("print_Harmony_root"): - if status == "found": - print(f"The Harmony project root found at: {harmony_root}") - elif status == "different": - print(f"Harmony not found, but found: {harmony_root}") + prefix = os.path.join(prefix, part) + ancestors.append(prefix) + + # Filter ancestors to those that exist as directories in A + ancestors_in_A = [d for d in ancestors if d in A_dir_keys] + + if not ancestors_in_A: + # This B node is not under any directory from A; ignore it. + continue + + # Any leaf ancestor in A? + has_leaf_ancestor = any(d in leaf_dirs for d in ancestors_in_A) + + if has_leaf_ancestor: + below[key] = info else: - print("Harmony not found.") + in_between[key] = info + + if meta.debug_has("tree_dict_in_between_and_below"): + tree_dict_print(result) + + return in_between, below + + +def tree_dict_newer( + A: Dict[str, Dict[str, Any]] + ,B: Dict[str, Dict[str, Any]] +) -> Dict[str, Dict[str, Any]]: + """ + Return a dictionary of nodes from B that are newer than their + corresponding nodes in A. + + For each key k: + + - If k exists in both A and B, and + - B[k]['mtime'] > A[k]['mtime'], + + then k is included in the result with value B[k]. + + Keys that are only in B (not in A) are ignored here; use a separate + set-difference function for "only-in-B" detection. + """ + result: Dict[str, Dict[str, Any]] = {} + + for key, info_B in B.items(): + info_A = A.get(key) + if info_A is None: + continue + + mtime_A = info_A.get("mtime") + mtime_B = info_B.get("mtime") + + if mtime_A is None or mtime_B is None: + continue + + if mtime_B > mtime_A: + result[key] = info_B + + if meta.debug_has("tree_dict_newer"): + tree_dict_print(result) + + return result + +def tree_dict_older( + A: Dict[str, Dict[str, Any]] + ,B: Dict[str, Dict[str, Any]] +) -> Dict[str, Dict[str, Any]]: + """ + Return a dictionary of nodes from B that are older than their + corresponding nodes in A. + + For each key k: + + - If k exists in both A and B, and + - B[k]['mtime'] < A[k]['mtime'], + + then k is included in the result with value B[k]. + + Keys that are only in B (not in A) are ignored here. + """ + result: Dict[str, Dict[str, Any]] = {} + + for key, info_B in B.items(): + info_A = A.get(key) + if info_A is None: + continue + + mtime_A = info_A.get("mtime") + mtime_B = info_B.get("mtime") + + if mtime_A is None or mtime_B is None: + continue + + if mtime_B < mtime_A: + result[key] = info_B + + if meta.debug_has("tree_dict_older"): + tree_dict_print(result) + + return result + + +def test_tree_dict() -> int: + """ + Test helper for tree_dict_make: + + 1. Locate the Harmony project root. + 2. Set the debug flag 'tree_dict_print'. + 3. Call tree_dict_make() on the Harmony root with no checksum + function (checksum_fn=None). + + The debug flag causes tree_dict_print() to be called automatically + inside tree_dict_make(). + """ + status, Harmony_root = Harmony.where() + + if status == "not-found": + print("Harmony project not found; cannot test tree_dict_make.") + return 1 + + if status == "different": + print("Warning: Harmony not found, using nearest .git directory for tree_dict_make test.") + + meta.debug_set("tree_dict_print") + + _tree = tree_dict_make(Harmony_root, None) + + return 0 - return status, harmony_root -def test(): - meta.debug_set("print_Harmony_root") - status,Harmony_root = where_is_Harmony() +def test() -> int: + flag = 1 + if not test_tree_dict(): + print("fail: test_tree_dict") + flag = 0 + return flag if __name__ == "__main__": raise SystemExit(test()) diff --git a/tool/skeleton/skeleton_CLI.py b/tool/skeleton/skeleton_CLI.py new file mode 100644 index 0000000..b66b369 --- /dev/null +++ b/tool/skeleton/skeleton_CLI.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*- + +""" +skeleton_CLI.py - generic CLI for skeleton A,B tree_dict functions + +Usage: + skeleton_CLI.py + +Where is one of: + + - tree_dict_A_minus_B + - tree_dict_in_between_and_below + - tree_dict_newer + - tree_dict_older + +Behavior: + 1. Build tree_dict A from . + 2. Build tree_dict B from . + 3. Enable the debug flag named . + 4. Call skeleton.(A, B). + +Any printing is expected to be done by the called function when the +corresponding debug flag is set. +""" + +from __future__ import annotations + +import os +import sys +from typing import Sequence + +import meta +import skeleton + + +FUNCTIONS = { + "tree_dict_A_minus_B": skeleton.tree_dict_A_minus_B + ,"tree_dict_in_between_and_below": skeleton.tree_dict_in_between_and_below + ,"tree_dict_newer": skeleton.tree_dict_newer + ,"tree_dict_older": skeleton.tree_dict_older +} + + +def _print_usage(prog: str) -> None: + print(f"Usage: {prog} ") + print("Where is one of:") + for name in sorted(FUNCTIONS.keys()): + print(f" - {name}") + + +def CLI(argv: Sequence[str] | None = None) -> int: + if argv is None: + argv = sys.argv[1:] + + prog = os.path.basename(sys.argv[0]) if sys.argv else "skeleton_CLI.py" + + if len(argv) != 3 or argv[0] in ("-h", "--help"): + _print_usage(prog) + return 1 + + func_name = argv[0] + A_root = argv[1] + B_root = argv[2] + + func = FUNCTIONS.get(func_name) + if func is None: + print(f"{prog}: unknown function_name: {func_name}") + _print_usage(prog) + return 2 + + if not os.path.isdir(A_root): + print(f"{prog}: {A_root}: not a directory") + return 3 + + if not os.path.isdir(B_root): + print(f"{prog}: {B_root}: not a directory") + return 4 + + # Build tree_dicts + A = skeleton.tree_dict_make(A_root, None) + B = skeleton.tree_dict_make(B_root, None) + + # Enable debug flag with the same name as the function + meta.debug_set(func_name) + + # Call the function; any printing is done via debug hooks + _result = func(A, B) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(CLI()) diff --git a/tool/skeleton/temp.txt b/tool/skeleton/temp.txt new file mode 100644 index 0000000..fb4aebf --- /dev/null +++ b/tool/skeleton/temp.txt @@ -0,0 +1,36 @@ +Perhaps we are now ready to summit. + +We will add the module 'command.py' + +functions: help, + +The top level function, `dispatch`, will take the list. It will then dispatch a distinct function for each command. + +Each command specific function will use the functions we have written to accomplish the work of the command. See notes below. + +``` +Usage: + check * [] + +Where: + :: | | + + :: version | help | usage + :: environment + :: structure | import | export | suspicious | addendum | all +``` +The meaning of these commands: + +structure: differences in directory structure, directories in A - B + +import: a list of shell copy commands that would copy newer nodes in B into A, or nodes that are not in B into A. Editing this list, then running the shell commands will be helpful for updating the Harmony skeleton (A). + +export: a list of shell copy commands that would copy newer nodes in A into B, or nodes in A that are not in B. Comes from the older list. Editing this list, then running the shell commands will be helpful for keeping the skeleton in B up to date. + +suspicious: basically the InBetween list. Why did the user put things into areas that are part of the skeleton instead of under leaf directories? tools, source code, tests, etc. all have leaf directories for adding things to. What are these files? Perhaps proposed additions to the skeleton? Perhaps bad usage of the skeleton (in general use scenarios this is more likely). + +addendum: this is the work that has been added to the project in proper places. It is interesting to see the work that has been done on the project. + +all: runs structure, import, export, suspicious, and addendum + +it seems I left an analysis out, but it doesn't come to mind at the moment. Can you think of it?