Skip to content

Commit a7d308e

Browse files
committed
add top level contents and example qt app
1 parent ea90b91 commit a7d308e

File tree

5 files changed

+190
-3
lines changed

5 files changed

+190
-3
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ dependencies = [
2626
"fsspec >=2025.5.1",
2727
"pyyaml",
2828
"toml",
29-
"click"
29+
"click",
30+
"jinja2"
3031
]
3132

3233
[project.optional-dependencies]

src/projspec/proj/base.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ def all_artifacts(self, names=None) -> list:
216216
arts.update(flatten(spec.artifacts))
217217
for child in self.children.values():
218218
arts.update(child.artifacts)
219+
arts.update(self.artifacts.values())
219220
if names:
220221
if isinstance(names, str):
221222
names = {names}
@@ -236,6 +237,34 @@ def has_artifact_type(self, types: Iterable[type]) -> bool:
236237
types = tuple(types)
237238
return any(isinstance(_, types) for _ in self.all_artifacts())
238239

240+
def all_contents(self, names=None) -> list:
241+
"""A flat list of all the content objects nested in this project."""
242+
cont = set(self.contents.values())
243+
for spec in self.specs.values():
244+
cont.update(flatten(spec.contents))
245+
for child in self.children.values():
246+
cont.update(child.contents)
247+
cont.update(self.contents.values())
248+
if names:
249+
if isinstance(names, str):
250+
names = {names}
251+
cont = [
252+
a
253+
for a in cont
254+
if any(a.snake_name() == camel_to_snake(n) for n in names)
255+
]
256+
else:
257+
cont = list(cont)
258+
return cont
259+
260+
def has_content_type(self, types: Iterable[type]) -> bool:
261+
"""Answers 'does this project support outputting the given content type'
262+
263+
This is an experimental example of filtering through projects
264+
"""
265+
types = tuple(types)
266+
return any(isinstance(_, types) for _ in self.all_contents())
267+
239268
def __contains__(self, item) -> bool:
240269
"""Is the given project type supported ANYWHERE in this directory?"""
241270
return item in self.specs or any(item in _ for _ in self.children.values())

src/projspec/proj/node.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,11 @@ def parse(self):
142142

143143
super().parse0()
144144

145-
with self.proj.fs.open(f"{self.proj.url}/yarn.lock", "rt") as f:
146-
txt = f.read()
145+
try:
146+
with self.proj.fs.open(f"{self.proj.url}/yarn.lock", "rt") as f:
147+
txt = f.read()
148+
except FileNotFoundError:
149+
raise ParseFailed
147150
hits = re.findall(r'resolution: "(.*?)"', txt, flags=re.MULTILINE)
148151

