Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/xdg/DesktopEntry.py: 23%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

299 statements  

1""" 

2Complete implementation of the XDG Desktop Entry Specification 

3http://standards.freedesktop.org/desktop-entry-spec/ 

4 

5Not supported: 

6- Encoding: Legacy Mixed 

7- Does not check exec parameters 

8- Does not check URL's 

9- Does not completly validate deprecated/kde items 

10- Does not completly check categories 

11""" 

12 

13from xdg.IniFile import IniFile, is_ascii 

14import xdg.Locale 

15from xdg.Exceptions import ParsingError 

16from xdg.util import which 

17import os.path 

18import re 

19import warnings 

20 

21class DesktopEntry(IniFile): 

22 "Class to parse and validate Desktop Entries" 

23 

24 defaultGroup = 'Desktop Entry' 

25 

26 def __init__(self, filename=None): 

27 """Create a new DesktopEntry. 

28  

29 If filename exists, it will be parsed as a desktop entry file. If not, 

30 or if filename is None, a blank DesktopEntry is created. 

31 """ 

32 self.content = dict() 

33 if filename and os.path.exists(filename): 

34 self.parse(filename) 

35 elif filename: 

36 self.new(filename) 

37 

38 def __str__(self): 

39 return self.getName() 

40 

41 def parse(self, file): 

42 """Parse a desktop entry file. 

43  

44 This can raise :class:`~xdg.Exceptions.ParsingError`, 

45 :class:`~xdg.Exceptions.DuplicateGroupError` or 

46 :class:`~xdg.Exceptions.DuplicateKeyError`. 

47 """ 

48 IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"]) 

49 

50 def findTryExec(self): 

51 """Looks in the PATH for the executable given in the TryExec field. 

52  

53 Returns the full path to the executable if it is found, None if not. 

54 Raises :class:`~xdg.Exceptions.NoKeyError` if TryExec is not present. 

55 """ 

56 tryexec = self.get('TryExec', strict=True) 

57 return which(tryexec) 

58 

59 # start standard keys 

60 def getType(self): 

61 return self.get('Type') 

62 def getVersion(self): 

63 """deprecated, use getVersionString instead """ 

64 return self.get('Version', type="numeric") 

65 def getVersionString(self): 

66 return self.get('Version') 

67 def getName(self): 

68 return self.get('Name', locale=True) 

69 def getGenericName(self): 

70 return self.get('GenericName', locale=True) 

71 def getNoDisplay(self): 

72 return self.get('NoDisplay', type="boolean") 

73 def getComment(self): 

74 return self.get('Comment', locale=True) 

75 def getIcon(self): 

76 return self.get('Icon', locale=True) 

77 def getHidden(self): 

78 return self.get('Hidden', type="boolean") 

79 def getOnlyShowIn(self): 

80 return self.get('OnlyShowIn', list=True) 

81 def getNotShowIn(self): 

82 return self.get('NotShowIn', list=True) 

83 def getTryExec(self): 

84 return self.get('TryExec') 

85 def getExec(self): 

86 return self.get('Exec') 

87 def getPath(self): 

88 return self.get('Path') 

89 def getTerminal(self): 

90 return self.get('Terminal', type="boolean") 

91 def getMimeType(self): 

92 """deprecated, use getMimeTypes instead """ 

93 return self.get('MimeType', list=True, type="regex") 

94 def getMimeTypes(self): 

95 return self.get('MimeType', list=True) 

96 def getCategories(self): 

97 return self.get('Categories', list=True) 

98 def getStartupNotify(self): 

99 return self.get('StartupNotify', type="boolean") 

100 def getStartupWMClass(self): 

101 return self.get('StartupWMClass') 

102 def getURL(self): 

103 return self.get('URL') 

104 # end standard keys 

105 

106 # start kde keys 

107 def getServiceTypes(self): 

108 return self.get('ServiceTypes', list=True) 

109 def getDocPath(self): 

110 return self.get('DocPath') 

111 def getKeywords(self): 

112 return self.get('Keywords', list=True, locale=True) 

113 def getInitialPreference(self): 

114 return self.get('InitialPreference') 

115 def getDev(self): 

116 return self.get('Dev') 

117 def getFSType(self): 

118 return self.get('FSType') 

119 def getMountPoint(self): 

120 return self.get('MountPoint') 

121 def getReadonly(self): 

122 return self.get('ReadOnly', type="boolean") 

123 def getUnmountIcon(self): 

124 return self.get('UnmountIcon', locale=True) 

125 # end kde keys 

126 

127 # start deprecated keys 

128 def getMiniIcon(self): 

129 return self.get('MiniIcon', locale=True) 

130 def getTerminalOptions(self): 

131 return self.get('TerminalOptions') 

132 def getDefaultApp(self): 

