Skip to content

Commit 2a21470

Browse files
committed
💥 changed Node
1 parent 7cfea7b commit 2a21470

File tree

3 files changed

+171
-165
lines changed

3 files changed

+171
-165
lines changed

src/arclet/cithun/monitor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass, field
4-
from .node import Node, NodeState, ROOT, NODE_CHILD_MAP
4+
from .node import Node, NodeState, ROOT
55
from .ctx import Context
66
from .owner import Owner, Group, User
77

src/arclet/cithun/node.py

Lines changed: 146 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
from __future__ import annotations
22
import inspect
3-
from typing import overload, Literal
4-
from weakref import WeakKeyDictionary
53
from dataclasses import dataclass, field
64

75
_MAPPING = {"-": 0, "a": 1, "m": 2, "v": 4}
@@ -59,149 +57,173 @@ def __repr__(self):
5957
return "".join(state)
6058

6159

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] = {}
6362

6463

6564
@dataclass(init=False, repr=False, eq=True, unsafe_hash=True)
6665
class Node:
6766
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)
9768

9869
def __init__(
9970
self,
10071
name: str,
101-
parent: Node | None = None,
102-
isdir: bool = False,
72+
content: dict[str, str] | None = None,
10373
):
10474
_in_current_module = inspect.currentframe().f_back.f_globals["__name__"] == __name__ # type: ignore
10575
if not _in_current_module and not name:
10676
raise ValueError("name is required")
10777
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
17488

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"
17792

178-
def set(self, node: Node):
179-
node.move(self)
93+
@property
94+
def isfile(self):
95+
return self.content.get("$type") == "file"
18096

18197
@property
18298
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)
194100

195101
@property
196-
def isfile(self):
197-
return not self.isdir
102+
def parent(self):
103+
return self.content.get("$parent")
198104

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
204107

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)
205127

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

tests/node.py

Lines changed: 24 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
from arclet.cithun.node import Node, ROOT, NODE_CHILD_MAP, Selector
1+
from arclet.cithun.node import Node, ROOT, mkdir, INDEX_MAP
22
from pprint import pprint
33

4-
FOOD = Node("food", isdir=True)
5-
FRUIT = FOOD / "fruit/"
6-
VEGETABLE = Node("vegetable", FOOD, isdir=True)
7-
APPLE = Node("apple", FRUIT)
8-
BANANA = Node("banana", FRUIT)
9-
SMALL_TOMATO = VEGETABLE / "tomato" / "small tomato"
4+
FOOD = mkdir("food")
5+
FRUIT = FOOD.mkdir("fruit")
6+
VEGETABLE = FOOD.mkdir("vegetable")
7+
APPLE = FRUIT.touch("apple")
8+
BANANA = FRUIT.touch("banana")
9+
SMALL_TOMATO = VEGETABLE.touch("tomato/small tomato", parents=True)
1010

1111
"""
1212
# NodeTree
@@ -20,36 +20,20 @@
2020
└── small tomato
2121
"""
2222

23-
print(ROOT)
24-
print(FOOD)
25-
print(FRUIT)
26-
print(VEGETABLE)
27-
print(APPLE)
28-
print(BANANA)
29-
print(SMALL_TOMATO)
30-
pprint(NODE_CHILD_MAP[FOOD])
31-
32-
FRUIT.move(VEGETABLE)
33-
print("=============================")
34-
pprint(NODE_CHILD_MAP[FOOD])
35-
pprint(NODE_CHILD_MAP[VEGETABLE])
36-
37-
"""
38-
/
39-
└── food
40-
└── vegetable
41-
├── fruit
42-
│ ├── apple
43-
│ └── banana
44-
└── tomato
45-
└── small tomato
46-
"""
47-
48-
try:
49-
Node("")
50-
except ValueError as e:
51-
print(e)
52-
53-
print(FOOD.get("./vegetable"))
54-
print(ROOT.get("/food/vegetable/tomato/small tomato"))
55-
print(ROOT.get("/food/vegetable/tomato/small tomato/"))
23+
pprint(INDEX_MAP)
24+
#
25+
# FRUIT.move(VEGETABLE)
26+
# print("=============================")
27+
# pprint(NODE_CHILD_MAP[FOOD])
28+
# pprint(NODE_CHILD_MAP[VEGETABLE])
29+
#
30+
# """
31+
# /
32+
# └── food
33+
# └── vegetable
34+
# ├── fruit
35+
# │ ├── apple
36+
# │ └── banana
37+
# └── tomato
38+
# └── small tomato
39+
# """

0 commit comments

Comments
 (0)