149152
self.artifacts["lock_file"] = LockFile(

src/projspec/qtapp/__init__.py

Whitespace-only changes.

src/projspec/qtapp/main.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import sys
2+
from pathlib import Path
3+
from PyQt5.QtWidgets import (
4+
QApplication,
5+
QMainWindow,
6+
QTreeWidget,
7+
QTreeWidgetItem,
8+
QVBoxLayout,
9+
QWidget,
10+
QLabel,
11+
QHBoxLayout,
12+
QDialog,
13+
)
14+
from PyQt5.QtWebEngineWidgets import QWebEngineView
15+
from PyQt5.QtCore import Qt, QUuid, QUrl
16+
from PyQt5.QtGui import QIcon
17+
18+
19+
class FileBrowserWindow(QMainWindow):
20+
def __init__(self, parent=None):
21+
super().__init__(parent)
22+
self.setWindowTitle("Projspec Browser")
23+
self.setGeometry(100, 100, 950, 600)
24+
25+
# Create tree widget
26+
self.tree = QTreeWidget(self)
27+
self.tree.setHeaderLabels(["Name", "Type", "Size"])
28+
self.tree.setColumnWidth(0, 400)
29+
self.tree.setColumnWidth(1, 50)
30+
31+
# Connect signals
32+
self.tree.itemExpanded.connect(self.on_item_expanded)
33+
self.tree.currentItemChanged.connect(self.on_item_changed)
34+
35+
self.detail = QWebEngineView(self)
36+
# self.detail.load(QUrl("https://qt-project.org/"))
37+
self.detail.setFixedWidth(600)
38+
39+
# Create central widget and layout
40+
central_widget = QWidget(self)
41+
self.setCentralWidget(central_widget)
42+
43+
layout = QHBoxLayout(central_widget)
44+
layout.addWidget(self.tree)
45+
layout.addWidget(self.detail)
46+
central_widget.setLayout(layout)
47+
48+
# Status bar
49+
self.statusBar().showMessage("Ready")
50+
51+
# Populate with home directory
52+
self.populate_tree()
53+
54+
def populate_tree(self):
55+
"""Populate the tree with the user's home directory"""
56+
home_path = Path.home()
57+
root_item = QTreeWidgetItem(self.tree)
58+
root_item.setText(0, home_path.name or str(home_path))
59+
root_item.setText(1, "Folder")
60+
root_item.setData(0, Qt.ItemDataRole.UserRole, str(home_path))
61+
62+
# Add a dummy child to make it expandable
63+
self.add_children(root_item, home_path)
64+
65+
# Expand the root
66+
root_item.setExpanded(True)
67+
68+
def add_children(self, parent_item, path):
69+
"""Add child items for a directory"""
70+
try:
71+
path_obj = Path(path)
72+
73+
# Get all items in directory
74+
items = sorted(
75+
path_obj.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())
76+
)
77+
78+
for item in items:
79+
# Skip hidden files (optional)
80+
if item.name.startswith("."):
81+
continue
82+
83+
child_item = QTreeWidgetItem(parent_item)
84+
child_item.setText(0, item.name)
85+
child_item.setData(0, Qt.ItemDataRole.UserRole, str(item))
86+
87+
if item.is_dir():
88+
child_item.setText(1, "Folder")
89+
child_item.setText(2, "")
90+
# Add dummy child to make it expandable
91+
dummy = QTreeWidgetItem(child_item)
92+
dummy.setText(0, "Loading...")
93+
else:
94+
child_item.setText(1, "File")
95+
try:
96+
size = item.stat().st_size
97+
child_item.setText(2, self.format_size(size))
98+
except:
99+
child_item.setText(2, "")
100+
101+
except PermissionError:
102+
error_item = QTreeWidgetItem(parent_item)
103+
error_item.setText(0, "Permission Denied")
104+
error_item.setForeground(0, Qt.GlobalColor.red)
105+
except Exception as e:
106+
error_item = QTreeWidgetItem(parent_item)
107+
error_item.setText(0, f"Error: {str(e)}")
108+
error_item.setForeground(0, Qt.GlobalColor.red)
109+
110+
def on_item_expanded(self, item):
111+
"""Handle item expansion - load children if not already loaded"""
112+
# Check if we need to load children (has dummy child)
113+
if item.childCount() == 1 and item.child(0).text(0) == "Loading...":
114+
# Remove dummy child
115+
item.removeChild(item.child(0))
116+
117+
# Get path from item data
118+
path = item.data(0, Qt.ItemDataRole.UserRole)
119+
120+
# Add real children
121+
if path:
122+
self.add_children(item, path)
123+
self.statusBar().showMessage(f"Loaded: {path}")
124+
125+
def on_item_changed(self, item):
126+
import projspec
127+
128+
if item.text(1) == "Folder":
129+
proj = projspec.Project(item.data(0, Qt.ItemDataRole.UserRole), walk=False)
130+
if proj.specs:
131+
print(proj.text_summary())
132+
html = f"<!DOCTYPE html><html><body>{proj._repr_html_()}</body></html>"
133+
self.detail.setHtml(html)
134+
else:
135+
self.detail.setHtml("<!DOCTYPE html><html><body></body></html>")
136+
137+
def format_size(self, size):
138+
"""Format file size in human-readable format"""
139+
for unit in ["B", "KB", "MB", "GB", "TB"]:
140+
if size < 1024.0:
141+
return f"{size:.1f} {unit}"
142+
size /= 1024.0
143+
return f"{size:.1f} PB"
144+
145+
146+
def main():
147+
app = QApplication(sys.argv)
148+
window = FileBrowserWindow()
149+
window.show()
150+
sys.exit(app.exec())
151+
152+
153+
if __name__ == "__main__":
154+
main()

0 commit comments

Comments
 (0)