debian-policy-checker 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2026 Daniel Nylander <daniel@danielnylander.se>
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: debian-policy-checker
3
+ Version: 0.1.3
4
+ Summary: GTK4 Debian Policy Manual validator with fix suggestions
5
+ Author-email: Daniel Nylander <daniel@danielnylander.se>
6
+ License-Expression: GPL-3.0-or-later
7
+ Requires-Python: >=3.10
8
+ License-File: LICENSE
9
+ Requires-Dist: PyGObject>=3.42
10
+ Dynamic: license-file
@@ -0,0 +1,20 @@
1
+ # Debian Policy Checker
2
+
3
+ Validate packages against the Debian Policy Manual with fix suggestions.
4
+
5
+ Built with GTK4/Adwaita. Part of the [Danne L10n Suite](https://github.com/yeager/debian-repo).
6
+
7
+ ## Installation
8
+
9
+ ### Debian/Ubuntu
10
+ ```bash
11
+ sudo apt install debian-policy-checker
12
+ ```
13
+
14
+ ## License
15
+
16
+ GPL-3.0
17
+
18
+ ## Author
19
+
20
+ Daniel Nylander — [danielnylander.se](https://danielnylander.se)
@@ -0,0 +1,18 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "debian-policy-checker"
7
+ version = "0.1.3"
8
+ description = "GTK4 Debian Policy Manual validator with fix suggestions"
9
+ requires-python = ">=3.10"
10
+ license = "GPL-3.0-or-later"
11
+ authors = [{name = "Daniel Nylander", email = "daniel@danielnylander.se"}]
12
+ dependencies = ["PyGObject>=3.42"]
13
+
14
+ [project.scripts]
15
+ debian-policy-checker = "debian_policy_checker.main:main"
16
+
17
+ [tool.setuptools.packages.find]
18
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1"
@@ -0,0 +1 @@
1
+ from .main import main
@@ -0,0 +1,66 @@
1
+ """Accessibility features: zoom, high contrast, ATK."""
2
+ import gi
3
+ gi.require_version('Gtk', '4.0')
4
+ from gi.repository import Gtk, Gdk, Gio
5
+
6
+
7
+ class AccessibilityManager:
8
+ """Manages accessibility features for a GTK4 window."""
9
+
10
+ def __init__(self, window, app=None):
11
+ self._window = window
12
+ self._app = app or window.get_application()
13
+ self._font_scale = 1.0
14
+ self._high_contrast = False
15
+ self._css = Gtk.CssProvider()
16
+ Gtk.StyleContext.add_provider_for_display(
17
+ Gdk.Display.get_default(), self._css,
18
+ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 1
19
+ )
20
+ self._setup_actions()
21
+
22
+ def _setup_actions(self):
23
+ if self._app is None:
24
+ return
25
+ actions = [
26
+ ('zoom-in', self._zoom_in, ['<Control>plus', '<Control>equal']),
27
+ ('zoom-out', self._zoom_out, ['<Control>minus']),
28
+ ('zoom-reset', self._zoom_reset, ['<Control>0']),
29
+ ('toggle-high-contrast', self._toggle_hc, ['<Control><Shift>h']),
30
+ ]
31
+ for name, cb, accels in actions:
32
+ if not self._app.lookup_action(name):
33
+ action = Gio.SimpleAction.new(name, None)
34
+ action.connect('activate', lambda a, p, c=cb: c())
35
+ self._app.add_action(action)
36
+ self._app.set_accels_for_action(f'app.{name}', accels)
37
+
38
+ def _apply_css(self):
39
+ css = f'window {{ font-size: {self._font_scale}em; }}'
40
+ if self._high_contrast:
41
+ css += """
42
+ window.high-contrast {
43
+ border: 2px solid @accent_color;
44
+ font-weight: bold;
45
+ }"""
46
+ self._css.load_from_string(css.encode())
47
+
48
+ def _zoom_in(self):
49
+ self._font_scale = min(self._font_scale + 0.1, 3.0)
50
+ self._apply_css()
51
+
52
+ def _zoom_out(self):
53
+ self._font_scale = max(self._font_scale - 0.1, 0.5)
54
+ self._apply_css()
55
+
56
+ def _zoom_reset(self):
57
+ self._font_scale = 1.0
58
+ self._apply_css()
59
+
60
+ def _toggle_hc(self):
61
+ self._high_contrast = not self._high_contrast
62
+ if self._high_contrast:
63
+ self._window.add_css_class('high-contrast')
64
+ else:
65
+ self._window.remove_css_class('high-contrast')
66
+ self._apply_css()
@@ -0,0 +1,385 @@
1
+ """Debian Policy Checker — Validate packages against Debian Policy Manual."""
2
+ import gi
3
+ gi.require_version("Gtk", "4.0")
4
+ gi.require_version("Adw", "1")
5
+ from gi.repository import Gtk, Adw, Gdk, Gio, GLib, Pango
6
+
7
+ import gettext
8
+ import locale
9
+ import os
10
+ import sys
11
+ import json
12
+ import datetime
13
+ import threading
14
+ import subprocess
15
+ import re
16
+ from debian_policy_checker.accessibility import AccessibilityManager
17
+
18
+ LOCALE_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "po")
19
+ if not os.path.isdir(LOCALE_DIR):
20
+ LOCALE_DIR = "/usr/share/locale"
21
+ locale.bindtextdomain("debian-policy-checker", LOCALE_DIR)
22
+ gettext.bindtextdomain("debian-policy-checker", LOCALE_DIR)
23
+ gettext.textdomain("debian-policy-checker")
24
+ _ = gettext.gettext
25
+
26
+ APP_ID = "se.danielnylander.debian.policy.checker"
27
+ SETTINGS_DIR = os.path.join(
28
+ os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
29
+ "debian-policy-checker"
30
+ )
31
+ SETTINGS_FILE = os.path.join(SETTINGS_DIR, "settings.json")
32
+
33
+
34
+ def _load_settings():
35
+ if os.path.exists(SETTINGS_FILE):
36
+ with open(SETTINGS_FILE) as f:
37
+ return json.load(f)
38
+ return {"welcome_shown": False}
39
+
40
+
41
+ def _save_settings(s):
42
+ os.makedirs(SETTINGS_DIR, exist_ok=True)
43
+ with open(SETTINGS_FILE, "w") as f:
44
+ json.dump(s, f, indent=2)
45
+
46
+
47
+
48
+ def _run_lintian(path):
49
+ """Run lintian on a .deb or source directory."""
50
+ results = []
51
+ try:
52
+ r = subprocess.run(["lintian", "--display-info", "--display-experimental",
53
+ "--pedantic", path],
54
+ capture_output=True, text=True, timeout=120)
55
+ for line in r.stdout.splitlines():
56
+ m = re.match(r'(\w):\s+(\S+):\s+(\S+)\s+(.*)', line)
57
+ if m:
58
+ results.append({
59
+ "severity": m.group(1),
60
+ "package": m.group(2),
61
+ "tag": m.group(3),
62
+ "detail": m.group(4),
63
+ })
64
+ else:
65
+ results.append({"severity": "I", "package": "", "tag": line, "detail": ""})
66
+ except FileNotFoundError:
67
+ results.append({"severity": "E", "tag": "lintian-not-installed",
68
+ "detail": _("Install with: sudo apt install lintian"), "package": ""})
69
+ except Exception as e:
70
+ results.append({"severity": "E", "tag": "error", "detail": str(e), "package": ""})
71
+ return results
72
+
73
+
74
+
75
+ class DebianPolicyCheckerWindow(Adw.ApplicationWindow):
76
+ def __init__(self, app):
77
+ super().__init__(application=app, title=_("Debian Policy Checker"), default_width=1000, default_height=700)
78
+ self.settings = _load_settings()
79
+ self._results = []
80
+
81
+ main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
82
+
83
+ # Header
84
+ headerbar = Adw.HeaderBar()
85
+ title_widget = Adw.WindowTitle(title=_("Debian Policy Checker"), subtitle="")
86
+ headerbar.set_title_widget(title_widget)
87
+ self._title_widget = title_widget
88
+
89
+
90
+ open_btn = Gtk.Button(icon_name="document-open-symbolic", tooltip_text=_("Open .deb or package directory"))
91
+ open_btn.connect("clicked", self._on_open)
92
+ headerbar.pack_start(open_btn)
93
+
94
+ # Menu
95
+ menu = Gio.Menu()
96
+ menu.append(_("Settings"), "app.settings")
97
+ menu.append(_("Copy Debug Info"), "app.copy-debug")
98
+ menu.append(_("Keyboard Shortcuts"), "app.shortcuts")
99
+ menu.append(_("About Debian Policy Checker"), "app.about")
100
+ menu_btn = Gtk.MenuButton(icon_name="open-menu-symbolic", menu_model=menu)
101
+ headerbar.pack_end(menu_btn)
102
+
103
+ main_box.append(headerbar)
104
+
105
+
106
+ scroll = Gtk.ScrolledWindow(vexpand=True)
107
+ self._list = Gtk.ListBox()
108
+ self._list.set_selection_mode(Gtk.SelectionMode.NONE)
109
+ self._list.add_css_class("boxed-list")
110
+ self._list.set_margin_start(12)
111
+ self._list.set_margin_end(12)
112
+ self._list.set_margin_top(8)
113
+ self._list.set_margin_bottom(8)
114
+ scroll.set_child(self._list)
115
+
116
+ self._empty = Adw.StatusPage()
117
+ self._empty.set_icon_name("dialog-information-symbolic")
118
+ self._empty.set_title(_("No package checked"))
119
+ self._empty.set_description(_("Open a .deb file or package directory to check policy compliance."))
120
+ self._empty.set_vexpand(True)
121
+
122
+ self._stack = Gtk.Stack()
123
+ self._stack.add_named(self._empty, "empty")
124
+ self._stack.add_named(scroll, "list")
125
+ self._stack.set_vexpand(True)
126
+ main_box.append(self._stack)
127
+
128
+ # Status bar
129
+ self._status = Gtk.Label(label=_("Ready"), xalign=0)
130
+ self._status.set_margin_start(12)
131
+ self._status.set_margin_end(12)
132
+ self._status.set_margin_top(4)
133
+ self._status.set_margin_bottom(4)
134
+ self._status.add_css_class("dim-label")
135
+ main_box.append(self._status)
136
+
137
+ self.set_content(main_box)
138
+
139
+ if not self.settings.get("welcome_shown"):
140
+ GLib.idle_add(self._show_welcome)
141
+
142
+ def _show_welcome(self):
143
+ dialog = Adw.Dialog()
144
+ dialog.set_title(_("Welcome"))
145
+ dialog.set_content_width(420)
146
+ dialog.set_content_height(480)
147
+
148
+ page = Adw.StatusPage()
149
+ page.set_icon_name("dialog-information-symbolic")
150
+ page.set_title(_("Welcome to Debian Policy Checker"))
151
+ page.set_description(_("Check Debian Policy compliance.\n\n"
152
+ "✓ Validate packages against Debian Policy\n"
153
+ "✓ Clear error descriptions with fix suggestions\n"
154
+ "✓ Integrates with lintian output\n"
155
+ "✓ Policy section references\n"
156
+ "✓ Export validation reports"))
157
+
158
+ btn = Gtk.Button(label=_("Get Started"))
159
+ btn.add_css_class("suggested-action")
160
+ btn.add_css_class("pill")
161
+ btn.set_halign(Gtk.Align.CENTER)
162
+ btn.set_margin_top(12)
163
+ btn.connect("clicked", self._on_welcome_close, dialog)
164
+ page.set_child(btn)
165
+
166
+ box = Adw.ToolbarView()
167
+ hb = Adw.HeaderBar()
168
+ hb.set_show_title(False)
169
+ box.add_top_bar(hb)
170
+ box.set_content(page)
171
+ dialog.set_child(box)
172
+ dialog.present(self)
173
+
174
+ def _on_welcome_close(self, btn, dialog):
175
+ self.settings["welcome_shown"] = True
176
+ _save_settings(self.settings)
177
+ dialog.close()
178
+
179
+
180
+ def _on_open(self, btn):
181
+ dialog = Gtk.FileDialog()
182
+ dialog.set_title(_("Open package"))
183
+ dialog.open(self, None, self._on_file_opened)
184
+
185
+ def _on_file_opened(self, dialog, result):
186
+ try:
187
+ f = dialog.open_finish(result)
188
+ path = f.get_path()
189
+ self._status.set_text(_("Checking %s...") % path)
190
+ self._title_widget.set_subtitle(os.path.basename(path))
191
+ threading.Thread(target=self._do_check, args=(path,), daemon=True).start()
192
+ except:
193
+ pass
194
+
195
+ def _do_check(self, path):
196
+ results = _run_lintian(path)
197
+ GLib.idle_add(self._show_results, results)
198
+
199
+ def _show_results(self, results):
200
+ self._results = results
201
+ while True:
202
+ row = self._list.get_row_at_index(0)
203
+ if row is None:
204
+ break
205
+ self._list.remove(row)
206
+
207
+ severity_icons = {"E": "❌", "W": "⚠️", "I": "ℹ️", "N": "📝", "P": "🔍"}
208
+ severity_names = {"E": _("Error"), "W": _("Warning"), "I": _("Info"), "N": _("Note"), "P": _("Pedantic")}
209
+
210
+ errors = sum(1 for r in results if r["severity"] == "E")
211
+ warnings = sum(1 for r in results if r["severity"] == "W")
212
+
213
+ for r in results:
214
+ icon = severity_icons.get(r["severity"], "❓")
215
+ sev = severity_names.get(r["severity"], r["severity"])
216
+ row = Adw.ActionRow()
217
+ row.set_title(f"{icon} {r['tag']}")
218
+ row.set_subtitle(r.get("detail", ""))
219
+ badge = Gtk.Label(label=sev)
220
+ badge.add_css_class("caption")
221
+ if r["severity"] == "E":
222
+ badge.add_css_class("error")
223
+ row.add_suffix(badge)
224
+ self._list.append(row)
225
+
226
+ self._stack.set_visible_child_name("list")
227
+ self._status.set_text(_("%(total)d issues: %(errors)d errors, %(warnings)d warnings") %
228
+ {"total": len(results), "errors": errors, "warnings": warnings})
229
+
230
+
231
+ class DebianPolicyCheckerApp(Adw.Application):
232
+ def __init__(self):
233
+ super().__init__(application_id=APP_ID, flags=Gio.ApplicationFlags.FLAGS_NONE)
234
+ self.window = None
235
+
236
+ for name, callback in [
237
+ ("settings", self._on_settings),
238
+ ("copy-debug", self._on_copy_debug),
239
+ ("shortcuts", self._on_shortcuts),
240
+ ("about", self._on_about),
241
+ ("quit", self._on_quit),
242
+ ]:
243
+ action = Gio.SimpleAction.new(name, None)
244
+ action.connect("activate", callback)
245
+ self.add_action(action)
246
+
247
+ self.set_accels_for_action("app.quit", ["<Ctrl>q"])
248
+ self.set_accels_for_action("app.shortcuts", ["<Ctrl>slash"])
249
+
250
+ def do_activate(self):
251
+ if not self.window:
252
+ self.window = DebianPolicyCheckerWindow(self)
253
+ self.window.present()
254
+
255
+ def _on_settings(self, *_args):
256
+ if not self.window:
257
+ return
258
+ dialog = Adw.PreferencesDialog()
259
+ dialog.set_title(_("Settings"))
260
+ page = Adw.PreferencesPage()
261
+
262
+ group = Adw.PreferencesGroup(title=_("Lintian"))
263
+ row = Adw.SwitchRow(title=_("Show pedantic warnings"))
264
+ row.set_active(True)
265
+ group.add(row)
266
+ row2 = Adw.SwitchRow(title=_("Show experimental tags"))
267
+ row2.set_active(True)
268
+ group.add(row2)
269
+ page.add(group)
270
+ dialog.add(page)
271
+ dialog.present(self.window)
272
+
273
+ def _on_copy_debug(self, *_args):
274
+ if not self.window:
275
+ return
276
+ from . import __version__
277
+ info = (
278
+ f"Debian Policy Checker {__version__}\n"
279
+ f"Python {sys.version}\n"
280
+ f"GTK {Gtk.MAJOR_VERSION}.{Gtk.MINOR_VERSION}\n"
281
+ f"Adw {Adw.MAJOR_VERSION}.{Adw.MINOR_VERSION}\n"
282
+ f"OS: {os.uname().sysname} {os.uname().release}\n"
283
+ )
284
+ clipboard = Gdk.Display.get_default().get_clipboard()
285
+ clipboard.set(info)
286
+ self.window._status.set_text(_("Debug info copied"))
287
+
288
+ def _on_shortcuts(self, *_args):
289
+ if self.window:
290
+ dialog = Gtk.ShortcutsWindow(transient_for=self.window)
291
+ section = Gtk.ShortcutsSection(visible=True)
292
+ group = Gtk.ShortcutsGroup(title=_("General"), visible=True)
293
+ for accel, title in [
294
+ ("<Ctrl>q", _("Quit")),
295
+ ("<Ctrl>slash", _("Keyboard shortcuts")),
296
+ ]:
297
+ group.append(Gtk.ShortcutsShortcut(accelerator=accel, title=title, visible=True))
298
+ section.append(group)
299
+ dialog.append(section)
300
+ dialog.present()
301
+
302
+ def _on_about(self, *_args):
303
+ from . import __version__
304
+ dialog = Adw.AboutDialog(
305
+ application_name=_("Debian Policy Checker"),
306
+ application_icon="dialog-information-symbolic",
307
+ version=__version__,
308
+ developer_name="Daniel Nylander",
309
+ website="https://github.com/yeager/debian-policy-checker",
310
+ license_type=Gtk.License.GPL_3_0,
311
+ issue_url="https://github.com/yeager/debian-policy-checker/issues",
312
+ comments=_("Validate packages against the Debian Policy Manual with clear error reports and fix suggestions."),
313
+ )
314
+ dialog.present(self.window)
315
+
316
+ def _on_quit(self, *_args):
317
+ self.quit()
318
+
319
+
320
+ def main():
321
+ app = DebianPolicyCheckerApp()
322
+ app.run(sys.argv)
323
+
324
+
325
+ # --- Session restore ---
326
+ import json as _json
327
+ import os as _os
328
+
329
+ def _save_session(window, app_name):
330
+ config_dir = _os.path.join(_os.path.expanduser('~'), '.config', app_name)
331
+ _os.makedirs(config_dir, exist_ok=True)
332
+ state = {'width': window.get_width(), 'height': window.get_height(),
333
+ 'maximized': window.is_maximized()}
334
+ try:
335
+ with open(_os.path.join(config_dir, 'session.json'), 'w') as f:
336
+ _json.dump(state, f)
337
+ except OSError:
338
+ pass
339
+
340
+ def _restore_session(window, app_name):
341
+ path = _os.path.join(_os.path.expanduser('~'), '.config', app_name, 'session.json')
342
+ try:
343
+ with open(path) as f:
344
+ state = _json.load(f)
345
+ window.set_default_size(state.get('width', 800), state.get('height', 600))
346
+ if state.get('maximized'):
347
+ window.maximize()
348
+ except (FileNotFoundError, _json.JSONDecodeError, OSError):
349
+ pass
350
+
351
+
352
+ # --- Fullscreen toggle (F11) ---
353
+ def _setup_fullscreen(window, app):
354
+ """Add F11 fullscreen toggle."""
355
+ from gi.repository import Gio
356
+ if not app.lookup_action('toggle-fullscreen'):
357
+ action = Gio.SimpleAction.new('toggle-fullscreen', None)
358
+ action.connect('activate', lambda a, p: (
359
+ window.unfullscreen() if window.is_fullscreen() else window.fullscreen()
360
+ ))
361
+ app.add_action(action)
362
+ app.set_accels_for_action('app.toggle-fullscreen', ['F11'])
363
+
364
+
365
+ # --- Plugin system ---
366
+ import importlib.util
367
+ import os as _pos
368
+
369
+ def _load_plugins(app_name):
370
+ """Load plugins from ~/.config/<app>/plugins/."""
371
+ plugin_dir = _pos.path.join(_pos.path.expanduser('~'), '.config', app_name, 'plugins')
372
+ plugins = []
373
+ if not _pos.path.isdir(plugin_dir):
374
+ return plugins
375
+ for fname in sorted(_pos.listdir(plugin_dir)):
376
+ if fname.endswith('.py') and not fname.startswith('_'):
377
+ path = _pos.path.join(plugin_dir, fname)
378
+ try:
379
+ spec = importlib.util.spec_from_file_location(fname[:-3], path)
380
+ mod = importlib.util.module_from_spec(spec)
381
+ spec.loader.exec_module(mod)
382
+ plugins.append(mod)
383
+ except Exception as e:
384
+ print(f"Plugin {fname}: {e}")
385
+ return plugins
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: debian-policy-checker
3
+ Version: 0.1.3
4
+ Summary: GTK4 Debian Policy Manual validator with fix suggestions
5
+ Author-email: Daniel Nylander <daniel@danielnylander.se>
6
+ License-Expression: GPL-3.0-or-later
7
+ Requires-Python: >=3.10
8
+ License-File: LICENSE
9
+ Requires-Dist: PyGObject>=3.42
10
+ Dynamic: license-file
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/debian_policy_checker/__init__.py
5
+ src/debian_policy_checker/__main__.py
6
+ src/debian_policy_checker/accessibility.py
7
+ src/debian_policy_checker/main.py
8
+ src/debian_policy_checker.egg-info/PKG-INFO
9
+ src/debian_policy_checker.egg-info/SOURCES.txt
10
+ src/debian_policy_checker.egg-info/dependency_links.txt
11
+ src/debian_policy_checker.egg-info/entry_points.txt
12
+ src/debian_policy_checker.egg-info/requires.txt
13
+ src/debian_policy_checker.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ debian-policy-checker = debian_policy_checker.main:main