133 return self.get('DefaultApp') 

134 def getProtocols(self): 

135 return self.get('Protocols', list=True) 

136 def getExtensions(self): 

137 return self.get('Extensions', list=True) 

138 def getBinaryPattern(self): 

139 return self.get('BinaryPattern') 

140 def getMapNotify(self): 

141 return self.get('MapNotify') 

142 def getEncoding(self): 

143 return self.get('Encoding') 

144 def getSwallowTitle(self): 

145 return self.get('SwallowTitle', locale=True) 

146 def getSwallowExec(self): 

147 return self.get('SwallowExec') 

148 def getSortOrder(self): 

149 return self.get('SortOrder', list=True) 

150 def getFilePattern(self): 

151 return self.get('FilePattern', type="regex") 

152 def getActions(self): 

153 return self.get('Actions', list=True) 

154 # end deprecated keys 

155 

156 # desktop entry edit stuff 

157 def new(self, filename): 

158 """Make this instance into a new, blank desktop entry. 

159  

160 If filename has a .desktop extension, Type is set to Application. If it 

161 has a .directory extension, Type is Directory. Other extensions will 

162 cause :class:`~xdg.Exceptions.ParsingError` to be raised. 

163 """ 

164 if os.path.splitext(filename)[1] == ".desktop": 

165 type = "Application" 

166 elif os.path.splitext(filename)[1] == ".directory": 

167 type = "Directory" 

168 else: 

169 raise ParsingError("Unknown extension", filename) 

170 

171 self.content = dict() 

172 self.addGroup(self.defaultGroup) 

173 self.set("Type", type) 

174 self.filename = filename 

175 # end desktop entry edit stuff 

176 

177 # validation stuff 

178 def checkExtras(self): 

179 # header 

180 if self.defaultGroup == "KDE Desktop Entry": 

181 self.warnings.append('[KDE Desktop Entry]-Header is deprecated') 

182 

183 # file extension 

184 if self.fileExtension == ".kdelnk": 

185 self.warnings.append("File extension .kdelnk is deprecated") 

186 elif self.fileExtension != ".desktop" and self.fileExtension != ".directory": 

187 self.warnings.append('Unknown File extension') 

188 

189 # Type 

190 try: 

191 self.type = self.content[self.defaultGroup]["Type"] 

192 except KeyError: 

193 self.errors.append("Key 'Type' is missing") 

194 

195 # Name 

196 try: 

197 self.name = self.content[self.defaultGroup]["Name"] 

198 except KeyError: 

199 self.errors.append("Key 'Name' is missing") 

200 

201 def checkGroup(self, group): 

202 # check if group header is valid 

203 if not (group == self.defaultGroup \ 

204 or re.match("^Desktop Action [a-zA-Z0-9-]+$", group) \ 

205 or (re.match("^X-", group) and is_ascii(group))): 

206 self.errors.append("Invalid Group name: %s" % group) 

207 else: 

208 #OnlyShowIn and NotShowIn 

209 if ("OnlyShowIn" in self.content[group]) and ("NotShowIn" in self.content[group]): 

210 self.errors.append("Group may either have OnlyShowIn or NotShowIn, but not both") 

211 

212 def checkKey(self, key, value, group): 

213 # standard keys  

214 if key == "Type": 

215 if value == "ServiceType" or value == "Service" or value == "FSDevice": 

216 self.warnings.append("Type=%s is a KDE extension" % key) 

217 elif value == "MimeType": 

218 self.warnings.append("Type=MimeType is deprecated") 

219 elif not (value == "Application" or value == "Link" or value == "Directory"): 

220 self.errors.append("Value of key 'Type' must be Application, Link or Directory, but is '%s'" % value) 

221 

222 if self.fileExtension == ".directory" and not value == "Directory": 

223 self.warnings.append("File extension is .directory, but Type is '%s'" % value) 

224 elif self.fileExtension == ".desktop" and value == "Directory": 

225 self.warnings.append("Files with Type=Directory should have the extension .directory") 

226 

227 if value == "Application": 

228 if "Exec" not in self.content[group]: 

229 self.warnings.append("Type=Application needs 'Exec' key") 

230 if value == "Link": 

231 if "URL" not in self.content[group]: 

232 self.warnings.append("Type=Link needs 'URL' key") 

233 

234 elif key == "Version": 

235 self.checkValue(key, value) 

236 

237 elif re.match("^Name"+xdg.Locale.regex+"$", key): 

238 pass # locale string 

239 

240 elif re.match("^GenericName"+xdg.Locale.regex+"$", key): 

241 pass # locale string 

242 

243 elif key == "NoDisplay": 

244 self.checkValue(key, value, type="boolean") 

245 

246 elif re.match("^Comment"+xdg.Locale.regex+"$", key): 

247 pass # locale string 

