From 748a62d5d99039a8d9558deab79e3d002f0e2d8e Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sat, 2 May 2026 09:47:45 -0500 Subject: [PATCH 1/7] refactor: add custom repr for file system --- src/mega/filesystem.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/mega/filesystem.py b/src/mega/filesystem.py index 0008ea4..fd506d3 100644 --- a/src/mega/filesystem.py +++ b/src/mega/filesystem.py @@ -82,6 +82,16 @@ class SimpleFileSystem(_NodeWalker, _DictDumper): file_count: int folder_count: int + def __repr__(self) -> str: + def fields(): + for name, node in [("root", self.root), ("inbox", self.inbox), ("trash_bin", self.trash_bin)]: + yield name, node.id if node is not None else None + yield "files", self.file_count + yield "folders", self.file_count + + all_fields = "".join(f"{name}={value!r}" for name, value in fields()) + return f"<{type(self).__name__}({all_fields})>" + def __len__(self) -> int: return len(self.nodes) From 0010ba89a629a8e4fc5c0807fc801a4f81cff2a9 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sat, 2 May 2026 09:51:00 -0500 Subject: [PATCH 2/7] refactor: remove redundant properties --- src/mega/filesystem.py | 64 ++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 40 deletions(-) diff --git a/src/mega/filesystem.py b/src/mega/filesystem.py index fd506d3..88549e5 100644 --- a/src/mega/filesystem.py +++ b/src/mega/filesystem.py @@ -48,12 +48,15 @@ def walk(node_id: str, current_path: PurePosixPath) -> Generator[tuple[NodeID, P @dataclasses.dataclass(slots=True, frozen=True, kw_only=True, weakref_slot=True) class _NodeWalker: - _nodes: MappingProxyType[NodeID, Node] = dataclasses.field(repr=False) - _children: MappingProxyType[NodeID, tuple[NodeID, ...]] = dataclasses.field(repr=False) + nodes: MappingProxyType[NodeID, Node] = dataclasses.field(repr=False) + "A mapping of every node" + + children: MappingProxyType[NodeID, tuple[NodeID, ...]] = dataclasses.field(repr=False) + "A mapping of nodes to their inmediate children" def _ls(self, node_id: NodeID, *, recursive: bool) -> Iterable[NodeID]: """Get ID of every child of this node""" - for child_id in self._children.get(node_id, ()): + for child_id in self.children.get(node_id, ()): yield child_id if recursive: yield from self._ls(child_id, recursive=recursive) @@ -61,7 +64,7 @@ def _ls(self, node_id: NodeID, *, recursive: bool) -> Iterable[NodeID]: def iterdir(self, node_id: NodeID, *, recursive: bool = False) -> Iterable[Node]: """Iterate over the children in this node""" for child_id in self._ls(node_id, recursive=recursive): - yield self._nodes[child_id] + yield self.nodes[child_id] def listdir(self, node_id: NodeID) -> list[Node]: """Get a list of children of this node (non recursive)""" @@ -105,16 +108,6 @@ def __getitem__(self, node_id: NodeID) -> Node: """Get the node with this ID""" return self.nodes[node_id] - @property - def nodes(self) -> MappingProxyType[NodeID, Node]: - """A mapping of every node""" - return self._nodes - - @property - def children(self) -> MappingProxyType[NodeID, tuple[NodeID, ...]]: - """A mapping of nodes to their inmediate children""" - return self._children - @property def deleted(self) -> Iterable[Node]: """Files or folders currently on the trash bin (Non recursive)""" @@ -153,8 +146,8 @@ def build(cls, nodes: Iterable[Node]) -> Self: trash_bin=trash_bin, file_count=file_count, folder_count=folder_count, - _nodes=MappingProxyType(nodes_map), - _children=MappingProxyType({node_id: tuple(nodes) for node_id, nodes in children.items()}), + nodes=MappingProxyType(nodes_map), + children=MappingProxyType({node_id: tuple(nodes) for node_id, nodes in children.items()}), ) def dump(self) -> dict[str, Any]: @@ -183,8 +176,12 @@ class FileSystem(SimpleFileSystem): NOTE: Mega's filesystem is **not POSIX-compliant**: multiple nodes may have the same path """ - _paths: MappingProxyType[NodeID, PurePosixPath] = dataclasses.field(repr=False) - _inv_paths: MappingProxyType[PurePosixPath, tuple[NodeID, ...]] = dataclasses.field(repr=False) + paths: MappingProxyType[NodeID, PurePosixPath] = dataclasses.field(repr=False) + """A mapping of every node to its absolute path within the filesystem""" + + inv_paths: MappingProxyType[PurePosixPath, tuple[NodeID, ...]] = dataclasses.field(repr=False) + """A mapping of paths to every node located at that path""" + _deleted: frozenset[NodeID] = dataclasses.field(repr=False) @classmethod @@ -219,26 +216,13 @@ def build(cls, nodes: Iterable[Node]) -> Self: trash_bin=self.trash_bin, file_count=self.file_count, folder_count=self.folder_count, - _nodes=self.nodes, - _children=self.children, - _paths=MappingProxyType(paths), - _inv_paths=MappingProxyType({path: tuple(nodes) for path, nodes in inv_paths.items()}), + nodes=self.nodes, + children=self.children, + paths=MappingProxyType(paths), + inv_paths=MappingProxyType({path: tuple(nodes) for path, nodes in inv_paths.items()}), _deleted=frozenset(deleted_ids), ) - @property - def paths(self) -> MappingProxyType[NodeID, PurePosixPath]: - """A mapping of every node to its absolute path within the filesystem""" - return self._paths - - @property - def inv_paths(self) -> MappingProxyType[PurePosixPath, tuple[NodeID, ...]]: - """A mapping of paths to every node located at that path - - Mega's filesystem is **not POSIX-compliant**: multiple nodes may have the same path - """ - return self._inv_paths - @property def files(self) -> Iterable[Node]: """All files that are NOT deleted (recursive)""" @@ -270,7 +254,7 @@ def relative_path(self, node_id: NodeID) -> PurePosixPath: def absolute_path(self, node_id: NodeID) -> PurePosixPath: """Get the absolute path of this node""" - return self._paths[node_id] + return self.paths[node_id] def search( self, @@ -281,7 +265,7 @@ def search( """Returns nodes that have "query" as a substring on their path""" query = PurePosixPath(query).as_posix() - for node_id, path in self._paths.items(): + for node_id, path in self.paths.items(): if query not in path.as_posix(): continue @@ -302,7 +286,7 @@ def find(self, path: str | PathLike[str]) -> Node: """ path = POSIX_ROOT / PurePosixPath(path) try: - nodes = self._inv_paths[path] + nodes = self.inv_paths[path] except LookupError: msg = f'A node with path "{path!s}" does not exists' raise FileNotFoundError(errno.ENOENT, msg) from None @@ -342,8 +326,8 @@ def dump(self, *, simple: bool = False) -> dict[str, Any]: return dump | dict( # noqa: C408 deleted=sorted(self._deleted), - paths={node_id: str(path) for node_id, path in self._paths.items()}, - inv_paths={str(path): node_id for path, node_id in self._inv_paths.items()}, + paths={node_id: str(path) for node_id, path in self.paths.items()}, + inv_paths={str(path): node_id for path, node_id in self.inv_paths.items()}, children=dict(self.children), ) From 137e10c9740a66474f11448ca2978965376cc2c6 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sat, 2 May 2026 09:53:34 -0500 Subject: [PATCH 3/7] refactor: remove build unsafe --- src/mega/filesystem.py | 68 ------------------------------------------ 1 file changed, 68 deletions(-) diff --git a/src/mega/filesystem.py b/src/mega/filesystem.py index 88549e5..06263cb 100644 --- a/src/mega/filesystem.py +++ b/src/mega/filesystem.py @@ -338,72 +338,6 @@ class UserFileSystem(FileSystem): inbox: Node trash_bin: Node - @classmethod - def build_unsafe(cls, nodes: Iterable[Node]) -> Self: - """Build a filesystem from a pre-ordered node stream without validation - - Nodes MUST be topologically sorted: - - The first three nodes are ROOT, INBOX and TRASH_BIN. - - Every parent appears before its descendants. - """ - # Build fs using only 3 loops: - # - 1. Create a map to all nodes and resolve their paths - # - 2. Freeze children (list -> tuple) - # - 3. Freeze inv paths (list -> tuple) - root = inbox = trash_bin = None - file_count = folder_count = 0 - - paths: dict[NodeID, PurePosixPath] = {} - inv_paths: dict[PurePosixPath, list[NodeID]] = {} - deleted_ids: set[NodeID] = set() - nodes_map: dict[NodeID, Node] = {} - children: dict[NodeID, list[NodeID]] = {} - trash_bin_id = None - - for node in nodes: - nodes_map[node.id] = node - if node.parent_id: - children.setdefault(node.parent_id, []).append(node.id) - - path = None - match node.type: - case NodeType.FILE: - file_count += 1 - case NodeType.FOLDER: - folder_count += 1 - case NodeType.ROOT_FOLDER: - path = POSIX_ROOT - root = node - case NodeType.INBOX: - path = POSIX_ROOT / node.attributes.name - inbox = node - case NodeType.TRASH: - path = POSIX_ROOT / node.attributes.name - trash_bin_id = node.id - trash_bin = node - case _: - raise RuntimeError # pyright: ignore[reportUnreachable] - - path = path or (paths[node.parent_id] / node.attributes.name) - paths[node.id] = path - inv_paths.setdefault(path, []).append(node.id) - if node.parent_id == trash_bin_id or node.parent_id in deleted_ids: - deleted_ids.add(node.id) - - assert root and inbox and trash_bin - return cls( - root=root, - inbox=inbox, - trash_bin=trash_bin, - file_count=file_count, - folder_count=folder_count, - _nodes=MappingProxyType(nodes_map), - _children=MappingProxyType({node_id: tuple(nodes) for node_id, nodes in children.items()}), - _paths=MappingProxyType(paths), - _inv_paths=MappingProxyType({path: tuple(nodes) for path, nodes in inv_paths.items()}), - _deleted=frozenset(deleted_ids), - ) - if __name__ == "__main__": import json @@ -417,7 +351,5 @@ def build_unsafe(cls, nodes: Iterable[Node]) -> Self: print(f"Testing filesystem build of {len(nodes):_} nodes") # noqa: T201 iterations = 1_000 - unsafe = timeit.timeit(lambda: UserFileSystem.build_unsafe(nodes), number=iterations) - print(f"{iterations} [UNSAFE] calls took {unsafe:.4f}s") # noqa: T201 safe = timeit.timeit(lambda: UserFileSystem.build(nodes), number=iterations) print(f"{iterations} [SAFE] calls took {safe:.4f}s") # noqa: T201 From 036397d9c9b73918b6251f3bf7631bf21d1242b9 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sat, 2 May 2026 09:58:00 -0500 Subject: [PATCH 4/7] refactor: add additional check for special nodes --- src/mega/filesystem.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/mega/filesystem.py b/src/mega/filesystem.py index 06263cb..2c4a1c9 100644 --- a/src/mega/filesystem.py +++ b/src/mega/filesystem.py @@ -12,7 +12,7 @@ from typing import TYPE_CHECKING, Any, Final, NamedTuple, Self from mega.data_structures import Node, NodeID, NodeType, _DictDumper -from mega.errors import MultipleNodesFoundError +from mega.errors import MultipleNodesFoundError, ValidationError if TYPE_CHECKING: from collections.abc import Generator, Iterable, Iterator @@ -122,6 +122,11 @@ def build(cls, nodes: Iterable[Node]) -> Self: nodes_map: dict[NodeID, Node] = {} children: dict[NodeID, list[NodeID]] = {} + def sanity_check(current: Node | None, found: Node) -> None: + if root is not None: + ids = root.id, found.id + raise ValidationError(f"Multiple nodes of type {found.type.name} found: {ids}") + for node in nodes: nodes_map[node.id] = node if node.parent_id: @@ -132,10 +137,13 @@ def build(cls, nodes: Iterable[Node]) -> Self: case NodeType.FOLDER: folder_count += 1 case NodeType.ROOT_FOLDER: + sanity_check(root, node) root = node case NodeType.INBOX: + sanity_check(inbox, node) inbox = node case NodeType.TRASH: + sanity_check(trash_bin, node) trash_bin = node case _: raise RuntimeError # pyright: ignore[reportUnreachable] From 49a031f913f66b4cf938aa655909d882b9a07b13 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sat, 2 May 2026 09:59:12 -0500 Subject: [PATCH 5/7] chore: bump version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a717c8a..55d6c62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ license = "Apache-2.0" license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.11" -version = "2.3.1" +version = "2.4.0" [project.optional-dependencies] cli = [ diff --git a/uv.lock b/uv.lock index ecc0afa..6a304d6 100644 --- a/uv.lock +++ b/uv.lock @@ -137,7 +137,7 @@ wheels = [ [[package]] name = "async-mega-py" -version = "2.3.1" +version = "2.4.0" source = { editable = "." } dependencies = [ { name = "aiohttp" }, From a03b0ce9facf61ed5926a91b5affca49ae421857 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sat, 2 May 2026 15:02:53 +0000 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> --- src/mega/filesystem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mega/filesystem.py b/src/mega/filesystem.py index 2c4a1c9..7752e17 100644 --- a/src/mega/filesystem.py +++ b/src/mega/filesystem.py @@ -92,7 +92,7 @@ def fields(): yield "files", self.file_count yield "folders", self.file_count - all_fields = "".join(f"{name}={value!r}" for name, value in fields()) + all_fields = ", ".join(f"{name}={value!r}" for name, value in fields()) return f"<{type(self).__name__}({all_fields})>" def __len__(self) -> int: @@ -123,8 +123,8 @@ def build(cls, nodes: Iterable[Node]) -> Self: children: dict[NodeID, list[NodeID]] = {} def sanity_check(current: Node | None, found: Node) -> None: - if root is not None: - ids = root.id, found.id + if current is not None: + ids = current.id, found.id raise ValidationError(f"Multiple nodes of type {found.type.name} found: {ids}") for node in nodes: From 3e618102cd2c4d4da5ef43dc37b96473cea67c24 Mon Sep 17 00:00:00 2001 From: NTFSvolume <172021377+NTFSvolume@users.noreply.github.com> Date: Sat, 2 May 2026 10:04:51 -0500 Subject: [PATCH 7/7] tests: update tests --- tests/test_filesystem.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/test_filesystem.py b/tests/test_filesystem.py index 16e84de..b4ca7b3 100644 --- a/tests/test_filesystem.py +++ b/tests/test_filesystem.py @@ -119,13 +119,7 @@ def get_path(recursive: bool) -> list[str]: assert get_path(recursive=True) == sorted(recursive_children) -def test_unsafe_filesystem_build() -> None: - dump = json.loads(TEST_FS.read_text()) - nodes = (Node.from_dump(node) for node in dump["nodes"].values()) - UserFileSystem.build_unsafe(nodes) - - -def test_safe_filesystem_build() -> None: +def test_filesystem_build() -> None: dump = json.loads(TEST_FS.read_text()) nodes = (Node.from_dump(node) for node in dump["nodes"].values()) fs = UserFileSystem.build(nodes)