|
1 | 1 | from __future__ import annotations
|
2 | 2 | import inspect
|
3 |
| -from typing import overload, Literal |
4 |
| -from weakref import WeakKeyDictionary |
5 | 3 | from dataclasses import dataclass, field
|
6 | 4 |
|
7 | 5 | _MAPPING = {"-": 0, "a": 1, "m": 2, "v": 4}
|
@@ -59,149 +57,173 @@ def __repr__(self):
|
59 | 57 | return "".join(state)
|
60 | 58 |
|
61 | 59 |
|
62 |
| -NODE_CHILD_MAP: WeakKeyDictionary['Node', dict[str, 'Node']] = WeakKeyDictionary() |
| 60 | +# NODE_CHILD_MAP: WeakKeyDictionary['Node', dict[str, 'Node']] = WeakKeyDictionary() |
| 61 | +INDEX_MAP: dict[str, Node] = {} |
63 | 62 |
|
64 | 63 |
|
65 | 64 | @dataclass(init=False, repr=False, eq=True, unsafe_hash=True)
|
66 | 65 | class Node:
|
67 | 66 | name: str
|
68 |
| - parent: Node | None |
69 |
| - isdir: bool |
70 |
| - content: dict[str, str] = field(default_factory=dict, compare=False, hash=False) |
71 |
| - |
72 |
| - @staticmethod |
73 |
| - def from_path(path: str, root: Node | None = None) -> Node: |
74 |
| - _root = root or ROOT |
75 |
| - if _root.isfile: |
76 |
| - raise ValueError("root is a file") |
77 |
| - parts = path.split("/") |
78 |
| - if not parts[0]: |
79 |
| - parts.pop(0) |
80 |
| - _root = ROOT |
81 |
| - elif (count := parts[0].count(".")) == len(parts[0]): |
82 |
| - if count != 2: |
83 |
| - parts.pop(0) |
84 |
| - elif not _root.parent: |
85 |
| - raise ValueError("root has no parent") |
86 |
| - else: |
87 |
| - _root = _root.parent |
88 |
| - end = parts.pop(-1) |
89 |
| - node = _root |
90 |
| - for part in parts: |
91 |
| - node = Node(part, node, True) |
92 |
| - if not end: |
93 |
| - node.isdir = True |
94 |
| - NODE_CHILD_MAP[node] = {} |
95 |
| - return node |
96 |
| - return Node(end, node) |
| 67 | + content: dict[str, str] = field(compare=False, hash=False) |
97 | 68 |
|
98 | 69 | def __init__(
|
99 | 70 | self,
|
100 | 71 | name: str,
|
101 |
| - parent: Node | None = None, |
102 |
| - isdir: bool = False, |
| 72 | + content: dict[str, str] | None = None, |
103 | 73 | ):
|
104 | 74 | _in_current_module = inspect.currentframe().f_back.f_globals["__name__"] == __name__ # type: ignore
|
105 | 75 | if not _in_current_module and not name:
|
106 | 76 | raise ValueError("name is required")
|
107 | 77 | self.name = name
|
108 |
| - self.parent = parent if _in_current_module else (parent or ROOT) |
109 |
| - self.isdir = isdir |
110 |
| - if isdir: |
111 |
| - NODE_CHILD_MAP[self] = {} |
112 |
| - if self.parent is not None: |
113 |
| - if self.parent.isfile: |
114 |
| - raise ValueError(f"parent {self.parent} is a file") |
115 |
| - NODE_CHILD_MAP.setdefault(self.parent, {})[self.name] = self |
116 |
| - |
117 |
| - def move(self, new_parent: Node): |
118 |
| - if new_parent.isfile: |
119 |
| - raise ValueError(f"new parent {new_parent} is a file") |
120 |
| - if self.parent is not None: |
121 |
| - NODE_CHILD_MAP[self.parent].pop(self.name) |
122 |
| - self.parent = new_parent |
123 |
| - NODE_CHILD_MAP[self.parent][self.name] = self |
124 |
| - |
125 |
| - def _get_once(self, name: str): |
126 |
| - if self not in NODE_CHILD_MAP: |
127 |
| - return None |
128 |
| - if name in NODE_CHILD_MAP[self]: |
129 |
| - return NODE_CHILD_MAP[self][name] |
130 |
| - if name in {"$self", ".", ""}: |
131 |
| - return self |
132 |
| - return self.parent if name in {"$parent", ".."} else None |
133 |
| - |
134 |
| - @overload |
135 |
| - def get(self, path: str) -> Node: |
136 |
| - ... |
137 |
| - |
138 |
| - @overload |
139 |
| - def get(self, path: str, missing_ok: Literal[True]) -> Node | None: |
140 |
| - ... |
141 |
| - |
142 |
| - @overload |
143 |
| - def get(self, path: str, missing_ok: Literal[False]) -> Node: |
144 |
| - ... |
145 |
| - |
146 |
| - def get(self, path: str, missing_ok: bool = False) -> Node | None: |
147 |
| - if not path: |
148 |
| - return self |
149 |
| - if "/" not in path: |
150 |
| - if (res := self._get_once(path)) is None and not missing_ok: |
151 |
| - raise KeyError(path) |
152 |
| - return res |
153 |
| - parts = path.split("/") |
154 |
| - if not parts[0]: |
155 |
| - return ROOT.get("/".join(parts[1:]), missing_ok) # type: ignore |
156 |
| - if (count := parts[0].count(".")) == len(parts[0]) and count > 2: |
157 |
| - parts[0] = "." |
158 |
| - node = self |
159 |
| - for part in parts: |
160 |
| - node = node._get_once(part) |
161 |
| - if node is None: |
162 |
| - if not missing_ok: |
163 |
| - raise KeyError("/".join(parts[: parts.index(part) + 1])) |
164 |
| - return None |
165 |
| - return node |
166 |
| - |
167 |
| - def __getitem__(self, name: str): |
168 |
| - if res := self.get(name): |
169 |
| - return res |
170 |
| - raise KeyError(name) |
171 |
| - |
172 |
| - def __contains__(self, name: str): |
173 |
| - return name in NODE_CHILD_MAP[self] |
| 78 | + self.content = content or {} |
| 79 | + # self.name = name |
| 80 | + # self.parent = parent if _in_current_module else (parent or ROOT) |
| 81 | + # self.isdir = isdir |
| 82 | + # if isdir: |
| 83 | + # NODE_CHILD_MAP[self] = {} |
| 84 | + # if self.parent is not None: |
| 85 | + # if self.parent.isfile: |
| 86 | + # raise ValueError(f"parent {self.parent} is a file") |
| 87 | + # NODE_CHILD_MAP.setdefault(self.parent, {})[self.name] = self |
174 | 88 |
|
175 |
| - def __iter__(self): |
176 |
| - return iter(NODE_CHILD_MAP[self].values()) |
| 89 | + @property |
| 90 | + def isdir(self): |
| 91 | + return self.content.get("$type") == "dir" |
177 | 92 |
|
178 |
| - def set(self, node: Node): |
179 |
| - node.move(self) |
| 93 | + @property |
| 94 | + def isfile(self): |
| 95 | + return self.content.get("$type") == "file" |
180 | 96 |
|
181 | 97 | @property
|
182 | 98 | def path(self):
|
183 |
| - if self.parent is None: |
184 |
| - return "/" |
185 |
| - path = f"'{self.name}'" if " " in self.name else self.name |
186 |
| - node = self |
187 |
| - while node.parent: |
188 |
| - node = node.parent |
189 |
| - path = f"{node.name}/{path}" |
190 |
| - return f"{path}/" if self.isdir else path |
191 |
| - |
192 |
| - def __repr__(self): |
193 |
| - return f"Node({self.path})" |
| 99 | + return self.content.get("$path", self.name) |
194 | 100 |
|
195 | 101 | @property
|
196 |
| - def isfile(self): |
197 |
| - return not self.isdir |
| 102 | + def parent(self): |
| 103 | + return self.content.get("$parent") |
198 | 104 |
|
199 |
| - def __truediv__(self, other: str): |
200 |
| - if self.isfile: |
201 |
| - self.isdir = True |
202 |
| - NODE_CHILD_MAP[self] = {} |
203 |
| - return Node.from_path(other, self) |
| 105 | + def exist(self): |
| 106 | + return self.path in INDEX_MAP |
204 | 107 |
|
| 108 | + def mkdir( |
| 109 | + self, |
| 110 | + path: str, |
| 111 | + content: dict[str, str] | None = None, |
| 112 | + *, |
| 113 | + exist_ok: bool = False, |
| 114 | + parents: bool = False, |
| 115 | + ): |
| 116 | + return mkdir(path, self, content, exist_ok=exist_ok, parents=parents) |
| 117 | + |
| 118 | + def touch( |
| 119 | + self, |
| 120 | + path: str, |
| 121 | + content: dict[str, str] | None = None, |
| 122 | + *, |
| 123 | + exist_ok: bool = False, |
| 124 | + parents: bool = False, |
| 125 | + ): |
| 126 | + return touch(path, self, content, exist_ok=exist_ok, parents=parents) |
205 | 127 |
|
206 |
| -ROOT = Node("", None, True) |
207 |
| -NODE_CHILD_MAP[ROOT] = {} |
| 128 | + def __str__(self): |
| 129 | + return self.name |
| 130 | + |
| 131 | + def __repr__(self): |
| 132 | + return f"{'DIR' if self.isdir else 'FILE'}({self.name!r})" |
| 133 | + |
| 134 | + |
| 135 | +ROOT = Node("/", {"$type": "dir", "$path": ""}) |
| 136 | + |
| 137 | + |
| 138 | +def split_path(path: str, base: Node) -> tuple[Node, list[str]]: |
| 139 | + if not path: |
| 140 | + raise ValueError("path is required") |
| 141 | + if path == "/": |
| 142 | + return ROOT, [] |
| 143 | + parts = path.split("/") |
| 144 | + first = parts[0] |
| 145 | + if not first: # absolute path |
| 146 | + parts.pop(0) |
| 147 | + return ROOT, parts |
| 148 | + if first == ".": # current node |
| 149 | + return base, parts[1:] |
| 150 | + _base = base |
| 151 | + while first == "..": # parent node |
| 152 | + parts.pop(0) |
| 153 | + first = parts[0] |
| 154 | + if not _base.parent: |
| 155 | + raise ValueError("base has no parent") |
| 156 | + _base = _base.parent |
| 157 | + return _base, parts |
| 158 | + |
| 159 | + |
| 160 | +def mkdir( |
| 161 | + path: str, |
| 162 | + base: Node = ROOT, |
| 163 | + content: dict[str, str] | None = None, |
| 164 | + *, |
| 165 | + exist_ok: bool = False, |
| 166 | + parents: bool = False, |
| 167 | +): |
| 168 | + if base.isfile: |
| 169 | + raise ValueError("base is a file") |
| 170 | + _base, parts = split_path(path, base) |
| 171 | + if not parts: |
| 172 | + raise ValueError("path is required") |
| 173 | + if len(parts) == 1: |
| 174 | + _path = f"{_base.path}/{parts[0]}" |
| 175 | + if not exist_ok and _path in INDEX_MAP: |
| 176 | + raise FileExistsError(_path) |
| 177 | + node = Node(parts[0], {"$type": "dir", "$path": _path, "$parent": _base, **(content or {})}) |
| 178 | + INDEX_MAP[_path] = node |
| 179 | + return node |
| 180 | + if not parents: |
| 181 | + raise FileNotFoundError(parts[0]) |
| 182 | + for part in parts[:-1]: |
| 183 | + _path = f"{_base.path}/{part}" |
| 184 | + if not exist_ok and _path in INDEX_MAP: |
| 185 | + raise FileExistsError(_path) |
| 186 | + _base = Node(part, {"$type": "dir", "$path": _path, "$parent": _base}) |
| 187 | + INDEX_MAP[_path] = _base |
| 188 | + _path = f"{_base.path}/{parts[-1]}" |
| 189 | + if not exist_ok and _path in INDEX_MAP: |
| 190 | + raise FileExistsError(_path) |
| 191 | + node = Node(parts[-1], {"$type": "dir", "$path": _path, "$parent": _base, **(content or {})}) |
| 192 | + INDEX_MAP[_path] = node |
| 193 | + return node |
| 194 | + |
| 195 | + |
| 196 | +def touch( |
| 197 | + path: str, |
| 198 | + base: Node = ROOT, |
| 199 | + content: dict[str, str] | None = None, |
| 200 | + *, |
| 201 | + exist_ok: bool = False, |
| 202 | + parents: bool = False, |
| 203 | +): |
| 204 | + if base.isfile: |
| 205 | + raise ValueError("base is a file") |
| 206 | + _base, parts = split_path(path, base) |
| 207 | + if not parts: |
| 208 | + raise ValueError("path is required") |
| 209 | + if len(parts) == 1: |
| 210 | + _path = f"{_base.path}/{parts[0]}" |
| 211 | + if not exist_ok and _path in INDEX_MAP: |
| 212 | + raise FileExistsError(_path) |
| 213 | + node = Node(parts[0], {"$type": "file", "$path": _path, "$parent": _base, **(content or {})}) |
| 214 | + INDEX_MAP[_path] = node |
| 215 | + return node |
| 216 | + if not parents: |
| 217 | + raise FileNotFoundError(parts[0]) |
| 218 | + for part in parts[:-1]: |
| 219 | + _path = f"{_base.path}/{part}" |
| 220 | + if not exist_ok and _path in INDEX_MAP: |
| 221 | + raise FileExistsError(_path) |
| 222 | + _base = Node(part, {"$type": "dir", "$path": _path, "$parent": _base}) |
| 223 | + INDEX_MAP[_path] = _base |
| 224 | + _path = f"{_base.path}/{parts[-1]}" |
| 225 | + if not exist_ok and _path in INDEX_MAP: |
| 226 | + raise FileExistsError(_path) |
| 227 | + node = Node(parts[-1], {"$type": "file", "$path": _path, "$parent": _base, **(content or {})}) |
| 228 | + INDEX_MAP[_path] = node |
| 229 | + return node |
0 commit comments