248 

249 elif re.match("^Icon"+xdg.Locale.regex+"$", key): 

250 self.checkValue(key, value) 

251 

252 elif key == "Hidden": 

253 self.checkValue(key, value, type="boolean") 

254 

255 elif key == "OnlyShowIn": 

256 self.checkValue(key, value, list=True) 

257 self.checkOnlyShowIn(value) 

258 

259 elif key == "NotShowIn": 

260 self.checkValue(key, value, list=True) 

261 self.checkOnlyShowIn(value) 

262 

263 elif key == "TryExec": 

264 self.checkValue(key, value) 

265 self.checkType(key, "Application") 

266 

267 elif key == "Exec": 

268 self.checkValue(key, value) 

269 self.checkType(key, "Application") 

270 

271 elif key == "Path": 

272 self.checkValue(key, value) 

273 self.checkType(key, "Application") 

274 

275 elif key == "Terminal": 

276 self.checkValue(key, value, type="boolean") 

277 self.checkType(key, "Application") 

278 

279 elif key == "Actions": 

280 self.checkValue(key, value, list=True) 

281 self.checkType(key, "Application") 

282 

283 elif key == "MimeType": 

284 self.checkValue(key, value, list=True) 

285 self.checkType(key, "Application") 

286 

287 elif key == "Categories": 

288 self.checkValue(key, value) 

289 self.checkType(key, "Application") 

290 self.checkCategories(value) 

291 

292 elif re.match("^Keywords"+xdg.Locale.regex+"$", key): 

293 self.checkValue(key, value, type="localestring", list=True) 

294 self.checkType(key, "Application") 

295 

296 elif key == "StartupNotify": 

297 self.checkValue(key, value, type="boolean") 

298 self.checkType(key, "Application") 

299 

300 elif key == "StartupWMClass": 

301 self.checkType(key, "Application") 

302 

303 elif key == "URL": 

304 self.checkValue(key, value) 

305 self.checkType(key, "URL") 

306 

307 # kde extensions 

308 elif key == "ServiceTypes": 

309 self.checkValue(key, value, list=True) 

310 self.warnings.append("Key '%s' is a KDE extension" % key) 

311 

312 elif key == "DocPath": 

313 self.checkValue(key, value) 

314 self.warnings.append("Key '%s' is a KDE extension" % key) 

315 

316 elif key == "InitialPreference": 

317 self.checkValue(key, value, type="numeric") 

318 self.warnings.append("Key '%s' is a KDE extension" % key) 

319 

320 elif key == "Dev": 

321 self.checkValue(key, value) 

322 self.checkType(key, "FSDevice") 

323 self.warnings.append("Key '%s' is a KDE extension" % key) 

324 

325 elif key == "FSType": 

326 self.checkValue(key, value) 

327 self.checkType(key, "FSDevice") 

328 self.warnings.append("Key '%s' is a KDE extension" % key) 

329 

330 elif key == "MountPoint": 

331 self.checkValue(key, value) 

332 self.checkType(key, "FSDevice") 

333 self.warnings.append("Key '%s' is a KDE extension" % key) 

334 

335 elif key == "ReadOnly": 

336 self.checkValue(key, value, type="boolean") 

337 self.checkType(key, "FSDevice") 

338 self.warnings.append("Key '%s' is a KDE extension" % key) 

339 

340 elif re.match("^UnmountIcon"+xdg.Locale.regex+"$", key): 

341 self.checkValue(key, value) 

342 self.checkType(key, "FSDevice") 

343 self.warnings.append("Key '%s' is a KDE extension" % key) 

344 

345 # deprecated keys 

346 elif key == "Encoding": 

347 self.checkValue(key, value) 

348 self.warnings.append("Key '%s' is deprecated" % key) 

349 

350 elif re.match("^MiniIcon"+xdg.Locale.regex+"$", key): 

351 self.checkValue(key, value) 

352 self.warnings.append("Key '%s' is deprecated" % key) 

353 

354 elif key == "TerminalOptions": 

355 self.checkValue(key, value) 

356 self.warnings.append("Key '%s' is deprecated" % key) 

357 

358 elif key == "DefaultApp": 

359 self.checkValue(key, value) 

360 self.warnings.append("Key '%s' is deprecated" % key) 

361 

362 elif key == "Protocols": 

363 self.checkValue(key, value, list=True) 

364 self.warnings.append("Key '%s' is deprecated" % key) 

365 

366 elif key == "Extensions": 

367 self.checkValue(key, value, list=True) 

368 self.warnings.append("Key '%s' is deprecated" % key) 

369 

370 elif key == "BinaryPattern": 

371 self.checkValue(key, value) 

372 self.warnings.append("Key '%s' is deprecated" % key) 

373 

374 elif key == "MapNotify": 

375 self.checkValue(key, value) 

376 self.warnings.append("Key '%s' is deprecated" % key) 

377 

378 elif re.match("^SwallowTitle"+xdg.Locale.regex+"$", key): 

379 self.warnings.append("Key '%s' is deprecated" % key) 

380 

381 elif key == "SwallowExec": 

382 self.checkValue(key, value) 

383 self.warnings.append("Key '%s' is deprecated" % key) 

384 

385 elif key == "FilePattern": 

386 self.checkValue(key, value, type="regex", list=True) 

387 self.warnings.append("Key '%s' is deprecated" % key) 

388 

389 elif key == "SortOrder": 

390 self.checkValue(key, value, list=True) 

391 self.warnings.append("Key '%s' is deprecated" % key) 

392 

393 # "X-" extensions 

394 elif re.match("^X-[a-zA-Z0-9-]+", key): 

395 pass 

396 

397 else: 

398 self.errors.append("Invalid key: %s" % key) 

399 

400 def checkType(self, key, type): 

401 if not self.getType() == type: 

402 self.errors.append("Key '%s' only allowed in Type=%s" % (key, type)) 

403 

404 def checkOnlyShowIn(self, value): 

405 values = self.getList(value) 

406 valid = ["GNOME", "KDE", "LXDE", "MATE", "Razor", "ROX", "TDE", "Unity", 

407 "XFCE", "Old"] 

408 for item in values: 

409 if item not in valid and item[0:2] != "X-": 

410 self.errors.append("'%s' is not a registered OnlyShowIn value" % item); 

411 

412 def checkCategories(self, value): 

413 values = self.getList(value) 

414 

415 main = ["AudioVideo", "Audio", "Video", "Development", "Education", "Game", "Graphics", "Network", "Office", "Science", "Settings", "System", "Utility"] 

416 if not any(item in main for item in values): 

417 self.errors.append("Missing main category") 

418 

419 additional = ['Building', 'Debugger', 'IDE', 'GUIDesigner', 'Profiling', 'RevisionControl', 'Translation', 'Calendar', 'ContactManagement', 'Database', 'Dictionary', 'Chart', 'Email', 'Finance', 'FlowChart', 'PDA', 'ProjectManagement', 'Presentation', 'Spreadsheet', 'WordProcessor', '2DGraphics', 'VectorGraphics', 'RasterGraphics', '3DGraphics', 'Scanning', 'OCR', 'Photography', 'Publishing', 'Viewer', 'TextTools', 'DesktopSettings', 'HardwareSettings', 'Printing', 'PackageManager', 'Dialup', 'InstantMessaging', 'Chat', 'IRCClient', 'Feed', 'FileTransfer', 'HamRadio', 'News', 'P2P', 'RemoteAccess', 'Telephony', 'TelephonyTools', 'VideoConference', 'WebBrowser', 'WebDevelopment', 'Midi', 'Mixer', 'Sequencer', 'Tuner', 'TV', 'AudioVideoEditing', 'Player', 'Recorder', 'DiscBurning', 'ActionGame', 'AdventureGame', 'ArcadeGame', 'BoardGame', 'BlocksGame', 'CardGame', 'KidsGame', 'LogicGame', 'RolePlaying', 'Shooter', 'Simulation', 'SportsGame', 'StrategyGame', 'Art', 'Construction', 'Music', 'Languages', 'ArtificialIntelligence', 'Astronomy', 'Biology', 'Chemistry', 'ComputerScience', 'DataVisualization', 'Economy', 'Electricity', 'Geography', 'Geology', 'Geoscience', 'History', 'Humanities', 'ImageProcessing', 'Literature', 'Maps', 'Math', 'NumericalAnalysis', 'MedicalSoftware', 'Physics', 'Robotics', 'Spirituality', 'Sports', 'ParallelComputing', 'Amusement', 'Archiving', 'Compression', 'Electronics', 'Emulator', 'Engineering', 'FileTools', 'FileManager', 'TerminalEmulator', 'Filesystem', 'Monitor', 'Security', 'Accessibility', 'Calculator', 'Clock', 'TextEditor', 'Documentation', 'Adult', 'Core', 'KDE', 'GNOME', 'XFCE', 'GTK', 'Qt', 'Motif', 'Java', 'ConsoleOnly'] 

420 allcategories = additional + main 

421 

422 for item in values: 

423 if item not in allcategories and not item.startswith("X-"): 

424 self.errors.append("'%s' is not a registered Category" % item); 

425 

426 def checkCategorie(self, value): 

427 """Deprecated alias for checkCategories - only exists for backwards 

428 compatibility. 

429 """ 

430 warnings.warn("checkCategorie is deprecated, use checkCategories", 

431 DeprecationWarning) 

432 return self.checkCategories(value) 

433