././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1603032827.767752 pyxdg-0.27/0000775000175000017500000000000000000000000014676 5ustar00takluyvertakluyver00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1479086820.0 pyxdg-0.27/AUTHORS0000664000175000017500000000022200000000000015742 0ustar00takluyvertakluyver00000000000000Current Maintainer: Thomas Kluyver Inactive: Heinrich Wendel Sergey Kuleshov ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1479086820.0 pyxdg-0.27/COPYING0000664000175000017500000006130300000000000015734 0ustar00takluyvertakluyver00000000000000 GNU LIBRARY GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the library GPL. It is numbered 2 because it goes with version 2 of the ordinary GPL.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Library General Public License, applies to some specially designated Free Software Foundation software, and to any other libraries whose authors decide to use it. You can use it for your libraries, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library, or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link a program with the library, you must provide complete object files to the recipients so that they can relink them with the library, after making changes to the library and recompiling it. And you must show them these terms so they know their rights. Our method of protecting your rights has two steps: (1) copyright the library, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the library. Also, for each distributor's protection, we want to make certain that everyone understands that there is no warranty for this free library. If the library is modified by someone else and passed on, we want its recipients to know that what they have is not the original version, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that companies distributing free software will individually obtain patent licenses, thus in effect transforming the program into proprietary software. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License, which was designed for utility programs. This license, the GNU Library General Public License, applies to certain designated libraries. This license is quite different from the ordinary one; be sure to read it in full, and don't assume that anything in it is the same as in the ordinary license. The reason we have a separate public license for some libraries is that they blur the distinction we usually make between modifying or adding to a program and simply using it. Linking a program with a library, without changing the library, is in some sense simply using the library, and is analogous to running a utility program or application program. However, in a textual and legal sense, the linked executable is a combined work, a derivative of the original library, and the ordinary General Public License treats it as such. Because of this blurred distinction, using the ordinary General Public License for libraries did not effectively promote software sharing, because most developers did not use the libraries. We concluded that weaker conditions might promote sharing better. However, unrestricted linking of non-free programs would deprive the users of those programs of all benefit from the free status of the libraries themselves. This Library General Public License is intended to permit developers of non-free programs to use free libraries, while preserving your freedom as a user of such programs to change the free libraries that are incorporated in them. (We have not seen how to achieve this as regards changes in header files, but we have achieved it as regards changes in the actual functions of the Library.) The hope is that this will lead to faster development of free libraries. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, while the latter only works together with the library. Note that it is possible for a library to be covered by the ordinary General Public License rather than by this special one. GNU LIBRARY GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Library General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also compile or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. c) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. d) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Library General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS Appendix: How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1479086820.0 pyxdg-0.27/ChangeLog0000664000175000017500000002021700000000000016452 0ustar00takluyvertakluyver00000000000000Version 0.26 () * DesktopEntry: Add a method to check the existence of the TryExec value, Debian bug #618514. Version 0.25 (December 2012) * Add support for $XDG_RUNTIME_DIR, Debian bug #656338. * Allow desktop entry files that are not encoded in UTF-8, Debian bug #693855. * Mime: Add support for subclasses and aliases. Version 0.24 (October 2012) * Update allowed DesktopEntry categories following changes to the specification. * Fix removal of empty submenu, freedesktop bug #54747. * Documentation is now available on RTD: http://pyxdg.readthedocs.org/ * A few more tests, and some code cleanup. * Fix failure to parse some menu files when kde-config is missing, freedesktop bug #56426. Version 0.23 (July 2012) * Fix a test for non-UTF-8 locales. Version 0.22 (July 2012) * Better unicode handling in several modules. * Fix for sorting non-ASCII menu entries, freedesktop bug #52492. * More tests. Version 0.21 (July 2012) * Tests can now be run conveniently using nosetests, and cover more of the code. * BaseDirectory: New save_cache_path() function, freedesktop bug #26458. * Config: Default icon theme is 'hicolor', not 'highcolor'. * Menu: Obsolete Rule.compile() method removed. * DesktopEntry: Corrected spelling of checkCategories() method, freedesktop bug #24974. * DesktopEntry: Consider Actions and Keywords keys standard. * DesktopEntry: Accept non-ASCII Keywords. * DesktopEntry: Update list of environments valid for OnlyShowIn. * Mime: Fix get_type_by_contents() in Python 3. * RecentFiles: Minor bug fixes. Version 0.20 (June 2012) * Compatible with Python 3; requires Python 2.6 or later * Clean up accidental GPL license notice in Menu.py * Add test scripts for xdg.Mime, xdg.Locale and xdg.RecentFiles * Fixes for icon theme validation * Fix exception in xdg.Mime * Replace invalid string exceptions * Fall back to default base directories if $XDG* environment variables are set but empty. * Remove use of deprecated os.popen3 in Menu.py * Correct URLs in README Version 0.19 * IniFile.py: add support for trusted desktop files (thanks to karl mikaelsson) * DesktopEntry.py: Support spec version 1.0, Debian bug #563660 * MimeType.py: Fix parsing of in memory data, Debian bug #563718 * DesktopEntry.py: Fix constructor, Debian bug #551297, #562951, #562952 Version 0.18 * DesktopEntry.py: Add getMimeTypes() method, correctly returning strings * DesktopEntry.py: Deprecated getMimeType() returning list of regex * Menu.py: Add support for XDG_MENU_PREFIX * Mime.py: Add get_type_by_contents() Version 0.17 2008-10-30 Heinrich Wendel * Menu.py: Python <= 2.3 compatibility fix * DesktopEntry.py: Fix wrong indention Version 0.16 2008-08-07 Heinrich Wendel * IconTheme.py: Add more directories to the pixmap path 2008-03-02 Heinrich Wendel * IniFile.py: Fix saving of relative filenames * IniFile.py, DesktopEntry.py: Fix __cmp__ method * IniFile.py, IconTheme.py: Better error handling Version 0.15 2005-08-10 Heinrich Wendel * Menu.py: Add support for TryExec 2005-08-09 Heinrich Wendel * Menu.py: Unicode bug fixed! * IconTheme.py: small speedup 2005-08-04 Heinrich Wendel * Menu.py, IconTheme.py: Bugfixes... * MenuEditor.py: Make xml nice; add hide/unhide functions Versinon 0.14 2005-06-02 Heinrich Wendel * Menu.py, MenuEditor.py: Bugfixes... version 0.13 2005-06-01 Heinrich Wendel * Menu.py, MenuEditor.py: Bugfixes... * Config.py: Add root_mode Version 0.12 2005-05-30 Heinrich Wendel * MenuEditor.py: New in this release, use to edit Menus thx to Travis Watkins and Matt Kynaston for their help * Menu.py, IniFile.py, DesktopEntry.py: Lot of bugfixing... * BaseDirectory.py: Add xdg_cache_home * IconTheme.py, Config.py: More caching stuff, make cachetime configurable Version 0.11 2005-05-23 Heinrich Wendel * DesktopEntry.p, Menu.py: A lot of bugfixes, thx to Travis Watkins 2005-05-02 Heinrich Wendel * Config.py: Module to configure Basic Settings, currently available: - Locale, IconTheme, IconSize, WindowManager * Locale.py: Internal Module to support Locales * Mime.py: Implementation of the Mime Specification * Menu.py: Now supports LegacyDirs * RecentFiles.py: Implementation of the Recent Files Specification Version 0.10 2005-04-26 Heinrich Wendel * Menu.py: various bug fixing to support version 1.0.draft-1 2005-04-13 Heinrich Wendel * IniFily.py: Detect if a .desktop file was edited * Menu.py Fix bug caused by excluding NoDisplay/Hidden Items to early Version 0.9 2005-03-23 Heinrich Wendel * IniFile.py: various speedups * Menu.py: add support for , menu-spec-0.91 2005-03-21 Heinrich Wendel * IniFily.py: Small fixes * Menu.py: remove __preparse and don't edit the parsed document, so menu editing is possible store parsed document in Menu.Doc store document name in Menu.Filename 2005-03-18 Heinrich Wendel * Menu.py: fix basename argument, thx to Matt Kynaston ; make it comply to menu-spec-0.9 2004-30-11 Heinrich Wendel * Update BaseDirectory.py to the current ROX version Version 0.8 2004-10-18 Ross Burton * xdg/DesktopEntry.py, xdg/IconTheme.py: Add . to the literal FileExtensions so that the checks work. * xdg/Menu.py: Don't read .desktop-* files, only .desktop 2004-10-18 Martin Grimme * xdg/IconTheme.py (getIconPath): The "hicolor" theme has to be used as the fallback. * xdg/IniFile.py (IniFile.getList): Fixed bug in splitting up strings. Version 0.7 2004-09-04 Heinrich Wendel * Add 'import codecs' to IniFile, needed by write support * Fix parsing of lists with only one entry Version 0.6 2004-08-04 Heinrich Wendel * Performance Improvements Version 0.5 2004-03-29 Heinrich Wendel * Finished Support for menu-spec 0.7 2004-03-27 Heinrich Wendel * 5* speed improvement in Menu.py parsing code 2004-03-20 Heinrich Wendel * check values of Categories/OnlyShowIn keys * various misc changes 2004-03-17 Martin Grimme * xdg/Menu.py (__preparse): * xdg/IconTheme.py (IconTheme.parse): Made compatible with Python 2.3 (None is a keyword). (__parseTheme): Prepend new icon themes to make sure that they have priority when looking up icons. (icondirs): Add "~/.icons" to the paths where to look for icons. Users may have icon themes installed in their home directory. 2003-10-08 Heinrich Wendel * Completed write-support in IniFile 2003-10-05 Heinrich Wendel * Added support for Hidden and NoDisplay in menu-spec * inital write-support in IniFile 2003-10-04 Heinrich Wendel * License change to LGPL-2 * initial support for menu-spec 0.7 Version 0.4 2003-09-30 Heinrich Wendel * Bugfix release Version 0.3 2003-09-12 Heinrich Wendel * Complete IconSpec implementation, including cache and validation 2003-09-07 Heinrich Wendel * Basedir spec converted to version 0.6 * First part of separating DesktopEntry backend in IniFile * added getPath(...) function to Menu.py Version 0.2 2003-09-05 Heinrich Wendel * Rewrite of menu-spec code * Taken basedir-spec code from ROX Version 0.1 2003-08-08 Heinrich Wendel * initial public release ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1479086820.0 pyxdg-0.27/INSTALL0000664000175000017500000000005700000000000015731 0ustar00takluyvertakluyver00000000000000Quite easy, just run: python setup.py install ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1479086820.0 pyxdg-0.27/MANIFEST.in0000664000175000017500000000022200000000000016430 0ustar00takluyvertakluyver00000000000000include AUTHORS include ChangeLog include COPYING include INSTALL include README include TODO include setup.py include test/*.py include xdg/*.py ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1603032827.767752 pyxdg-0.27/PKG-INFO0000664000175000017500000000102600000000000015772 0ustar00takluyvertakluyver00000000000000Metadata-Version: 1.2 Name: pyxdg Version: 0.27 Summary: PyXDG contains implementations of freedesktop.org standards in python. Home-page: http://freedesktop.org/wiki/Software/pyxdg Maintainer: Freedesktop.org Maintainer-email: xdg@lists.freedesktop.org License: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2) Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Desktop Environment ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1479086820.0 pyxdg-0.27/README0000664000175000017500000000136500000000000015563 0ustar00takluyvertakluyver00000000000000The XDG Package contains: - Implementation of the XDG-Base-Directory Standard http://standards.freedesktop.org/basedir-spec/ - Implementation of the XDG-Desktop Standard http://standards.freedesktop.org/desktop-entry-spec/ - Implementation of the XDG-Menu Standard http://standards.freedesktop.org/menu-spec/ - Implementation of the XDG-Icon-Theme Standard http://standards.freedesktop.org/icon-theme-spec/ - Implementation of the XDG-Shared MIME-info Database http://standards.freedesktop.org/shared-mime-info-spec/ - Implementation of the XDG-Recent File Storage Specification http://standards.freedesktop.org/recent-file-spec/ To run the tests, run nosetests in the top level directory. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1479086820.0 pyxdg-0.27/TODO0000664000175000017500000000010700000000000015364 0ustar00takluyvertakluyver00000000000000TODO: ===== Never Finished: - Performance improvements - Debug Info ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1603032827.7647521 pyxdg-0.27/pyxdg.egg-info/0000775000175000017500000000000000000000000017523 5ustar00takluyvertakluyver00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603032827.0 pyxdg-0.27/pyxdg.egg-info/PKG-INFO0000664000175000017500000000102600000000000020617 0ustar00takluyvertakluyver00000000000000Metadata-Version: 1.2 Name: pyxdg Version: 0.27 Summary: PyXDG contains implementations of freedesktop.org standards in python. Home-page: http://freedesktop.org/wiki/Software/pyxdg Maintainer: Freedesktop.org Maintainer-email: xdg@lists.freedesktop.org License: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2) Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Desktop Environment ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603032827.0 pyxdg-0.27/pyxdg.egg-info/SOURCES.txt0000664000175000017500000000116400000000000021411 0ustar00takluyvertakluyver00000000000000AUTHORS COPYING ChangeLog INSTALL MANIFEST.in README TODO setup.cfg setup.py pyxdg.egg-info/PKG-INFO pyxdg.egg-info/SOURCES.txt pyxdg.egg-info/dependency_links.txt pyxdg.egg-info/top_level.txt test/fuzz-mime.py test/resources.py test/test-basedirectory.py test/test-desktop.py test/test-icon.py test/test-inifile.py test/test-locale.py test/test-menu-rules.py test/test-menu.py test/test-mime.py test/test-recentfiles.py xdg/BaseDirectory.py xdg/Config.py xdg/DesktopEntry.py xdg/Exceptions.py xdg/IconTheme.py xdg/IniFile.py xdg/Locale.py xdg/Menu.py xdg/MenuEditor.py xdg/Mime.py xdg/RecentFiles.py xdg/__init__.py xdg/util.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603032827.0 pyxdg-0.27/pyxdg.egg-info/dependency_links.txt0000664000175000017500000000000100000000000023571 0ustar00takluyvertakluyver00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603032827.0 pyxdg-0.27/pyxdg.egg-info/top_level.txt0000664000175000017500000000000400000000000022247 0ustar00takluyvertakluyver00000000000000xdg ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1603032827.768752 pyxdg-0.27/setup.cfg0000664000175000017500000000010300000000000016511 0ustar00takluyvertakluyver00000000000000[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603032749.0 pyxdg-0.27/setup.py0000775000175000017500000000121100000000000016406 0ustar00takluyvertakluyver00000000000000#!/usr/bin/env python3 from setuptools import setup setup( name = "pyxdg", version = "0.27", description = "PyXDG contains implementations of freedesktop.org standards in python.", maintainer = "Freedesktop.org", maintainer_email = "xdg@lists.freedesktop.org", url = "http://freedesktop.org/wiki/Software/pyxdg", packages = ['xdg'], classifiers = [ "License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Topic :: Desktop Environment", ], ) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1603032827.765752 pyxdg-0.27/test/0000775000175000017500000000000000000000000015655 5ustar00takluyvertakluyver00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603032264.0 pyxdg-0.27/test/fuzz-mime.py0000775000175000017500000000063300000000000020157 0ustar00takluyvertakluyver00000000000000#!/usr/bin/env python3 """Run this manually to test xdg.Mime.get_type2 against all files in a directory. Syntax: ./fuzz-mime.py /dir/to/test/ """ from __future__ import print_function import sys, os from xdg import Mime testdir = sys.argv[1] files = os.listdir(testdir) for f in files: f = os.path.join(testdir, f) try: print(f, Mime.get_type2(f)) except: print(f) raise ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603032264.0 pyxdg-0.27/test/resources.py0000664000175000017500000003247500000000000020254 0ustar00takluyvertakluyver00000000000000# coding: utf-8 """This file contains sample data for the test suite - these are written out to temporary files for the relevant tests. """ from __future__ import unicode_literals import sys # With additions from firefox.desktop, to test locale & unicode support gedit_desktop = """[Desktop Entry] Name=gedit Name[ar]=متصفح الوِب فَيَرفُكْس GenericName=Text Editor Comment= Edit text files Keywords=Plaintext;Write; Keywords[ja]=Internet;WWW;Web;インターネット;ブラウザ;ウェブ;エクスプローラ Exec=gedit %U Terminal=false Type=Application StartupNotify=true MimeType=text/plain; Icon=accessories-text-editor Categories=GNOME;GTK;Utility;TextEditor; X-GNOME-DocPath=gedit/gedit.xml X-GNOME-FullName=Text Editor X-GNOME-Bugzilla-Bugzilla=GNOME X-GNOME-Bugzilla-Product=gedit X-GNOME-Bugzilla-Component=general X-GNOME-Bugzilla-Version=3.4.1 X-GNOME-Bugzilla-ExtraInfoScript=/usr/share/gedit/gedit-bugreport Actions=Window;Document; X-Ubuntu-Gettext-Domain=gedit [Desktop Action Window] Name=Open a New Window Exec=gedit --new-window OnlyShowIn=Unity; [Desktop Action Document] Name=Open a New Document Exec=gedit --new-window OnlyShowIn=Unity; """ # Unicode, + TryExec that doesn't exist unicode_desktop = """[Desktop Entry] Name=Abc€þ Type=Application Exec=date TryExec=ewoirjge """ # Invalid - see the Categories line spout_desktop = """[Desktop Entry] Type=Application Encoding=UTF-8 Name=Spout GenericName= Comment= Icon=spout Exec=/usr/games/spout Terminal=false Categories:Application:Game:ArcadeGame """ # Test with invalid UTF-8 gnome_alsamixer_desktop = """[Desktop Entry] Name=GNOME ALSA Mixer Comment=ALSA sound mixer for GNOME Comment[es]=Mezclador de sonido ALSA para GNOME Comment[fr]=Mélangeur de son ALSA pour GNOME Exec=gnome-alsamixer Type=Application """ # TryExec that should exist python_desktop = """[Desktop Entry] Name=Python Comment=Dynamic programming language Exec=%s TryExec=%s Type=Application """ % (sys.executable, sys.executable) recently_used = """ file:///home/thomas/foo/bar.ods application/vnd.oasis.opendocument.spreadsheet 1272385187 openoffice.org staroffice starsuite file:///tmp/2.ppt application/vnd.ms-powerpoint 1272378716 openoffice.org staroffice starsuite """ applications_menu = """ Applications X-GNOME-Menu-Applications.directory /etc/X11/applnk /usr/share/gnome/apps Accessories Utility.directory Utility Accessibility System Universal Access Utility-Accessibility.directory Accessibility Settings Development Development.directory Development emacs.desktop Education Education.directory Education Science Science GnomeScience.directory Education Science Games Game.directory Game ActionGame AdventureGame ArcadeGame BoardGame BlocksGame CardGame KidsGame LogicGame Simulation SportsGame StrategyGame Action ActionGames.directory ActionGame Adventure AdventureGames.directory AdventureGame Arcade ArcadeGames.directory ArcadeGame Board BoardGames.directory BoardGame Blocks BlocksGames.directory BlocksGame Cards CardGames.directory CardGame Kids KidsGames.directory KidsGame Logic LogicGames.directory LogicGame Role Playing RolePlayingGames.directory RolePlaying Simulation SimulationGames.directory Simulation Sports SportsGames.directory SportsGame Strategy StrategyGames.directory StrategyGame Graphics Graphics.directory Graphics Internet Network.directory Network Multimedia AudioVideo.directory AudioVideo Office Office.directory Office System System-Tools.directory System Settings Game Preferences Settings.directory Settings System X-GNOME-Settings-Panel Administration Settings-System.directory Settings System X-GNOME-Settings-Panel Other X-GNOME-Other.directory Core Screensaver X-GNOME-Settings-Panel Other Debian debian-menu.menu Debian.directory ubuntu-software-center.desktop ubuntu-software-center.desktop """ legacy_menu = """ Legacy legacy_dir """ kde_legacy_menu = """ KDE Legacy """ layout_menu = """ Layout More More Games Steam Action Steam Arcade Accessories """ mime_globs2_a = """#globs2 MIME data file 55:text/x-diff:*.patch 50:text/x-c++src:*.C:cs 50:text/x-python:*.py 10:text/x-readme:readme* """ mime_globs2_b = """#globs2 MIME data file # Add to existing MIMEtype 50:text/x-diff:*.diff # Remove one 50:text/x-python:__NOGLOBS__ # Replace one 40:text/x-readme:__NOGLOBS__ 20:text/x-readme:RDME:cs """ mime_magic_db = b"""MIME-Magic\0 [50:image/png] >0=\0\x04\x89PNG [50:image/jpeg] >0=\0\x03\xff\xd8\xff >0=\0\x02\xff\xd8 [50:image/openraster] >0=\0\x04PK\x03\x04 1>30=\0\x08mimetype 2>38=\0\x10image/openraster [80:image/svg+xml] >0=\0\x0d0=\0\x040=\0\x0a8BPS \0\0\0\0&\xff\xff\xff\xff\0\0\xff\xff\xff\xff [40:application/x-executable] >0=\0\02\x01\x11~2 [10:application/madeup] >0=\0\x05ab cd >10=\0\x05ab de&\xff\xff \xff\xff [10:application/imaginary] >0=\0\x03abc@unhandled_future_field [10:application/toberemoved] >0=\0\x03def [10:application/tobereplaced] >0=\0\x03ghi [10:application/tobeaddedto] >0=\0\x03mno """ mime_magic_db2 = b"""MIME-Magic\0 [10:application/toberemoved] >0=__NOMAGIC__ [10:application/tobereplaced] >0=__NOMAGIC__ >1=\0\x03jkl [10:application/tobeaddedto] >1=\0\x03pqr """ png_data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\rIDAT\x08\x99c\xf8\x7f\x83\xe1?\x00\x07\x88\x02\xd7\xd9\n\xd8\xdc\x00\x00\x00\x00IEND\xaeB`\x82' icon_data = """[Icon Data] DisplayName=Mime text/plain EmbeddedTextRectangle=100,100,900,900 AttachPoints=200,200|800,200|500,500|200,800|800,800 """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1479086820.0 pyxdg-0.27/test/test-basedirectory.py0000664000175000017500000000741300000000000022050 0ustar00takluyvertakluyver00000000000000from xdg import BaseDirectory import os from os import environ import unittest import tempfile, shutil import stat try: reload except NameError: from imp import reload class BaseDirectoryTest(unittest.TestCase): def setUp(self): self.environ_save = environ.copy() def tearDown(self): # Restore environment variables environ.clear() for k, v in self.environ_save.items(): environ[k] = v def test_save_config_path(self): tmpdir = tempfile.mkdtemp() try: environ['XDG_CONFIG_HOME'] = tmpdir reload(BaseDirectory) configpath = BaseDirectory.save_config_path("foo") self.assertEqual(configpath, os.path.join(tmpdir, "foo")) finally: shutil.rmtree(tmpdir) def test_save_data_path(self): tmpdir = tempfile.mkdtemp() try: environ['XDG_DATA_HOME'] = tmpdir reload(BaseDirectory) datapath = BaseDirectory.save_data_path("foo") self.assertEqual(datapath, os.path.join(tmpdir, "foo")) finally: shutil.rmtree(tmpdir) def test_save_cache_path(self): tmpdir = tempfile.mkdtemp() try: environ['XDG_CACHE_HOME'] = tmpdir reload(BaseDirectory) datapath = BaseDirectory.save_cache_path("foo") self.assertEqual(datapath, os.path.join(tmpdir, "foo")) finally: shutil.rmtree(tmpdir) def test_load_first_config(self): tmpdir = tempfile.mkdtemp() tmpdir2 = tempfile.mkdtemp() tmpdir3 = tempfile.mkdtemp() path = os.path.join(tmpdir3, "wpokewefketnrhruit") os.mkdir(path) try: environ['XDG_CONFIG_HOME'] = tmpdir environ['XDG_CONFIG_DIRS'] = ":".join([tmpdir2, tmpdir3]) reload(BaseDirectory) configdir = BaseDirectory.load_first_config("wpokewefketnrhruit") self.assertEqual(configdir, path) finally: shutil.rmtree(tmpdir) shutil.rmtree(tmpdir2) shutil.rmtree(tmpdir3) def test_load_config_paths(self): tmpdir = tempfile.mkdtemp() path = os.path.join(tmpdir, "wpokewefketnrhruit") os.mkdir(path) tmpdir2 = tempfile.mkdtemp() path2 = os.path.join(tmpdir2, "wpokewefketnrhruit") os.mkdir(path2) try: environ['XDG_CONFIG_HOME'] = tmpdir environ['XDG_CONFIG_DIRS'] = tmpdir2 + ":/etc/xdg" reload(BaseDirectory) configdirs = BaseDirectory.load_config_paths("wpokewefketnrhruit") self.assertEqual(list(configdirs), [path, path2]) finally: shutil.rmtree(tmpdir) shutil.rmtree(tmpdir2) def test_runtime_dir(self): rd = '/pyxdg-example/run/user/fred' environ['XDG_RUNTIME_DIR'] = rd self.assertEqual(BaseDirectory.get_runtime_dir(strict=True), rd) self.assertEqual(BaseDirectory.get_runtime_dir(strict=False), rd) def test_runtime_dir_notset(self): environ.pop('XDG_RUNTIME_DIR', None) self.assertRaises(KeyError, BaseDirectory.get_runtime_dir, strict=True) fallback = BaseDirectory.get_runtime_dir(strict=False) assert fallback.startswith('/tmp/'), fallback assert os.path.isdir(fallback), fallback mode = stat.S_IMODE(os.stat(fallback).st_mode) self.assertEqual(mode, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR) # Calling it again should return the same directory. fallback2 = BaseDirectory.get_runtime_dir(strict=False) self.assertEqual(fallback, fallback2) mode = stat.S_IMODE(os.stat(fallback2).st_mode) self.assertEqual(mode, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603032264.0 pyxdg-0.27/test/test-desktop.py0000664000175000017500000001041300000000000020654 0ustar00takluyvertakluyver00000000000000#!/usr/bin/env python3 # coding: utf-8 from xdg.DesktopEntry import DesktopEntry from xdg.Exceptions import ValidationError, ParsingError, NoKeyError from xdg.util import u import resources import io import os import shutil import re import tempfile import unittest class DesktopEntryTest(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() self.test_file = os.path.join(self.tmpdir, "gedit.desktop") with io.open(self.test_file, "w", encoding='utf-8') as f: f.write(resources.gedit_desktop) def tearDown(self): shutil.rmtree(self.tmpdir) def test_write_file(self): de = DesktopEntry() de.parse(self.test_file) de.removeKey("Name") de.addGroup("Hallo") de.set("key", "value", "Hallo") new_file = os.path.join(self.tmpdir, "test.desktop") de.write(new_file, trusted=True) with io.open(new_file, encoding='utf-8') as f: contents = f.read() assert "[Hallo]" in contents, contents assert re.search(r"key\s*=\s*value", contents), contents # This is missing the Name key, and has an unknown Hallo group, so it # shouldn't validate. new_entry = DesktopEntry(new_file) self.assertRaises(ValidationError, new_entry.validate) def test_validate(self): entry = DesktopEntry(self.test_file) entry.validate() def test_values(self): entry = DesktopEntry(self.test_file) self.assertEqual(entry.getName(), 'gedit') self.assertEqual(entry.getGenericName(), 'Text Editor') self.assertEqual(entry.getNoDisplay(), False) self.assertEqual(entry.getComment(), 'Edit text files') self.assertEqual(entry.getIcon(), 'accessories-text-editor') self.assertEqual(entry.getHidden(), False) self.assertEqual(entry.getOnlyShowIn(), []) self.assertEqual(entry.getExec(), 'gedit %U') self.assertEqual(entry.getTerminal(), False) self.assertEqual(entry.getMimeTypes(), ['text/plain']) self.assertEqual(entry.getCategories(), ['GNOME', 'GTK', 'Utility', 'TextEditor']) self.assertEqual(entry.getTerminal(), False) def test_basic(self): entry = DesktopEntry(self.test_file) assert entry.hasKey("Categories") assert not entry.hasKey("TryExec") assert entry.hasGroup("Desktop Action Window") assert not entry.hasGroup("Desktop Action Door") def test_unicode_name(self): with io.open(self.test_file, "w", encoding='utf-8') as f: f.write(resources.unicode_desktop) entry = DesktopEntry(self.test_file) self.assertEqual(entry.getName(), u('Abc€þ')) def test_invalid(self): test_file = os.path.join(self.tmpdir, "spout.desktop") with io.open(test_file, "w", encoding='utf-8') as f: f.write(resources.spout_desktop) self.assertRaises(ParsingError, DesktopEntry, test_file) def test_invalid_unicode(self): test_file = os.path.join(self.tmpdir, "gnome-alsamixer.desktop") with io.open(test_file, "w", encoding='latin-1') as f: f.write(resources.gnome_alsamixer_desktop) # Just check this doesn't throw a UnicodeError. DesktopEntry(test_file) class TestTryExec(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() self.test_file = os.path.join(self.tmpdir, "foo.desktop") def test_present(self): with io.open(self.test_file, "w", encoding='utf-8') as f: f.write(resources.python_desktop) entry = DesktopEntry(self.test_file) res = entry.findTryExec() assert res, repr(res) def test_absent(self): with io.open(self.test_file, "w", encoding='utf-8') as f: f.write(resources.unicode_desktop) entry = DesktopEntry(self.test_file) res = entry.findTryExec() assert res is None, repr(res) def test_no_TryExec(self): with io.open(self.test_file, "w", encoding='utf-8') as f: f.write(resources.gedit_desktop) entry = DesktopEntry(self.test_file) self.assertRaises(NoKeyError, entry.findTryExec) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603032264.0 pyxdg-0.27/test/test-icon.py0000664000175000017500000000325400000000000020140 0ustar00takluyvertakluyver00000000000000#!/usr/bin/env python3 from xdg.IconTheme import IconTheme, getIconPath, getIconData import tempfile, shutil, os import unittest import resources example_dir = os.path.join(os.path.dirname(__file__), 'example') def example_file(filename): return os.path.join(example_dir, filename) class IconThemeTest(unittest.TestCase): def test_find_icon_exists(self): print("Finding an icon that probably exists:") print (getIconPath("firefox")) def test_find_icon_nonexistant(self): icon = getIconPath("oijeorjewrjkngjhbqefew") assert icon is None, "%r is not None" % icon def test_validate_icon_theme(self): theme = IconTheme() theme.parse(example_file("index.theme")) theme.validate() class IconDataTest(unittest.TestCase): def test_read_icon_data(self): tmpdir = tempfile.mkdtemp() try: png_file = os.path.join(tmpdir, "test.png") with open(png_file, "wb") as f: f.write(resources.png_data) icon_file = os.path.join(tmpdir, "test.icon") with open(icon_file, "w") as f: f.write(resources.icon_data) icondata = getIconData(png_file) icondata.validate() self.assertEqual(icondata.getDisplayName(), 'Mime text/plain') self.assertEqual(icondata.getAttachPoints(), [(200,200), (800,200), (500,500), (200,800), (800,800)]) self.assertEqual(icondata.getEmbeddedTextRectangle(), [100,100,900,900]) assert " Accessibility Settings screenreader.desktop """, 'data': [ ('app1.desktop', ['Accessibility'], True), ('app2.desktop', ['Accessibility', 'Settings'], False), ('app3.desktop', ['Accessibility', 'Preferences'], True), ('app4.desktop', ['Graphics', 'Settings'], False), ('screenreader.desktop', ['Utility', 'Other'], True) ] }, { 'doc': """ Settings System X-GNOME-Settings-Panel foobar.desktop """, 'data': [ ('app0.desktop', [], False), ('app1.desktop', ['Settings'], True), ('app2.desktop', ['System', 'Settings'], False), ('app3.desktop', ['Games', 'Preferences'], False), ('app4.desktop', ['Graphics', 'Settings'], True), ('app5.desktop', ['X-GNOME-Settings-Panel', 'Settings'], False), ('foobar.desktop', ['Settings', 'Other'], False) ] }, # Empty conditions { 'doc': "", 'data': [ ('app0.desktop', ['Graphics', 'Settings'], False), ('screenreader.desktop', [], False) ] }, { 'doc': "", 'data': [ ('app0.desktop', ['Graphics', 'Settings'], False), ('screenreader.desktop', [], False) ] }, { 'doc': "", 'data': [ ('app0.desktop', ['Graphics', 'Settings'], False), ('screenreader.desktop', [], False) ] }, { 'doc': "", 'data': [ ('app0.desktop', ['Graphics', 'Settings'], False), ('screenreader.desktop', [], False) ] }, { 'doc': """ screenreader.desktop """, 'data': [ ('app0.desktop', ['Graphics', 'Settings'], False), ('screenreader.desktop', [], True) ] }, { 'doc': """ screenreader.desktop """, 'data': [ ('app0.desktop', ['Graphics', 'Settings'], False), ('screenreader.desktop', [], True) ] }, # Single condition { 'doc': "foobar.desktop", 'data': [ ('app0.desktop', ['Graphics', 'Settings'], False), ('foobar.desktop', [], True) ] }, # All { 'doc': "", 'data': [ ('app0.desktop', ['Graphics', 'Settings'], True), ('foobar.desktop', [], True) ] }, { 'doc': "foobar.desktop", 'data': [ ('app0.desktop', ['Graphics', 'Settings'], True), ('foobar.desktop', [], True) ] }, { 'doc': """ foobar.desktop Graphics """, 'data': [ ('app0.desktop', ['Graphics', 'Settings'], True), ('app1.desktop', ['Accessibility'], False), ('app2.desktop', ['Accessibility', 'Settings'], False), ('foobar.desktop', [], True), ] } ] class MockMenuEntry(object): def __init__(self, id, categories): self.DesktopFileID = id self.Categories = categories def __str__(self): return "<%s: %s>" % (self.DesktopFileID, self.Categories) class RulesTest(unittest.TestCase): """Basic rule matching tests""" def test_rule_from_node(self): parser = XMLMenuBuilder(debug=True) for i, test in enumerate(_tests): root = etree.fromstring(test['doc']) rule = parser.parse_rule(root) for j, data in enumerate(test['data']): menuentry = MockMenuEntry(data[0], data[1]) result = eval(rule.code) message = "Error in test %s with result set %s: got %s, expected %s" assert result == data[2], message % (i, j, result, data[2]) def test_rule_from_filename(self): tests = [ ('foobar.desktop', 'foobar.desktop', True), ('barfoo.desktop', 'foobar.desktop', False) ] for i, test in enumerate(tests): rule = Rule.fromFilename(Rule.TYPE_INCLUDE, test[0]) menuentry = MockMenuEntry(test[1], []) result = eval(rule.code) message = "Error with result set %s: got %s, expected %s" assert result == test[2], message % (i, result, test[2]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603032264.0 pyxdg-0.27/test/test-menu.py0000664000175000017500000000732000000000000020152 0ustar00takluyvertakluyver00000000000000#!/usr/bin/env python3 from __future__ import print_function import io import os import os.path import shutil import tempfile import unittest import xdg.Menu import xdg.DesktopEntry import resources def show_menu(menu, depth = 0): print(depth*"-" + "\x1b[01m" + menu.getName() + "\x1b[0m") depth += 1 for entry in menu.getEntries(): if isinstance(entry, xdg.Menu.Menu): show_menu(entry, depth) elif isinstance(entry, xdg.Menu.MenuEntry): print(depth*"-" + entry.DesktopEntry.getName()) print(depth*" " + menu.getPath(), entry.DesktopFileID, entry.DesktopEntry.getFileName()) elif isinstance(entry, xdg.Menu.Separator): print(depth*"-" + "|||") elif isinstance(entry, xdg.Menu.Header): print(depth*"-" + "\x1b[01m" + entry.Name + "\x1b[0m") depth -= 1 def entry_names(entries): names = [] for entry in entries: if isinstance(entry, xdg.Menu.Menu): names.append(entry.getName()) elif isinstance(entry, xdg.Menu.MenuEntry): names.append(entry.DesktopEntry.getName()) elif isinstance(entry, xdg.Menu.Separator): names.append("---") elif isinstance(entry, xdg.Menu.Header): names.append(entry.Name) return names class MenuTest(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() self.test_file = os.path.join(self.tmpdir, "applications.menu") with open(self.test_file, "w") as f: f.write(resources.applications_menu) def tearDown(self): shutil.rmtree(self.tmpdir) def test_parse_menu(self): menu = xdg.Menu.parse(self.test_file) show_menu(menu) # Check these don't throw an error menu.getName() menu.getGenericName() menu.getComment() menu.getIcon() def test_parse_layout(self): test_file = os.path.join(self.tmpdir, "layout.menu") with io.open(test_file, "w") as f: f.write(resources.layout_menu) menu = xdg.Menu.parse(test_file) show_menu(menu) assert len(menu.Entries) == 4 assert entry_names(menu.Entries) == ["Accessories", "Games", "---", "More"] games_menu = menu.getMenu("Games") assert len(games_menu.Entries) == 4 assert entry_names(games_menu.Entries) == ["Steam", "---", "Action", "Arcade"] def test_unicode_menuentry(self): test_file = os.path.join(self.tmpdir, "unicode.desktop") with io.open(test_file, 'w', encoding='utf-8') as f: f.write(resources.unicode_desktop) entry = xdg.Menu.MenuEntry(test_file) assert entry == entry assert not entry < entry def test_empty_legacy_dirs(self): legacy_dir = os.path.join(self.tmpdir, "applnk") os.mkdir(legacy_dir) os.mkdir(os.path.join(legacy_dir, "Toys")) os.mkdir(os.path.join(legacy_dir, "Utilities")) test_file = os.path.join(self.tmpdir, "legacy.menu") with open(test_file, "w") as f: f.write(resources.legacy_menu.replace("legacy_dir", legacy_dir)) menu = xdg.Menu.parse(test_file) # The menu should be empty besides the root named "Legacy" show_menu(menu) assert len(menu.Entries) == 0 def test_kde_legacy_dirs(self): """This was failing on systems which didn't have kde-config installed. We just check that parsing doesn't throw an error. See fd.o bug #56426. """ test_file = os.path.join(self.tmpdir, "kde_legacy.menu") with open(test_file, "w") as f: f.write(resources.kde_legacy_menu) menu = xdg.Menu.parse(test_file) if __name__ == '__main__': unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1517581597.0 pyxdg-0.27/test/test-mime.py0000664000175000017500000004050700000000000020141 0ustar00takluyvertakluyver00000000000000from xdg import Mime import unittest import os.path import tempfile, shutil import resources example_dir = os.path.join(os.path.dirname(__file__), 'example') def example_file(filename): return os.path.join(example_dir, filename) class MimeTestBase(unittest.TestCase): def check_mimetype(self, mimetype, media, subtype): self.assertEqual(mimetype.media, media) self.assertEqual(mimetype.subtype, subtype) class MimeTest(MimeTestBase): def test_create_mimetype(self): mt1 = Mime.MIMEtype('application', 'pdf') mt2 = Mime.MIMEtype('application', 'pdf') self.assertEqual(id(mt1), id(mt2)) # Check caching amr = Mime.MIMEtype('audio', 'AMR') self.check_mimetype(amr, 'audio', 'amr') # Check lowercase ogg = Mime.MIMEtype('audio/ogg') self.check_mimetype(ogg, 'audio', 'ogg') # Check split on / self.assertRaises(Exception, Mime.MIMEtype, 'audio/foo/bar') def test_get_type_by_name(self): appzip = Mime.get_type_by_name("foo.zip") self.check_mimetype(appzip, 'application', 'zip') def test_get_type_by_data(self): imgpng = Mime.get_type_by_data(resources.png_data) self.check_mimetype(imgpng, 'image', 'png') def test_mimetype_repr(self): mt = Mime.lookup('application', 'zip') repr(mt) # Just check that this doesn't throw an error. def test_get_type_by_contents(self): tmpdir = tempfile.mkdtemp() try: test_file = os.path.join(tmpdir, "test") with open(test_file, "wb") as f: f.write(resources.png_data) imgpng = Mime.get_type_by_contents(test_file) self.check_mimetype(imgpng, 'image', 'png') finally: shutil.rmtree(tmpdir) def test_get_type(self): # File that doesn't exist - get type by name imgpng = Mime.get_type(example_file("test.gif")) self.check_mimetype(imgpng, 'image', 'gif') # File that does exist - get type by contents imgpng = Mime.get_type(example_file("png_file")) self.check_mimetype(imgpng, 'image', 'png') # Directory - special case inodedir = Mime.get_type(example_file("subdir")) self.check_mimetype(inodedir, 'inode', 'directory') # Mystery files mystery_text = Mime.get_type(example_file('mystery_text')) self.check_mimetype(mystery_text, 'text', 'plain') mystery_exe = Mime.get_type(example_file('mystery_exe')) self.check_mimetype(mystery_exe, 'application', 'executable') # Symlink self.check_mimetype(Mime.get_type(example_file("png_symlink")), 'image', 'png') self.check_mimetype(Mime.get_type(example_file("png_symlink"), follow=False), 'inode', 'symlink') def test_get_type2(self): # File that doesn't exist - use the name self.check_mimetype(Mime.get_type2(example_file('test.gif')), 'image', 'gif') # File that does exist - use the contents self.check_mimetype(Mime.get_type2(example_file('png_file')), 'image', 'png') # Does exist - use name before contents self.check_mimetype(Mime.get_type2(example_file('file.png')), 'image', 'png') self.check_mimetype(Mime.get_type2(example_file('word.doc')), 'application', 'msword') # Ambiguous file extension glade_mime = Mime.get_type2(example_file('glade.ui')) self.assertEqual(glade_mime.media, 'application') # Grumble, this is still ambiguous on some systems self.assertIn(glade_mime.subtype, {'x-gtk-builder', 'x-glade'}) self.check_mimetype(Mime.get_type2(example_file('qtdesigner.ui')), 'application', 'x-designer') # text/x-python has greater weight than text/x-readme self.check_mimetype(Mime.get_type2(example_file('README.py')), 'text', 'x-python') # Directory - special filesystem object self.check_mimetype(Mime.get_type2(example_file('subdir')), 'inode', 'directory') # Mystery files: mystery_missing = Mime.get_type2(example_file('mystery_missing')) self.check_mimetype(mystery_missing, 'application', 'octet-stream') mystery_binary = Mime.get_type2(example_file('mystery_binary')) self.check_mimetype(mystery_binary, 'application', 'octet-stream') mystery_text = Mime.get_type2(example_file('mystery_text')) self.check_mimetype(mystery_text, 'text', 'plain') mystery_exe = Mime.get_type2(example_file('mystery_exe')) self.check_mimetype(mystery_exe, 'application', 'executable') # Symlink self.check_mimetype(Mime.get_type2(example_file("png_symlink")), 'image', 'png') self.check_mimetype(Mime.get_type2(example_file("png_symlink"), follow=False), 'inode', 'symlink') def test_lookup(self): pdf1 = Mime.lookup("application/pdf") pdf2 = Mime.lookup("application", "pdf") self.assertEqual(pdf1, pdf2) self.check_mimetype(pdf1, 'application', 'pdf') def test_get_comment(self): # Check these don't throw an error. One that is likely to exist: Mime.MIMEtype("application", "pdf").get_comment() # And one that's unlikely to exist: Mime.MIMEtype("application", "ierjg").get_comment() def test_by_name(self): dot_c = Mime.get_type_by_name('foo.c') self.check_mimetype(dot_c, 'text', 'x-csrc') dot_C = Mime.get_type_by_name('foo.C') self.check_mimetype(dot_C, 'text', 'x-c++src') # But most names should be case insensitive dot_GIF = Mime.get_type_by_name('IMAGE.GIF') self.check_mimetype(dot_GIF, 'image', 'gif') def test_canonical(self): text_xml = Mime.lookup('text/xml') self.check_mimetype(text_xml, 'text', 'xml') self.check_mimetype(text_xml.canonical(), 'application', 'xml') # Already is canonical python = Mime.lookup('text/x-python') self.check_mimetype(python.canonical(), 'text', 'x-python') def test_inheritance(self): text_python = Mime.lookup('text/x-python') self.check_mimetype(text_python, 'text', 'x-python') text_plain = Mime.lookup('text/plain') app_executable = Mime.lookup('application/x-executable') self.assertEqual(text_python.inherits_from(), set([text_plain, app_executable])) def test_is_text(self): assert Mime._is_text(b'abcdef \n') assert not Mime._is_text(b'abcdef\x08') assert not Mime._is_text(b'abcdef\x0e') assert not Mime._is_text(b'abcdef\x1f') assert not Mime._is_text(b'abcdef\x7f') # Check nonexistant file. assert not Mime.is_text_file('/fwoijorij') class MagicDBTest(MimeTestBase): def setUp(self): self.tmpdir = tempfile.mkdtemp() self.path = os.path.join(self.tmpdir, 'mimemagic') with open(self.path, 'wb') as f: f.write(resources.mime_magic_db) self.path2 = os.path.join(self.tmpdir, 'mimemagic2') with open(self.path2, 'wb') as f: f.write(resources.mime_magic_db2) # Read the files self.magic = Mime.MagicDB() self.magic.merge_file(self.path) self.magic.merge_file(self.path2) self.magic.finalise() def tearDown(self): shutil.rmtree(self.tmpdir) def test_parsing(self): self.assertEqual(len(self.magic.bytype), 9) # Check repr() doesn't throw an error repr(self.magic) prio, png = self.magic.bytype[Mime.lookup('image', 'png')][0] self.assertEqual(prio, 50) assert isinstance(png, Mime.MagicRule), type(png) repr(png) # Check this doesn't throw an error. self.assertEqual(png.start, 0) self.assertEqual(png.value, b'\x89PNG') self.assertEqual(png.mask, None) self.assertEqual(png.also, None) prio, jpeg = self.magic.bytype[Mime.lookup('image', 'jpeg')][0] assert isinstance(jpeg, Mime.MagicMatchAny), type(jpeg) self.assertEqual(len(jpeg.rules), 2) self.assertEqual(jpeg.rules[0].value, b'\xff\xd8\xff') prio, ora = self.magic.bytype[Mime.lookup('image', 'openraster')][0] assert isinstance(ora, Mime.MagicRule), type(ora) self.assertEqual(ora.value, b'PK\x03\x04') ora1 = ora.also assert ora1 is not None self.assertEqual(ora1.start, 30) ora2 = ora1.also assert ora2 is not None self.assertEqual(ora2.start, 38) self.assertEqual(ora2.value, b'image/openraster') prio, svg = self.magic.bytype[Mime.lookup('image', 'svg+xml')][0] self.assertEqual(len(svg.rules), 2) self.assertEqual(svg.rules[0].value, b'>file(os.path.join(dir, 'Options'), 'w'), "foo=2" Note: see the rox.Options module for a higher-level API for managing options. """ import os, stat _home = os.path.expanduser('~') xdg_data_home = os.environ.get('XDG_DATA_HOME') or \ os.path.join(_home, '.local', 'share') xdg_data_dirs = [xdg_data_home] + \ (os.environ.get('XDG_DATA_DIRS') or '/usr/local/share:/usr/share').split(':') xdg_config_home = os.environ.get('XDG_CONFIG_HOME') or \ os.path.join(_home, '.config') xdg_config_dirs = [xdg_config_home] + \ (os.environ.get('XDG_CONFIG_DIRS') or '/etc/xdg').split(':') xdg_cache_home = os.environ.get('XDG_CACHE_HOME') or \ os.path.join(_home, '.cache') xdg_data_dirs = [x for x in xdg_data_dirs if x] xdg_config_dirs = [x for x in xdg_config_dirs if x] def save_config_path(*resource): """Ensure ``$XDG_CONFIG_HOME//`` exists, and return its path. 'resource' should normally be the name of your application. Use this when saving configuration settings. """ resource = os.path.join(*resource) assert not resource.startswith('/') path = os.path.join(xdg_config_home, resource) if not os.path.isdir(path): os.makedirs(path, 0o700) return path def save_data_path(*resource): """Ensure ``$XDG_DATA_HOME//`` exists, and return its path. 'resource' should normally be the name of your application or a shared resource. Use this when saving or updating application data. """ resource = os.path.join(*resource) assert not resource.startswith('/') path = os.path.join(xdg_data_home, resource) if not os.path.isdir(path): os.makedirs(path) return path def save_cache_path(*resource): """Ensure ``$XDG_CACHE_HOME//`` exists, and return its path. 'resource' should normally be the name of your application or a shared resource.""" resource = os.path.join(*resource) assert not resource.startswith('/') path = os.path.join(xdg_cache_home, resource) if not os.path.isdir(path): os.makedirs(path) return path def load_config_paths(*resource): """Returns an iterator which gives each directory named 'resource' in the configuration search path. Information provided by earlier directories should take precedence over later ones, and the user-specific config dir comes first.""" resource = os.path.join(*resource) for config_dir in xdg_config_dirs: path = os.path.join(config_dir, resource) if os.path.exists(path): yield path def load_first_config(*resource): """Returns the first result from load_config_paths, or None if there is nothing to load.""" for x in load_config_paths(*resource): return x return None def load_data_paths(*resource): """Returns an iterator which gives each directory named 'resource' in the application data search path. Information provided by earlier directories should take precedence over later ones.""" resource = os.path.join(*resource) for data_dir in xdg_data_dirs: path = os.path.join(data_dir, resource) if os.path.exists(path): yield path def get_runtime_dir(strict=True): """Returns the value of $XDG_RUNTIME_DIR, a directory path. This directory is intended for 'user-specific non-essential runtime files and other file objects (such as sockets, named pipes, ...)', and 'communication and synchronization purposes'. As of late 2012, only quite new systems set $XDG_RUNTIME_DIR. If it is not set, with ``strict=True`` (the default), a KeyError is raised. With ``strict=False``, PyXDG will create a fallback under /tmp for the current user. This fallback does *not* provide the same guarantees as the specification requires for the runtime directory. The strict default is deliberately conservative, so that application developers can make a conscious decision to allow the fallback. """ try: return os.environ['XDG_RUNTIME_DIR'] except KeyError: if strict: raise import getpass fallback = '/tmp/pyxdg-runtime-dir-fallback-' + getpass.getuser() create = False try: # This must be a real directory, not a symlink, so attackers can't # point it elsewhere. So we use lstat to check it. st = os.lstat(fallback) except OSError as e: import errno if e.errno == errno.ENOENT: create = True else: raise else: # The fallback must be a directory if not stat.S_ISDIR(st.st_mode): os.unlink(fallback) create = True # Must be owned by the user and not accessible by anyone else elif (st.st_uid != os.getuid()) \ or (st.st_mode & (stat.S_IRWXG | stat.S_IRWXO)): os.rmdir(fallback) create = True if create: os.mkdir(fallback, 0o700) return fallback ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1479086820.0 pyxdg-0.27/xdg/Config.py0000664000175000017500000000133000000000000017234 0ustar00takluyvertakluyver00000000000000""" Functions to configure Basic Settings """ language = "C" windowmanager = None icon_theme = "hicolor" icon_size = 48 cache_time = 5 root_mode = False def setWindowManager(wm): global windowmanager windowmanager = wm def setIconTheme(theme): global icon_theme icon_theme = theme import xdg.IconTheme xdg.IconTheme.themes = [] def setIconSize(size): global icon_size icon_size = size def setCacheTime(time): global cache_time cache_time = time def setLocale(lang): import locale lang = locale.normalize(lang) locale.setlocale(locale.LC_ALL, lang) import xdg.Locale xdg.Locale.update(lang) def setRootMode(boolean): global root_mode root_mode = boolean ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1517581257.0 pyxdg-0.27/xdg/DesktopEntry.py0000664000175000017500000004206600000000000020475 0ustar00takluyvertakluyver00000000000000""" Complete implementation of the XDG Desktop Entry Specification http://standards.freedesktop.org/desktop-entry-spec/ Not supported: - Encoding: Legacy Mixed - Does not check exec parameters - Does not check URL's - Does not completly validate deprecated/kde items - Does not completly check categories """ from xdg.IniFile import IniFile, is_ascii import xdg.Locale from xdg.Exceptions import ParsingError from xdg.util import which import os.path import re import warnings class DesktopEntry(IniFile): "Class to parse and validate Desktop Entries" defaultGroup = 'Desktop Entry' def __init__(self, filename=None): """Create a new DesktopEntry. If filename exists, it will be parsed as a desktop entry file. If not, or if filename is None, a blank DesktopEntry is created. """ self.content = dict() if filename and os.path.exists(filename): self.parse(filename) elif filename: self.new(filename) def __str__(self): return self.getName() def parse(self, file): """Parse a desktop entry file. This can raise :class:`~xdg.Exceptions.ParsingError`, :class:`~xdg.Exceptions.DuplicateGroupError` or :class:`~xdg.Exceptions.DuplicateKeyError`. """ IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"]) def findTryExec(self): """Looks in the PATH for the executable given in the TryExec field. Returns the full path to the executable if it is found, None if not. Raises :class:`~xdg.Exceptions.NoKeyError` if TryExec is not present. """ tryexec = self.get('TryExec', strict=True) return which(tryexec) # start standard keys def getType(self): return self.get('Type') def getVersion(self): """deprecated, use getVersionString instead """ return self.get('Version', type="numeric") def getVersionString(self): return self.get('Version') def getName(self): return self.get('Name', locale=True) def getGenericName(self): return self.get('GenericName', locale=True) def getNoDisplay(self): return self.get('NoDisplay', type="boolean") def getComment(self): return self.get('Comment', locale=True) def getIcon(self): return self.get('Icon', locale=True) def getHidden(self): return self.get('Hidden', type="boolean") def getOnlyShowIn(self): return self.get('OnlyShowIn', list=True) def getNotShowIn(self): return self.get('NotShowIn', list=True) def getTryExec(self): return self.get('TryExec') def getExec(self): return self.get('Exec') def getPath(self): return self.get('Path') def getTerminal(self): return self.get('Terminal', type="boolean") def getMimeType(self): """deprecated, use getMimeTypes instead """ return self.get('MimeType', list=True, type="regex") def getMimeTypes(self): return self.get('MimeType', list=True) def getCategories(self): return self.get('Categories', list=True) def getStartupNotify(self): return self.get('StartupNotify', type="boolean") def getStartupWMClass(self): return self.get('StartupWMClass') def getURL(self): return self.get('URL') # end standard keys # start kde keys def getServiceTypes(self): return self.get('ServiceTypes', list=True) def getDocPath(self): return self.get('DocPath') def getKeywords(self): return self.get('Keywords', list=True, locale=True) def getInitialPreference(self): return self.get('InitialPreference') def getDev(self): return self.get('Dev') def getFSType(self): return self.get('FSType') def getMountPoint(self): return self.get('MountPoint') def getReadonly(self): return self.get('ReadOnly', type="boolean") def getUnmountIcon(self): return self.get('UnmountIcon', locale=True) # end kde keys # start deprecated keys def getMiniIcon(self): return self.get('MiniIcon', locale=True) def getTerminalOptions(self): return self.get('TerminalOptions') def getDefaultApp(self): return self.get('DefaultApp') def getProtocols(self): return self.get('Protocols', list=True) def getExtensions(self): return self.get('Extensions', list=True) def getBinaryPattern(self): return self.get('BinaryPattern') def getMapNotify(self): return self.get('MapNotify') def getEncoding(self): return self.get('Encoding') def getSwallowTitle(self): return self.get('SwallowTitle', locale=True) def getSwallowExec(self): return self.get('SwallowExec') def getSortOrder(self): return self.get('SortOrder', list=True) def getFilePattern(self): return self.get('FilePattern', type="regex") def getActions(self): return self.get('Actions', list=True) # end deprecated keys # desktop entry edit stuff def new(self, filename): """Make this instance into a new, blank desktop entry. If filename has a .desktop extension, Type is set to Application. If it has a .directory extension, Type is Directory. Other extensions will cause :class:`~xdg.Exceptions.ParsingError` to be raised. """ if os.path.splitext(filename)[1] == ".desktop": type = "Application" elif os.path.splitext(filename)[1] == ".directory": type = "Directory" else: raise ParsingError("Unknown extension", filename) self.content = dict() self.addGroup(self.defaultGroup) self.set("Type", type) self.filename = filename # end desktop entry edit stuff # validation stuff def checkExtras(self): # header if self.defaultGroup == "KDE Desktop Entry": self.warnings.append('[KDE Desktop Entry]-Header is deprecated') # file extension if self.fileExtension == ".kdelnk": self.warnings.append("File extension .kdelnk is deprecated") elif self.fileExtension != ".desktop" and self.fileExtension != ".directory": self.warnings.append('Unknown File extension') # Type try: self.type = self.content[self.defaultGroup]["Type"] except KeyError: self.errors.append("Key 'Type' is missing") # Name try: self.name = self.content[self.defaultGroup]["Name"] except KeyError: self.errors.append("Key 'Name' is missing") def checkGroup(self, group): # check if group header is valid if not (group == self.defaultGroup \ or re.match("^Desktop Action [a-zA-Z0-9-]+$", group) \ or (re.match("^X-", group) and is_ascii(group))): self.errors.append("Invalid Group name: %s" % group) else: #OnlyShowIn and NotShowIn if ("OnlyShowIn" in self.content[group]) and ("NotShowIn" in self.content[group]): self.errors.append("Group may either have OnlyShowIn or NotShowIn, but not both") def checkKey(self, key, value, group): # standard keys if key == "Type": if value == "ServiceType" or value == "Service" or value == "FSDevice": self.warnings.append("Type=%s is a KDE extension" % key) elif value == "MimeType": self.warnings.append("Type=MimeType is deprecated") elif not (value == "Application" or value == "Link" or value == "Directory"): self.errors.append("Value of key 'Type' must be Application, Link or Directory, but is '%s'" % value) if self.fileExtension == ".directory" and not value == "Directory": self.warnings.append("File extension is .directory, but Type is '%s'" % value) elif self.fileExtension == ".desktop" and value == "Directory": self.warnings.append("Files with Type=Directory should have the extension .directory") if value == "Application": if "Exec" not in self.content[group]: self.warnings.append("Type=Application needs 'Exec' key") if value == "Link": if "URL" not in self.content[group]: self.warnings.append("Type=Link needs 'URL' key") elif key == "Version": self.checkValue(key, value) elif re.match("^Name"+xdg.Locale.regex+"$", key): pass # locale string elif re.match("^GenericName"+xdg.Locale.regex+"$", key): pass # locale string elif key == "NoDisplay": self.checkValue(key, value, type="boolean") elif re.match("^Comment"+xdg.Locale.regex+"$", key): pass # locale string elif re.match("^Icon"+xdg.Locale.regex+"$", key): self.checkValue(key, value) elif key == "Hidden": self.checkValue(key, value, type="boolean") elif key == "OnlyShowIn": self.checkValue(key, value, list=True) self.checkOnlyShowIn(value) elif key == "NotShowIn": self.checkValue(key, value, list=True) self.checkOnlyShowIn(value) elif key == "TryExec": self.checkValue(key, value) self.checkType(key, "Application") elif key == "Exec": self.checkValue(key, value) self.checkType(key, "Application") elif key == "Path": self.checkValue(key, value) self.checkType(key, "Application") elif key == "Terminal": self.checkValue(key, value, type="boolean") self.checkType(key, "Application") elif key == "Actions": self.checkValue(key, value, list=True) self.checkType(key, "Application") elif key == "MimeType": self.checkValue(key, value, list=True) self.checkType(key, "Application") elif key == "Categories": self.checkValue(key, value) self.checkType(key, "Application") self.checkCategories(value) elif re.match("^Keywords"+xdg.Locale.regex+"$", key): self.checkValue(key, value, type="localestring", list=True) self.checkType(key, "Application") elif key == "StartupNotify": self.checkValue(key, value, type="boolean") self.checkType(key, "Application") elif key == "StartupWMClass": self.checkType(key, "Application") elif key == "URL": self.checkValue(key, value) self.checkType(key, "URL") # kde extensions elif key == "ServiceTypes": self.checkValue(key, value, list=True) self.warnings.append("Key '%s' is a KDE extension" % key) elif key == "DocPath": self.checkValue(key, value) self.warnings.append("Key '%s' is a KDE extension" % key) elif key == "InitialPreference": self.checkValue(key, value, type="numeric") self.warnings.append("Key '%s' is a KDE extension" % key) elif key == "Dev": self.checkValue(key, value) self.checkType(key, "FSDevice") self.warnings.append("Key '%s' is a KDE extension" % key) elif key == "FSType": self.checkValue(key, value) self.checkType(key, "FSDevice") self.warnings.append("Key '%s' is a KDE extension" % key) elif key == "MountPoint": self.checkValue(key, value) self.checkType(key, "FSDevice") self.warnings.append("Key '%s' is a KDE extension" % key) elif key == "ReadOnly": self.checkValue(key, value, type="boolean") self.checkType(key, "FSDevice") self.warnings.append("Key '%s' is a KDE extension" % key) elif re.match("^UnmountIcon"+xdg.Locale.regex+"$", key): self.checkValue(key, value) self.checkType(key, "FSDevice") self.warnings.append("Key '%s' is a KDE extension" % key) # deprecated keys elif key == "Encoding": self.checkValue(key, value) self.warnings.append("Key '%s' is deprecated" % key) elif re.match("^MiniIcon"+xdg.Locale.regex+"$", key): self.checkValue(key, value) self.warnings.append("Key '%s' is deprecated" % key) elif key == "TerminalOptions": self.checkValue(key, value) self.warnings.append("Key '%s' is deprecated" % key) elif key == "DefaultApp": self.checkValue(key, value) self.warnings.append("Key '%s' is deprecated" % key) elif key == "Protocols": self.checkValue(key, value, list=True) self.warnings.append("Key '%s' is deprecated" % key) elif key == "Extensions": self.checkValue(key, value, list=True) self.warnings.append("Key '%s' is deprecated" % key) elif key == "BinaryPattern": self.checkValue(key, value) self.warnings.append("Key '%s' is deprecated" % key) elif key == "MapNotify": self.checkValue(key, value) self.warnings.append("Key '%s' is deprecated" % key) elif re.match("^SwallowTitle"+xdg.Locale.regex+"$", key): self.warnings.append("Key '%s' is deprecated" % key) elif key == "SwallowExec": self.checkValue(key, value) self.warnings.append("Key '%s' is deprecated" % key) elif key == "FilePattern": self.checkValue(key, value, type="regex", list=True) self.warnings.append("Key '%s' is deprecated" % key) elif key == "SortOrder": self.checkValue(key, value, list=True) self.warnings.append("Key '%s' is deprecated" % key) # "X-" extensions elif re.match("^X-[a-zA-Z0-9-]+", key): pass else: self.errors.append("Invalid key: %s" % key) def checkType(self, key, type): if not self.getType() == type: self.errors.append("Key '%s' only allowed in Type=%s" % (key, type)) def checkOnlyShowIn(self, value): values = self.getList(value) valid = ["GNOME", "KDE", "LXDE", "MATE", "Razor", "ROX", "TDE", "Unity", "XFCE", "Old"] for item in values: if item not in valid and item[0:2] != "X-": self.errors.append("'%s' is not a registered OnlyShowIn value" % item); def checkCategories(self, value): values = self.getList(value) main = ["AudioVideo", "Audio", "Video", "Development", "Education", "Game", "Graphics", "Network", "Office", "Science", "Settings", "System", "Utility"] if not any(item in main for item in values): self.errors.append("Missing main category") 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'] allcategories = additional + main for item in values: if item not in allcategories and not item.startswith("X-"): self.errors.append("'%s' is not a registered Category" % item); def checkCategorie(self, value): """Deprecated alias for checkCategories - only exists for backwards compatibility. """ warnings.warn("checkCategorie is deprecated, use checkCategories", DeprecationWarning) return self.checkCategories(value) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1479086820.0 pyxdg-0.27/xdg/Exceptions.py0000664000175000017500000000467600000000000020170 0ustar00takluyvertakluyver00000000000000""" Exception Classes for the xdg package """ debug = False class Error(Exception): """Base class for exceptions defined here.""" def __init__(self, msg): self.msg = msg Exception.__init__(self, msg) def __str__(self): return self.msg class ValidationError(Error): """Raised when a file fails to validate. The filename is the .file attribute. """ def __init__(self, msg, file): self.msg = msg self.file = file Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg)) class ParsingError(Error): """Raised when a file cannot be parsed. The filename is the .file attribute. """ def __init__(self, msg, file): self.msg = msg self.file = file Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg)) class NoKeyError(Error): """Raised when trying to access a nonexistant key in an INI-style file. Attributes are .key, .group and .file. """ def __init__(self, key, group, file): Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file)) self.key = key self.group = group self.file = file class DuplicateKeyError(Error): """Raised when the same key occurs twice in an INI-style file. Attributes are .key, .group and .file. """ def __init__(self, key, group, file): Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file)) self.key = key self.group = group self.file = file class NoGroupError(Error): """Raised when trying to access a nonexistant group in an INI-style file. Attributes are .group and .file. """ def __init__(self, group, file): Error.__init__(self, "No group: %s in file %s" % (group, file)) self.group = group self.file = file class DuplicateGroupError(Error): """Raised when the same key occurs twice in an INI-style file. Attributes are .group and .file. """ def __init__(self, group, file): Error.__init__(self, "Duplicate group: %s in file %s" % (group, file)) self.group = group self.file = file class NoThemeError(Error): """Raised when trying to access a nonexistant icon theme. The name of the theme is the .theme attribute. """ def __init__(self, theme): Error.__init__(self, "No such icon-theme: %s" % theme) self.theme = theme ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1517581703.0 pyxdg-0.27/xdg/IconTheme.py0000664000175000017500000003732700000000000017721 0ustar00takluyvertakluyver00000000000000""" Complete implementation of the XDG Icon Spec http://standards.freedesktop.org/icon-theme-spec/ """ import os, time import re from xdg.IniFile import IniFile, is_ascii from xdg.BaseDirectory import xdg_data_dirs from xdg.Exceptions import NoThemeError, debug import xdg.Config class IconTheme(IniFile): "Class to parse and validate IconThemes" def __init__(self): IniFile.__init__(self) def __repr__(self): return self.name def parse(self, file): IniFile.parse(self, file, ["Icon Theme", "KDE Icon Theme"]) self.dir = os.path.dirname(file) (nil, self.name) = os.path.split(self.dir) def getDir(self): return self.dir # Standard Keys def getName(self): return self.get('Name', locale=True) def getComment(self): return self.get('Comment', locale=True) def getInherits(self): return self.get('Inherits', list=True) def getDirectories(self): return self.get('Directories', list=True) def getScaledDirectories(self): return self.get('ScaledDirectories', list=True) def getHidden(self): return self.get('Hidden', type="boolean") def getExample(self): return self.get('Example') # Per Directory Keys def getSize(self, directory): return self.get('Size', type="integer", group=directory) def getContext(self, directory): return self.get('Context', group=directory) def getType(self, directory): value = self.get('Type', group=directory) if value: return value else: return "Threshold" def getMaxSize(self, directory): value = self.get('MaxSize', type="integer", group=directory) if value or value == 0: return value else: return self.getSize(directory) def getMinSize(self, directory): value = self.get('MinSize', type="integer", group=directory) if value or value == 0: return value else: return self.getSize(directory) def getThreshold(self, directory): value = self.get('Threshold', type="integer", group=directory) if value or value == 0: return value else: return 2 def getScale(self, directory): value = self.get('Scale', type="integer", group=directory) return value or 1 # validation stuff def checkExtras(self): # header if self.defaultGroup == "KDE Icon Theme": self.warnings.append('[KDE Icon Theme]-Header is deprecated') # file extension if self.fileExtension == ".theme": pass elif self.fileExtension == ".desktop": self.warnings.append('.desktop fileExtension is deprecated') else: self.warnings.append('Unknown File extension') # Check required keys # Name try: self.name = self.content[self.defaultGroup]["Name"] except KeyError: self.errors.append("Key 'Name' is missing") # Comment try: self.comment = self.content[self.defaultGroup]["Comment"] except KeyError: self.errors.append("Key 'Comment' is missing") # Directories try: self.directories = self.content[self.defaultGroup]["Directories"] except KeyError: self.errors.append("Key 'Directories' is missing") def checkGroup(self, group): # check if group header is valid if group == self.defaultGroup: try: self.name = self.content[group]["Name"] except KeyError: self.errors.append("Key 'Name' in Group '%s' is missing" % group) try: self.name = self.content[group]["Comment"] except KeyError: self.errors.append("Key 'Comment' in Group '%s' is missing" % group) elif group in self.getDirectories(): try: self.type = self.content[group]["Type"] except KeyError: self.type = "Threshold" try: self.name = self.content[group]["Size"] except KeyError: self.errors.append("Key 'Size' in Group '%s' is missing" % group) elif not (re.match(r"^\[X-", group) and is_ascii(group)): self.errors.append("Invalid Group name: %s" % group) def checkKey(self, key, value, group): # standard keys if group == self.defaultGroup: if re.match("^Name"+xdg.Locale.regex+"$", key): pass elif re.match("^Comment"+xdg.Locale.regex+"$", key): pass elif key == "Inherits": self.checkValue(key, value, list=True) elif key == "Directories": self.checkValue(key, value, list=True) elif key == "ScaledDirectories": self.checkValue(key, value, list=True) elif key == "Hidden": self.checkValue(key, value, type="boolean") elif key == "Example": self.checkValue(key, value) elif re.match("^X-[a-zA-Z0-9-]+", key): pass else: self.errors.append("Invalid key: %s" % key) elif group in self.getDirectories(): if key == "Size": self.checkValue(key, value, type="integer") elif key == "Context": self.checkValue(key, value) elif key == "Type": self.checkValue(key, value) if value not in ["Fixed", "Scalable", "Threshold"]: self.errors.append("Key 'Type' must be one out of 'Fixed','Scalable','Threshold', but is %s" % value) elif key == "MaxSize": self.checkValue(key, value, type="integer") if self.type != "Scalable": self.errors.append("Key 'MaxSize' give, but Type is %s" % self.type) elif key == "MinSize": self.checkValue(key, value, type="integer") if self.type != "Scalable": self.errors.append("Key 'MinSize' give, but Type is %s" % self.type) elif key == "Threshold": self.checkValue(key, value, type="integer") if self.type != "Threshold": self.errors.append("Key 'Threshold' give, but Type is %s" % self.type) elif key == "Scale": self.checkValue(key, value, type="integer") elif re.match("^X-[a-zA-Z0-9-]+", key): pass else: self.errors.append("Invalid key: %s" % key) class IconData(IniFile): "Class to parse and validate IconData Files" def __init__(self): IniFile.__init__(self) def __repr__(self): displayname = self.getDisplayName() if displayname: return "" % displayname else: return "" def parse(self, file): IniFile.parse(self, file, ["Icon Data"]) # Standard Keys def getDisplayName(self): """Retrieve the display name from the icon data, if one is specified.""" return self.get('DisplayName', locale=True) def getEmbeddedTextRectangle(self): """Retrieve the embedded text rectangle from the icon data as a list of numbers (x0, y0, x1, y1), if it is specified.""" return self.get('EmbeddedTextRectangle', type="integer", list=True) def getAttachPoints(self): """Retrieve the anchor points for overlays & emblems from the icon data, as a list of co-ordinate pairs, if they are specified.""" return self.get('AttachPoints', type="point", list=True) # validation stuff def checkExtras(self): # file extension if self.fileExtension != ".icon": self.warnings.append('Unknown File extension') def checkGroup(self, group): # check if group header is valid if not (group == self.defaultGroup \ or (re.match(r"^\[X-", group) and is_ascii(group))): self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace")) def checkKey(self, key, value, group): # standard keys if re.match("^DisplayName"+xdg.Locale.regex+"$", key): pass elif key == "EmbeddedTextRectangle": self.checkValue(key, value, type="integer", list=True) elif key == "AttachPoints": self.checkValue(key, value, type="point", list=True) elif re.match("^X-[a-zA-Z0-9-]+", key): pass else: self.errors.append("Invalid key: %s" % key) icondirs = [] for basedir in xdg_data_dirs: icondirs.append(os.path.join(basedir, "icons")) icondirs.append(os.path.join(basedir, "pixmaps")) icondirs.append(os.path.expanduser("~/.icons")) # just cache variables, they give a 10x speed improvement themes = [] theme_cache = {} dir_cache = {} icon_cache = {} def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]): """Get the path to a specified icon. size : Icon size in pixels. Defaults to ``xdg.Config.icon_size``. theme : Icon theme name. Defaults to ``xdg.Config.icon_theme``. If the icon isn't found in the specified theme, it will be looked up in the basic 'hicolor' theme. extensions : List of preferred file extensions. Example:: >>> getIconPath("inkscape", 32) '/usr/share/icons/hicolor/32x32/apps/inkscape.png' """ global themes if size == None: size = xdg.Config.icon_size if theme == None: theme = xdg.Config.icon_theme # if we have an absolute path, just return it if os.path.isabs(iconname): return iconname # check if it has an extension and strip it if os.path.splitext(iconname)[1][1:] in extensions: iconname = os.path.splitext(iconname)[0] # parse theme files if (themes == []) or (themes[0].name != theme): themes = list(__get_themes(theme)) # more caching (icon looked up in the last 5 seconds?) tmp = (iconname, size, theme, tuple(extensions)) try: timestamp, icon = icon_cache[tmp] except KeyError: pass else: if (time.time() - timestamp) >= xdg.Config.cache_time: del icon_cache[tmp] else: return icon for thme in themes: icon = LookupIcon(iconname, size, thme, extensions) if icon: icon_cache[tmp] = (time.time(), icon) return icon # cache stuff again (directories looked up in the last 5 seconds?) for directory in icondirs: if (directory not in dir_cache \ or (int(time.time() - dir_cache[directory][1]) >= xdg.Config.cache_time \ and dir_cache[directory][2] < os.path.getmtime(directory))) \ and os.path.isdir(directory): dir_cache[directory] = (os.listdir(directory), time.time(), os.path.getmtime(directory)) for dir, values in dir_cache.items(): for extension in extensions: try: if iconname + "." + extension in values[0]: icon = os.path.join(dir, iconname + "." + extension) icon_cache[tmp] = [time.time(), icon] return icon except UnicodeDecodeError as e: if debug: raise e else: pass # we haven't found anything? "hicolor" is our fallback if theme != "hicolor": icon = getIconPath(iconname, size, "hicolor") icon_cache[tmp] = [time.time(), icon] return icon def getIconData(path): """Retrieve the data from the .icon file corresponding to the given file. If there is no .icon file, it returns None. Example:: getIconData("/usr/share/icons/Tango/scalable/places/folder.svg") """ if os.path.isfile(path): icon_file = os.path.splitext(path)[0] + ".icon" if os.path.isfile(icon_file): data = IconData() data.parse(icon_file) return data def __get_themes(themename): """Generator yielding IconTheme objects for a specified theme and any themes from which it inherits. """ for dir in icondirs: theme_file = os.path.join(dir, themename, "index.theme") if os.path.isfile(theme_file): break theme_file = os.path.join(dir, themename, "index.desktop") if os.path.isfile(theme_file): break else: if debug: raise NoThemeError(themename) return theme = IconTheme() theme.parse(theme_file) yield theme for subtheme in theme.getInherits(): for t in __get_themes(subtheme): yield t def LookupIcon(iconname, size, theme, extensions): # look for the cache if theme.name not in theme_cache: theme_cache[theme.name] = [] theme_cache[theme.name].append(time.time() - (xdg.Config.cache_time + 1)) # [0] last time of lookup theme_cache[theme.name].append(0) # [1] mtime theme_cache[theme.name].append(dict()) # [2] dir: [subdir, [items]] # cache stuff (directory lookuped up the in the last 5 seconds?) if int(time.time() - theme_cache[theme.name][0]) >= xdg.Config.cache_time: theme_cache[theme.name][0] = time.time() for subdir in theme.getDirectories(): for directory in icondirs: dir = os.path.join(directory,theme.name,subdir) if (dir not in theme_cache[theme.name][2] \ or theme_cache[theme.name][1] < os.path.getmtime(os.path.join(directory,theme.name))) \ and subdir != "" \ and os.path.isdir(dir): theme_cache[theme.name][2][dir] = [subdir, os.listdir(dir)] theme_cache[theme.name][1] = os.path.getmtime(os.path.join(directory,theme.name)) for dir, values in theme_cache[theme.name][2].items(): if DirectoryMatchesSize(values[0], size, theme): for extension in extensions: if iconname + "." + extension in values[1]: return os.path.join(dir, iconname + "." + extension) minimal_size = 2**31 closest_filename = "" for dir, values in theme_cache[theme.name][2].items(): distance = DirectorySizeDistance(values[0], size, theme) if distance < minimal_size: for extension in extensions: if iconname + "." + extension in values[1]: closest_filename = os.path.join(dir, iconname + "." + extension) minimal_size = distance return closest_filename def DirectoryMatchesSize(subdir, iconsize, theme): Type = theme.getType(subdir) Size = theme.getSize(subdir) Threshold = theme.getThreshold(subdir) MinSize = theme.getMinSize(subdir) MaxSize = theme.getMaxSize(subdir) if Type == "Fixed": return Size == iconsize elif Type == "Scaleable": return MinSize <= iconsize <= MaxSize elif Type == "Threshold": return Size - Threshold <= iconsize <= Size + Threshold def DirectorySizeDistance(subdir, iconsize, theme): Type = theme.getType(subdir) Size = theme.getSize(subdir) Threshold = theme.getThreshold(subdir) MinSize = theme.getMinSize(subdir) MaxSize = theme.getMaxSize(subdir) if Type == "Fixed": return abs(Size - iconsize) elif Type == "Scalable": if iconsize < MinSize: return MinSize - iconsize elif iconsize > MaxSize: return MaxSize - iconsize return 0 elif Type == "Threshold": if iconsize < Size - Threshold: return MinSize - iconsize elif iconsize > Size + Threshold: return iconsize - MaxSize return 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603024935.0 pyxdg-0.27/xdg/IniFile.py0000664000175000017500000003153400000000000017357 0ustar00takluyvertakluyver00000000000000""" Base Class for DesktopEntry, IconTheme and IconData """ import re, os, stat, io from xdg.Exceptions import (ParsingError, DuplicateGroupError, NoGroupError, NoKeyError, DuplicateKeyError, ValidationError, debug) import xdg.Locale from xdg.util import u def is_ascii(s): """Return True if a string consists entirely of ASCII characters.""" try: s.encode('ascii', 'strict') return True except UnicodeError: return False class IniFile: defaultGroup = '' fileExtension = '' filename = '' tainted = False def __init__(self, filename=None): self.content = dict() if filename: self.parse(filename) def __cmp__(self, other): return cmp(self.content, other.content) def parse(self, filename, headers=None): '''Parse an INI file. headers -- list of headers the parser will try to select as a default header ''' # for performance reasons content = self.content if not os.path.isfile(filename): raise ParsingError("File not found", filename) try: # The content should be UTF-8, but legacy files can have other # encodings, including mixed encodings in one file. We don't attempt # to decode them, but we silence the errors. fd = io.open(filename, 'r', encoding='utf-8', errors='replace') except IOError as e: if debug: raise e else: return # parse file with fd: for line in fd: line = line.strip() # empty line if not line: continue # comment elif line[0] == '#': continue # new group elif line[0] == '[': currentGroup = line.lstrip("[").rstrip("]") if debug and self.hasGroup(currentGroup): raise DuplicateGroupError(currentGroup, filename) else: content[currentGroup] = {} # key else: try: key, value = line.split("=", 1) except ValueError: raise ParsingError("Invalid line: " + line, filename) key = key.strip() # Spaces before/after '=' should be ignored try: if debug and self.hasKey(key, currentGroup): raise DuplicateKeyError(key, currentGroup, filename) else: content[currentGroup][key] = value.strip() except (IndexError, UnboundLocalError): raise ParsingError("Parsing error on key, group missing", filename) self.filename = filename self.tainted = False # check header if headers: for header in headers: if header in content: self.defaultGroup = header break else: raise ParsingError("[%s]-Header missing" % headers[0], filename) # start stuff to access the keys def get(self, key, group=None, locale=False, type="string", list=False, strict=False): # set default group if not group: group = self.defaultGroup # return key (with locale) if (group in self.content) and (key in self.content[group]): if locale: value = self.content[group][self.__addLocale(key, group)] else: value = self.content[group][key] else: if strict or debug: if group not in self.content: raise NoGroupError(group, self.filename) elif key not in self.content[group]: raise NoKeyError(key, group, self.filename) else: value = "" if list == True: values = self.getList(value) result = [] else: values = [value] for value in values: if type == "boolean": value = self.__getBoolean(value) elif type == "integer": try: value = int(value) except ValueError: value = 0 elif type == "numeric": try: value = float(value) except ValueError: value = 0.0 elif type == "regex": value = re.compile(value) elif type == "point": x, y = value.split(",") value = int(x), int(y) if list == True: result.append(value) else: result = value return result # end stuff to access the keys # start subget def getList(self, string): if re.search(r"(? 0: key = key + "[" + xdg.Locale.langs[0] + "]" try: self.content[group][key] = value except KeyError: raise NoGroupError(group, self.filename) self.tainted = (value == self.get(key, group)) def addGroup(self, group): if self.hasGroup(group): if debug: raise DuplicateGroupError(group, self.filename) else: self.content[group] = {} self.tainted = True def removeGroup(self, group): existed = group in self.content if existed: del self.content[group] self.tainted = True else: if debug: raise NoGroupError(group, self.filename) return existed def removeKey(self, key, group=None, locales=True): # set default group if not group: group = self.defaultGroup try: if locales: for name in list(self.content[group]): if re.match("^" + key + xdg.Locale.regex + "$", name) and name != key: del self.content[group][name] value = self.content[group].pop(key) self.tainted = True return value except KeyError as e: if debug: if e == group: raise NoGroupError(group, self.filename) else: raise NoKeyError(key, group, self.filename) else: return "" # misc def groups(self): return self.content.keys() def hasGroup(self, group): return group in self.content def hasKey(self, key, group=None): # set default group if not group: group = self.defaultGroup return key in self.content[group] def getFileName(self): return self.filename ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1517581257.0 pyxdg-0.27/xdg/Locale.py0000664000175000017500000000415700000000000017240 0ustar00takluyvertakluyver00000000000000""" Helper Module for Locale settings This module is based on a ROX module (LGPL): http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/i18n.py?rev=1.3&view=log """ import os from locale import normalize regex = r"(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z0-9-]+)?(@[a-zA-Z]+)?\])?" def _expand_lang(locale): locale = normalize(locale) COMPONENT_CODESET = 1 << 0 COMPONENT_MODIFIER = 1 << 1 COMPONENT_TERRITORY = 1 << 2 # split up the locale into its base components mask = 0 pos = locale.find('@') if pos >= 0: modifier = locale[pos:] locale = locale[:pos] mask |= COMPONENT_MODIFIER else: modifier = '' pos = locale.find('.') codeset = '' if pos >= 0: locale = locale[:pos] pos = locale.find('_') if pos >= 0: territory = locale[pos:] locale = locale[:pos] mask |= COMPONENT_TERRITORY else: territory = '' language = locale ret = [] for i in range(mask+1): if not (i & ~mask): # if all components for this combo exist ... val = language if i & COMPONENT_TERRITORY: val += territory if i & COMPONENT_CODESET: val += codeset if i & COMPONENT_MODIFIER: val += modifier ret.append(val) ret.reverse() return ret def expand_languages(languages=None): # Get some reasonable defaults for arguments that were not supplied if languages is None: languages = [] for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'): val = os.environ.get(envar) if val: languages = val.split(':') break #if 'C' not in languages: # languages.append('C') # now normalize and expand the languages nelangs = [] for lang in languages: for nelang in _expand_lang(lang): if nelang not in nelangs: nelangs.append(nelang) return nelangs def update(language=None): global langs if language: langs = expand_languages([language]) else: langs = expand_languages() langs = [] update() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603032264.0 pyxdg-0.27/xdg/Menu.py0000664000175000017500000011421300000000000016740 0ustar00takluyvertakluyver00000000000000""" Implementation of the XDG Menu Specification http://standards.freedesktop.org/menu-spec/ Example code: from xdg.Menu import parse, Menu, MenuEntry def print_menu(menu, tab=0): for submenu in menu.Entries: if isinstance(submenu, Menu): print ("\t" * tab) + unicode(submenu) print_menu(submenu, tab+1) elif isinstance(submenu, MenuEntry): print ("\t" * tab) + unicode(submenu.DesktopEntry) print_menu(parse()) """ import os import locale import subprocess import ast import sys try: import xml.etree.cElementTree as etree except ImportError: import xml.etree.ElementTree as etree from xdg.BaseDirectory import xdg_data_dirs, xdg_config_dirs from xdg.DesktopEntry import DesktopEntry from xdg.Exceptions import ParsingError from xdg.util import PY3 import xdg.Locale import xdg.Config def _ast_const(name): if sys.version_info >= (3, 4): name = ast.literal_eval(name) if sys.version_info >= (3, 8): return ast.Constant(name) else: return ast.NameConstant(name) else: return ast.Name(id=name, ctx=ast.Load()) def _strxfrm(s): """Wrapper around locale.strxfrm that accepts unicode strings on Python 2. See Python bug #2481. """ if (not PY3) and isinstance(s, unicode): s = s.encode('utf-8') return locale.strxfrm(s) DELETED = "Deleted" NO_DISPLAY = "NoDisplay" HIDDEN = "Hidden" EMPTY = "Empty" NOT_SHOW_IN = "NotShowIn" NO_EXEC = "NoExec" class Menu: """Menu containing sub menus under menu.Entries Contains both Menu and MenuEntry items. """ def __init__(self): # Public stuff self.Name = "" self.Directory = None self.Entries = [] self.Doc = "" self.Filename = "" self.Depth = 0 self.Parent = None self.NotInXml = False # Can be True, False, DELETED, NO_DISPLAY, HIDDEN, EMPTY or NOT_SHOW_IN self.Show = True self.Visible = 0 # Private stuff, only needed for parsing self.AppDirs = [] self.DefaultLayout = None self.Deleted = None self.Directories = [] self.DirectoryDirs = [] self.Layout = None self.MenuEntries = [] self.Moves = [] self.OnlyUnallocated = None self.Rules = [] self.Submenus = [] def __str__(self): return self.Name def __add__(self, other): for dir in other.AppDirs: self.AppDirs.append(dir) for dir in other.DirectoryDirs: self.DirectoryDirs.append(dir) for directory in other.Directories: self.Directories.append(directory) if other.Deleted is not None: self.Deleted = other.Deleted if other.OnlyUnallocated is not None: self.OnlyUnallocated = other.OnlyUnallocated if other.Layout: self.Layout = other.Layout if other.DefaultLayout: self.DefaultLayout = other.DefaultLayout for rule in other.Rules: self.Rules.append(rule) for move in other.Moves: self.Moves.append(move) for submenu in other.Submenus: self.addSubmenu(submenu) return self # FIXME: Performance: cache getName() def __cmp__(self, other): return locale.strcoll(self.getName(), other.getName()) def _key(self): """Key function for locale-aware sorting.""" return _strxfrm(self.getName()) def __lt__(self, other): try: other = other._key() except AttributeError: pass return self._key() < other def __eq__(self, other): try: return self.Name == unicode(other) except NameError: # unicode() becomes str() in Python 3 return self.Name == str(other) """ PUBLIC STUFF """ def getEntries(self, show_hidden=False): """Interator for a list of Entries visible to the user.""" for entry in self.Entries: if show_hidden: yield entry elif entry.Show is True: yield entry # FIXME: Add searchEntry/seaqrchMenu function # search for name/comment/genericname/desktopfileid # return multiple items def getMenuEntry(self, desktopfileid, deep=False): """Searches for a MenuEntry with a given DesktopFileID.""" for menuentry in self.MenuEntries: if menuentry.DesktopFileID == desktopfileid: return menuentry if deep: for submenu in self.Submenus: submenu.getMenuEntry(desktopfileid, deep) def getMenu(self, path): """Searches for a Menu with a given path.""" array = path.split("/", 1) for submenu in self.Submenus: if submenu.Name == array[0]: if len(array) > 1: return submenu.getMenu(array[1]) else: return submenu def getPath(self, org=False, toplevel=False): """Returns this menu's path in the menu structure.""" parent = self names = [] while 1: if org: names.append(parent.Name) else: names.append(parent.getName()) if parent.Depth > 0: parent = parent.Parent else: break names.reverse() path = "" if not toplevel: names.pop(0) for name in names: path = os.path.join(path, name) return path def getName(self): """Returns the menu's localised name.""" try: return self.Directory.DesktopEntry.getName() except AttributeError: return self.Name def getGenericName(self): """Returns the menu's generic name.""" try: return self.Directory.DesktopEntry.getGenericName() except AttributeError: return "" def getComment(self): """Returns the menu's comment text.""" try: return self.Directory.DesktopEntry.getComment() except AttributeError: return "" def getIcon(self): """Returns the menu's icon, filename or simple name""" try: return self.Directory.DesktopEntry.getIcon() except AttributeError: return "" def sort(self): self.Entries = [] self.Visible = 0 for submenu in self.Submenus: submenu.sort() _submenus = set() _entries = set() for order in self.Layout.order: if order[0] == "Filename": _entries.add(order[1]) elif order[0] == "Menuname": _submenus.add(order[1]) for order in self.Layout.order: if order[0] == "Separator": separator = Separator(self) if len(self.Entries) > 0 and isinstance(self.Entries[-1], Separator): separator.Show = False self.Entries.append(separator) elif order[0] == "Filename": menuentry = self.getMenuEntry(order[1]) if menuentry: self.Entries.append(menuentry) elif order[0] == "Menuname": submenu = self.getMenu(order[1]) if submenu: if submenu.Layout.inline: self.merge_inline(submenu) else: self.Entries.append(submenu) elif order[0] == "Merge": if order[1] == "files" or order[1] == "all": self.MenuEntries.sort() for menuentry in self.MenuEntries: if menuentry.DesktopFileID not in _entries: self.Entries.append(menuentry) elif order[1] == "menus" or order[1] == "all": self.Submenus.sort() for submenu in self.Submenus: if submenu.Name not in _submenus: if submenu.Layout.inline: self.merge_inline(submenu) else: self.Entries.append(submenu) # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec for entry in self.Entries: entry.Show = True self.Visible += 1 if isinstance(entry, Menu): if entry.Deleted is True: entry.Show = DELETED self.Visible -= 1 elif isinstance(entry.Directory, MenuEntry): if entry.Directory.DesktopEntry.getNoDisplay(): entry.Show = NO_DISPLAY self.Visible -= 1 elif entry.Directory.DesktopEntry.getHidden(): entry.Show = HIDDEN self.Visible -= 1 elif isinstance(entry, MenuEntry): if entry.DesktopEntry.getNoDisplay(): entry.Show = NO_DISPLAY self.Visible -= 1 elif entry.DesktopEntry.getHidden(): entry.Show = HIDDEN self.Visible -= 1 elif entry.DesktopEntry.getTryExec() and not entry.DesktopEntry.findTryExec(): entry.Show = NO_EXEC self.Visible -= 1 elif xdg.Config.windowmanager: if (entry.DesktopEntry.getOnlyShowIn() != [] and ( xdg.Config.windowmanager not in entry.DesktopEntry.getOnlyShowIn() ) ) or ( xdg.Config.windowmanager in entry.DesktopEntry.getNotShowIn() ): entry.Show = NOT_SHOW_IN self.Visible -= 1 elif isinstance(entry, Separator): self.Visible -= 1 # remove separators at the beginning and at the end if len(self.Entries) > 0: if isinstance(self.Entries[0], Separator): self.Entries[0].Show = False if len(self.Entries) > 1: if isinstance(self.Entries[-1], Separator): self.Entries[-1].Show = False # show_empty tag for entry in self.Entries[:]: if isinstance(entry, Menu) and not entry.Layout.show_empty and entry.Visible == 0: entry.Show = EMPTY self.Visible -= 1 if entry.NotInXml is True: self.Entries.remove(entry) """ PRIVATE STUFF """ def addSubmenu(self, newmenu): for submenu in self.Submenus: if submenu == newmenu: submenu += newmenu break else: self.Submenus.append(newmenu) newmenu.Parent = self newmenu.Depth = self.Depth + 1 # inline tags def merge_inline(self, submenu): """Appends a submenu's entries to this menu See the section of the spec about the "inline" attribute """ if len(submenu.Entries) == 1 and submenu.Layout.inline_alias: menuentry = submenu.Entries[0] menuentry.DesktopEntry.set("Name", submenu.getName(), locale=True) menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale=True) menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale=True) self.Entries.append(menuentry) elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0: if submenu.Layout.inline_header: header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment()) self.Entries.append(header) for entry in submenu.Entries: self.Entries.append(entry) else: self.Entries.append(submenu) class Move: "A move operation" def __init__(self, old="", new=""): self.Old = old self.New = new def __cmp__(self, other): return cmp(self.Old, other.Old) class Layout: "Menu Layout class" def __init__(self, show_empty=False, inline=False, inline_limit=4, inline_header=True, inline_alias=False): self.show_empty = show_empty self.inline = inline self.inline_limit = inline_limit self.inline_header = inline_header self.inline_alias = inline_alias self._order = [] self._default_order = [ ['Merge', 'menus'], ['Merge', 'files'] ] @property def order(self): return self._order if self._order else self._default_order @order.setter def order(self, order): self._order = order class Rule: """Include / Exclude Rules Class""" TYPE_INCLUDE, TYPE_EXCLUDE = 0, 1 @classmethod def fromFilename(cls, type, filename): tree = ast.Expression( body=ast.Compare( left=ast.Str(filename), ops=[ast.Eq()], comparators=[ast.Attribute( value=ast.Name(id='menuentry', ctx=ast.Load()), attr='DesktopFileID', ctx=ast.Load() )] ), lineno=1, col_offset=0 ) ast.fix_missing_locations(tree) rule = Rule(type, tree) return rule def __init__(self, type, expression): # Type is TYPE_INCLUDE or TYPE_EXCLUDE self.Type = type # expression is ast.Expression self.expression = expression self.code = compile(self.expression, '', 'eval') def __str__(self): return ast.dump(self.expression) def apply(self, menuentries, run): for menuentry in menuentries: if run == 2 and (menuentry.MatchedInclude is True or menuentry.Allocated is True): continue if eval(self.code): if self.Type is Rule.TYPE_INCLUDE: menuentry.Add = True menuentry.MatchedInclude = True else: menuentry.Add = False return menuentries class MenuEntry: "Wrapper for 'Menu Style' Desktop Entries" TYPE_USER = "User" TYPE_SYSTEM = "System" TYPE_BOTH = "Both" def __init__(self, filename, dir="", prefix=""): # Create entry self.DesktopEntry = DesktopEntry(os.path.join(dir, filename)) self.setAttributes(filename, dir, prefix) # Can True, False DELETED, HIDDEN, EMPTY, NOT_SHOW_IN or NO_EXEC self.Show = True # Semi-Private self.Original = None self.Parents = [] # Private Stuff self.Allocated = False self.Add = False self.MatchedInclude = False # Caching self.Categories = self.DesktopEntry.getCategories() def save(self): """Save any changes to the desktop entry.""" if self.DesktopEntry.tainted: self.DesktopEntry.write() def getDir(self): """Return the directory containing the desktop entry file.""" return self.DesktopEntry.filename.replace(self.Filename, '') def getType(self): """Return the type of MenuEntry, System/User/Both""" if not xdg.Config.root_mode: if self.Original: return self.TYPE_BOTH elif xdg_data_dirs[0] in self.DesktopEntry.filename: return self.TYPE_USER else: return self.TYPE_SYSTEM else: return self.TYPE_USER def setAttributes(self, filename, dir="", prefix=""): self.Filename = filename self.Prefix = prefix self.DesktopFileID = os.path.join(prefix, filename).replace("/", "-") if not os.path.isabs(self.DesktopEntry.filename): self.__setFilename() def updateAttributes(self): if self.getType() == self.TYPE_SYSTEM: self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix) self.__setFilename() def __setFilename(self): if not xdg.Config.root_mode: path = xdg_data_dirs[0] else: path = xdg_data_dirs[1] if self.DesktopEntry.getType() == "Application": dir_ = os.path.join(path, "applications") else: dir_ = os.path.join(path, "desktop-directories") self.DesktopEntry.filename = os.path.join(dir_, self.Filename) def __cmp__(self, other): return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName()) def _key(self): """Key function for locale-aware sorting.""" return _strxfrm(self.DesktopEntry.getName()) def __lt__(self, other): try: other = other._key() except AttributeError: pass return self._key() < other def __eq__(self, other): if self.DesktopFileID == str(other): return True else: return False def __repr__(self): return self.DesktopFileID class Separator: "Just a dummy class for Separators" def __init__(self, parent): self.Parent = parent self.Show = True class Header: "Class for Inline Headers" def __init__(self, name, generic_name, comment): self.Name = name self.GenericName = generic_name self.Comment = comment def __str__(self): return self.Name TYPE_DIR, TYPE_FILE = 0, 1 def _check_file_path(value, filename, type): path = os.path.dirname(filename) if not os.path.isabs(value): value = os.path.join(path, value) value = os.path.abspath(value) if not os.path.exists(value): return False if type == TYPE_DIR and os.path.isdir(value): return value if type == TYPE_FILE and os.path.isfile(value): return value return False def _get_menu_file_path(filename): dirs = list(xdg_config_dirs) if xdg.Config.root_mode is True: dirs.pop(0) for d in dirs: menuname = os.path.join(d, "menus", filename) if os.path.isfile(menuname): return menuname def _to_bool(value): if isinstance(value, bool): return value return value.lower() == "true" # remove duplicate entries from a list def _dedupe(_list): _set = {} _list.reverse() _list = [_set.setdefault(e, e) for e in _list if e not in _set] _list.reverse() return _list class XMLMenuBuilder(object): def __init__(self, debug=False): self.debug = debug def parse(self, filename=None): """Load an applications.menu file. filename : str, optional The default is ``$XDG_CONFIG_DIRS/menus/${XDG_MENU_PREFIX}applications.menu``. """ # convert to absolute path if filename and not os.path.isabs(filename): filename = _get_menu_file_path(filename) # use default if no filename given if not filename: candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu" filename = _get_menu_file_path(candidate) if not filename: raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate) # check if it is a .menu file if not filename.endswith(".menu"): raise ParsingError('Not a .menu file', filename) # create xml parser try: tree = etree.parse(filename) except: raise ParsingError('Not a valid .menu file', filename) # parse menufile self._merged_files = set() self._directory_dirs = set() self.cache = MenuEntryCache() menu = self.parse_menu(tree.getroot(), filename) menu.tree = tree menu.filename = filename self.handle_moves(menu) self.post_parse(menu) # generate the menu self.generate_not_only_allocated(menu) self.generate_only_allocated(menu) # and finally sort menu.sort() return menu def parse_menu(self, node, filename): menu = Menu() self.parse_node(node, filename, menu) return menu def parse_node(self, node, filename, parent=None): num_children = len(node) for child in node: tag, text = child.tag, child.text text = text.strip() if text else None if tag == 'Menu': menu = self.parse_menu(child, filename) parent.addSubmenu(menu) elif tag == 'AppDir' and text: self.parse_app_dir(text, filename, parent) elif tag == 'DefaultAppDirs': self.parse_default_app_dir(filename, parent) elif tag == 'DirectoryDir' and text: self.parse_directory_dir(text, filename, parent) elif tag == 'DefaultDirectoryDirs': self.parse_default_directory_dir(filename, parent) elif tag == 'Name' and text: parent.Name = text elif tag == 'Directory' and text: parent.Directories.append(text) elif tag == 'OnlyUnallocated': parent.OnlyUnallocated = True elif tag == 'NotOnlyUnallocated': parent.OnlyUnallocated = False elif tag == 'Deleted': parent.Deleted = True elif tag == 'NotDeleted': parent.Deleted = False elif tag == 'Include' or tag == 'Exclude': parent.Rules.append(self.parse_rule(child)) elif tag == 'MergeFile': if child.attrib.get("type", None) == "parent": self.parse_merge_file("applications.menu", child, filename, parent) elif text: self.parse_merge_file(text, child, filename, parent) elif tag == 'MergeDir' and text: self.parse_merge_dir(text, child, filename, parent) elif tag == 'DefaultMergeDirs': self.parse_default_merge_dirs(child, filename, parent) elif tag == 'Move': parent.Moves.append(self.parse_move(child)) elif tag == 'Layout': if num_children > 1: parent.Layout = self.parse_layout(child) elif tag == 'DefaultLayout': if num_children > 1: parent.DefaultLayout = self.parse_layout(child) elif tag == 'LegacyDir' and text: self.parse_legacy_dir(text, child.attrib.get("prefix", ""), filename, parent) elif tag == 'KDELegacyDirs': self.parse_kde_legacy_dirs(filename, parent) def parse_layout(self, node): layout = Layout( show_empty=_to_bool(node.attrib.get("show_empty", False)), inline=_to_bool(node.attrib.get("inline", False)), inline_limit=int(node.attrib.get("inline_limit", 4)), inline_header=_to_bool(node.attrib.get("inline_header", True)), inline_alias=_to_bool(node.attrib.get("inline_alias", False)) ) order = [] for child in node: tag, text = child.tag, child.text text = text.strip() if text else None if tag == "Menuname" and text: order.append([ "Menuname", text, _to_bool(child.attrib.get("show_empty", False)), _to_bool(child.attrib.get("inline", False)), int(child.attrib.get("inline_limit", 4)), _to_bool(child.attrib.get("inline_header", True)), _to_bool(child.attrib.get("inline_alias", False)) ]) elif tag == "Separator": order.append(['Separator']) elif tag == "Filename" and text: order.append(["Filename", text]) elif tag == "Merge": order.append([ "Merge", child.attrib.get("type", "all") ]) layout.order = order return layout def parse_move(self, node): old, new = "", "" for child in node: tag, text = child.tag, child.text text = text.strip() if text else None if tag == "Old" and text: old = text elif tag == "New" and text: new = text return Move(old, new) # ---------- parsing def parse_rule(self, node): type = Rule.TYPE_INCLUDE if node.tag == 'Include' else Rule.TYPE_EXCLUDE tree = ast.Expression(lineno=1, col_offset=0) expr = self.parse_bool_op(node, ast.Or()) if expr: tree.body = expr else: tree.body = _ast_const('False') ast.fix_missing_locations(tree) return Rule(type, tree) def parse_bool_op(self, node, operator): values = [] for child in node: rule = self.parse_rule_node(child) if rule: values.append(rule) num_values = len(values) if num_values > 1: return ast.BoolOp(operator, values) elif num_values == 1: return values[0] return None def parse_rule_node(self, node): tag = node.tag if tag == 'Or': return self.parse_bool_op(node, ast.Or()) elif tag == 'And': return self.parse_bool_op(node, ast.And()) elif tag == 'Not': expr = self.parse_bool_op(node, ast.Or()) return ast.UnaryOp(ast.Not(), expr) if expr else None elif tag == 'All': return _ast_const('True') elif tag == 'Category': category = node.text return ast.Compare( left=ast.Str(category), ops=[ast.In()], comparators=[ast.Attribute( value=ast.Name(id='menuentry', ctx=ast.Load()), attr='Categories', ctx=ast.Load() )] ) elif tag == 'Filename': filename = node.text return ast.Compare( left=ast.Str(filename), ops=[ast.Eq()], comparators=[ast.Attribute( value=ast.Name(id='menuentry', ctx=ast.Load()), attr='DesktopFileID', ctx=ast.Load() )] ) # ---------- App/Directory Dir Stuff def parse_app_dir(self, value, filename, parent): value = _check_file_path(value, filename, TYPE_DIR) if value: parent.AppDirs.append(value) def parse_default_app_dir(self, filename, parent): for d in reversed(xdg_data_dirs): self.parse_app_dir(os.path.join(d, "applications"), filename, parent) def parse_directory_dir(self, value, filename, parent): value = _check_file_path(value, filename, TYPE_DIR) if value: parent.DirectoryDirs.append(value) def parse_default_directory_dir(self, filename, parent): for d in reversed(xdg_data_dirs): self.parse_directory_dir(os.path.join(d, "desktop-directories"), filename, parent) # ---------- Merge Stuff def parse_merge_file(self, value, child, filename, parent): if child.attrib.get("type", None) == "parent": for d in xdg_config_dirs: rel_file = filename.replace(d, "").strip("/") if rel_file != filename: for p in xdg_config_dirs: if d == p: continue if os.path.isfile(os.path.join(p, rel_file)): self.merge_file(os.path.join(p, rel_file), child, parent) break else: value = _check_file_path(value, filename, TYPE_FILE) if value: self.merge_file(value, child, parent) def parse_merge_dir(self, value, child, filename, parent): value = _check_file_path(value, filename, TYPE_DIR) if value: for item in os.listdir(value): try: if item.endswith(".menu"): self.merge_file(os.path.join(value, item), child, parent) except UnicodeDecodeError: continue def parse_default_merge_dirs(self, child, filename, parent): basename = os.path.splitext(os.path.basename(filename))[0] for d in reversed(xdg_config_dirs): self.parse_merge_dir(os.path.join(d, "menus", basename + "-merged"), child, filename, parent) def merge_file(self, filename, child, parent): # check for infinite loops if filename in self._merged_files: if self.debug: raise ParsingError('Infinite MergeFile loop detected', filename) else: return self._merged_files.add(filename) # load file try: tree = etree.parse(filename) except IOError: if self.debug: raise ParsingError('File not found', filename) else: return except: if self.debug: raise ParsingError('Not a valid .menu file', filename) else: return root = tree.getroot() self.parse_node(root, filename, parent) # ---------- Legacy Dir Stuff def parse_legacy_dir(self, dir_, prefix, filename, parent): m = self.merge_legacy_dir(dir_, prefix, filename, parent) if m: parent += m def merge_legacy_dir(self, dir_, prefix, filename, parent): dir_ = _check_file_path(dir_, filename, TYPE_DIR) if dir_ and dir_ not in self._directory_dirs: self._directory_dirs.add(dir_) m = Menu() m.AppDirs.append(dir_) m.DirectoryDirs.append(dir_) m.Name = os.path.basename(dir_) m.NotInXml = True for item in os.listdir(dir_): try: if item == ".directory": m.Directories.append(item) elif os.path.isdir(os.path.join(dir_, item)): m.addSubmenu(self.merge_legacy_dir( os.path.join(dir_, item), prefix, filename, parent )) except UnicodeDecodeError: continue self.cache.add_menu_entries([dir_], prefix, True) menuentries = self.cache.get_menu_entries([dir_], False) for menuentry in menuentries: categories = menuentry.Categories if len(categories) == 0: r = Rule.fromFilename(Rule.TYPE_INCLUDE, menuentry.DesktopFileID) m.Rules.append(r) if not dir_ in parent.AppDirs: categories.append("Legacy") menuentry.Categories = categories return m def parse_kde_legacy_dirs(self, filename, parent): try: proc = subprocess.Popen( ['kde-config', '--path', 'apps'], stdout=subprocess.PIPE, universal_newlines=True ) output = proc.communicate()[0].splitlines() except OSError: # If kde-config doesn't exist, ignore this. return try: for dir_ in output[0].split(":"): self.parse_legacy_dir(dir_, "kde", filename, parent) except IndexError: pass def post_parse(self, menu): # unallocated / deleted if menu.Deleted is None: menu.Deleted = False if menu.OnlyUnallocated is None: menu.OnlyUnallocated = False # Layout Tags if not menu.Layout or not menu.DefaultLayout: if menu.DefaultLayout: menu.Layout = menu.DefaultLayout elif menu.Layout: if menu.Depth > 0: menu.DefaultLayout = menu.Parent.DefaultLayout else: menu.DefaultLayout = Layout() else: if menu.Depth > 0: menu.Layout = menu.Parent.DefaultLayout menu.DefaultLayout = menu.Parent.DefaultLayout else: menu.Layout = Layout() menu.DefaultLayout = Layout() # add parent's app/directory dirs if menu.Depth > 0: menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs # remove duplicates menu.Directories = _dedupe(menu.Directories) menu.DirectoryDirs = _dedupe(menu.DirectoryDirs) menu.AppDirs = _dedupe(menu.AppDirs) # go recursive through all menus for submenu in menu.Submenus: self.post_parse(submenu) # reverse so handling is easier menu.Directories.reverse() menu.DirectoryDirs.reverse() menu.AppDirs.reverse() # get the valid .directory file out of the list for directory in menu.Directories: for dir in menu.DirectoryDirs: if os.path.isfile(os.path.join(dir, directory)): menuentry = MenuEntry(directory, dir) if not menu.Directory: menu.Directory = menuentry elif menuentry.getType() == MenuEntry.TYPE_SYSTEM: if menu.Directory.getType() == MenuEntry.TYPE_USER: menu.Directory.Original = menuentry if menu.Directory: break # Finally generate the menu def generate_not_only_allocated(self, menu): for submenu in menu.Submenus: self.generate_not_only_allocated(submenu) if menu.OnlyUnallocated is False: self.cache.add_menu_entries(menu.AppDirs) menuentries = [] for rule in menu.Rules: menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 1) for menuentry in menuentries: if menuentry.Add is True: menuentry.Parents.append(menu) menuentry.Add = False menuentry.Allocated = True menu.MenuEntries.append(menuentry) def generate_only_allocated(self, menu): for submenu in menu.Submenus: self.generate_only_allocated(submenu) if menu.OnlyUnallocated is True: self.cache.add_menu_entries(menu.AppDirs) menuentries = [] for rule in menu.Rules: menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 2) for menuentry in menuentries: if menuentry.Add is True: menuentry.Parents.append(menu) # menuentry.Add = False # menuentry.Allocated = True menu.MenuEntries.append(menuentry) def handle_moves(self, menu): for submenu in menu.Submenus: self.handle_moves(submenu) # parse move operations for move in menu.Moves: move_from_menu = menu.getMenu(move.Old) if move_from_menu: # FIXME: this is assigned, but never used... move_to_menu = menu.getMenu(move.New) menus = move.New.split("/") oldparent = None while len(menus) > 0: if not oldparent: oldparent = menu newmenu = oldparent.getMenu(menus[0]) if not newmenu: newmenu = Menu() newmenu.Name = menus[0] if len(menus) > 1: newmenu.NotInXml = True oldparent.addSubmenu(newmenu) oldparent = newmenu menus.pop(0) newmenu += move_from_menu move_from_menu.Parent.Submenus.remove(move_from_menu) class MenuEntryCache: "Class to cache Desktop Entries" def __init__(self): self.cacheEntries = {} self.cacheEntries['legacy'] = [] self.cache = {} def add_menu_entries(self, dirs, prefix="", legacy=False): for dir_ in dirs: if not dir_ in self.cacheEntries: self.cacheEntries[dir_] = [] self.__addFiles(dir_, "", prefix, legacy) def __addFiles(self, dir_, subdir, prefix, legacy): for item in os.listdir(os.path.join(dir_, subdir)): if item.endswith(".desktop"): try: menuentry = MenuEntry(os.path.join(subdir, item), dir_, prefix) except ParsingError: continue self.cacheEntries[dir_].append(menuentry) if legacy: self.cacheEntries['legacy'].append(menuentry) elif os.path.isdir(os.path.join(dir_, subdir, item)) and not legacy: self.__addFiles(dir_, os.path.join(subdir, item), prefix, legacy) def get_menu_entries(self, dirs, legacy=True): entries = [] ids = set() # handle legacy items appdirs = dirs[:] if legacy: appdirs.append("legacy") # cache the results again key = "".join(appdirs) try: return self.cache[key] except KeyError: pass for dir_ in appdirs: for menuentry in self.cacheEntries[dir_]: try: if menuentry.DesktopFileID not in ids: ids.add(menuentry.DesktopFileID) entries.append(menuentry) elif menuentry.getType() == MenuEntry.TYPE_SYSTEM: # FIXME: This is only 99% correct, but still... idx = entries.index(menuentry) entry = entries[idx] if entry.getType() == MenuEntry.TYPE_USER: entry.Original = menuentry except UnicodeDecodeError: continue self.cache[key] = entries return entries def parse(filename=None, debug=False): """Helper function. Equivalent to calling xdg.Menu.XMLMenuBuilder().parse(filename) """ return XMLMenuBuilder(debug).parse(filename) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603026208.0 pyxdg-0.27/xdg/MenuEditor.py0000664000175000017500000004475500000000000020124 0ustar00takluyvertakluyver00000000000000""" CLass to edit XDG Menus """ import os try: import xml.etree.cElementTree as etree except ImportError: import xml.etree.ElementTree as etree from xdg.Menu import Menu, MenuEntry, Layout, Separator, XMLMenuBuilder from xdg.BaseDirectory import xdg_config_dirs, xdg_data_dirs from xdg.Exceptions import ParsingError from xdg.Config import setRootMode # XML-Cleanups: Move / Exclude # FIXME: proper reverte/delete # FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions # FIXME: catch Exceptions # FIXME: copy functions # FIXME: More Layout stuff # FIXME: unod/redo function / remove menu... # FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile # Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs class MenuEditor(object): def __init__(self, menu=None, filename=None, root=False): self.menu = None self.filename = None self.tree = None self.parser = XMLMenuBuilder() self.parse(menu, filename, root) # fix for creating two menus with the same name on the fly self.filenames = [] def parse(self, menu=None, filename=None, root=False): if root: setRootMode(True) if isinstance(menu, Menu): self.menu = menu elif menu: self.menu = self.parser.parse(menu) else: self.menu = self.parser.parse() if root: self.filename = self.menu.Filename elif filename: self.filename = filename else: self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1]) try: self.tree = etree.parse(self.filename) except IOError: root = etree.fromstring(""" Applications %s """ % self.menu.Filename) self.tree = etree.ElementTree(root) except ParsingError: raise ParsingError('Not a valid .menu file', self.filename) #FIXME: is this needed with etree ? self.__remove_whitespace_nodes(self.tree) def save(self): self.__saveEntries(self.menu) self.__saveMenu() def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None): menuentry = MenuEntry(self.__getFileName(name, ".desktop")) menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal) self.__addEntry(parent, menuentry, after, before) self.menu.sort() return menuentry def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None): menu = Menu() menu.Parent = parent menu.Depth = parent.Depth + 1 menu.Layout = parent.DefaultLayout menu.DefaultLayout = parent.DefaultLayout menu = self.editMenu(menu, name, genericname, comment, icon) self.__addEntry(parent, menu, after, before) self.menu.sort() return menu def createSeparator(self, parent, after=None, before=None): separator = Separator(parent) self.__addEntry(parent, separator, after, before) self.menu.sort() return separator def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): self.__deleteEntry(oldparent, menuentry, after, before) self.__addEntry(newparent, menuentry, after, before) self.menu.sort() return menuentry def moveMenu(self, menu, oldparent, newparent, after=None, before=None): self.__deleteEntry(oldparent, menu, after, before) self.__addEntry(newparent, menu, after, before) root_menu = self.__getXmlMenu(self.menu.Name) if oldparent.getPath(True) != newparent.getPath(True): self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name)) self.menu.sort() return menu def moveSeparator(self, separator, parent, after=None, before=None): self.__deleteEntry(parent, separator, after, before) self.__addEntry(parent, separator, after, before) self.menu.sort() return separator def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): self.__addEntry(newparent, menuentry, after, before) self.menu.sort() return menuentry def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None): deskentry = menuentry.DesktopEntry if name: if not deskentry.hasKey("Name"): deskentry.set("Name", name) deskentry.set("Name", name, locale=True) if comment: if not deskentry.hasKey("Comment"): deskentry.set("Comment", comment) deskentry.set("Comment", comment, locale=True) if genericname: if not deskentry.hasKey("GenericName"): deskentry.set("GenericName", genericname) deskentry.set("GenericName", genericname, locale=True) if command: deskentry.set("Exec", command) if icon: deskentry.set("Icon", icon) if terminal: deskentry.set("Terminal", "true") elif not terminal: deskentry.set("Terminal", "false") if nodisplay is True: deskentry.set("NoDisplay", "true") elif nodisplay is False: deskentry.set("NoDisplay", "false") if hidden is True: deskentry.set("Hidden", "true") elif hidden is False: deskentry.set("Hidden", "false") menuentry.updateAttributes() if len(menuentry.Parents) > 0: self.menu.sort() return menuentry def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None): # Hack for legacy dirs if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory": xml_menu = self.__getXmlMenu(menu.getPath(True, True)) self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory") menu.Directory.setAttributes(menu.Name + ".directory") # Hack for New Entries elif not isinstance(menu.Directory, MenuEntry): if not name: name = menu.Name filename = self.__getFileName(name, ".directory").replace("/", "") if not menu.Name: menu.Name = filename.replace(".directory", "") xml_menu = self.__getXmlMenu(menu.getPath(True, True)) self.__addXmlTextElement(xml_menu, 'Directory', filename) menu.Directory = MenuEntry(filename) deskentry = menu.Directory.DesktopEntry if name: if not deskentry.hasKey("Name"): deskentry.set("Name", name) deskentry.set("Name", name, locale=True) if genericname: if not deskentry.hasKey("GenericName"): deskentry.set("GenericName", genericname) deskentry.set("GenericName", genericname, locale=True) if comment: if not deskentry.hasKey("Comment"): deskentry.set("Comment", comment) deskentry.set("Comment", comment, locale=True) if icon: deskentry.set("Icon", icon) if nodisplay is True: deskentry.set("NoDisplay", "true") elif nodisplay is False: deskentry.set("NoDisplay", "false") if hidden is True: deskentry.set("Hidden", "true") elif hidden is False: deskentry.set("Hidden", "false") menu.Directory.updateAttributes() if isinstance(menu.Parent, Menu): self.menu.sort() return menu def hideMenuEntry(self, menuentry): self.editMenuEntry(menuentry, nodisplay=True) def unhideMenuEntry(self, menuentry): self.editMenuEntry(menuentry, nodisplay=False, hidden=False) def hideMenu(self, menu): self.editMenu(menu, nodisplay=True) def unhideMenu(self, menu): self.editMenu(menu, nodisplay=False, hidden=False) xml_menu = self.__getXmlMenu(menu.getPath(True, True), False) deleted = xml_menu.findall('Deleted') not_deleted = xml_menu.findall('NotDeleted') for node in deleted + not_deleted: xml_menu.remove(node) def deleteMenuEntry(self, menuentry): if self.getAction(menuentry) == "delete": self.__deleteFile(menuentry.DesktopEntry.filename) for parent in menuentry.Parents: self.__deleteEntry(parent, menuentry) self.menu.sort() return menuentry def revertMenuEntry(self, menuentry): if self.getAction(menuentry) == "revert": self.__deleteFile(menuentry.DesktopEntry.filename) menuentry.Original.Parents = [] for parent in menuentry.Parents: index = parent.Entries.index(menuentry) parent.Entries[index] = menuentry.Original index = parent.MenuEntries.index(menuentry) parent.MenuEntries[index] = menuentry.Original menuentry.Original.Parents.append(parent) self.menu.sort() return menuentry def deleteMenu(self, menu): if self.getAction(menu) == "delete": self.__deleteFile(menu.Directory.DesktopEntry.filename) self.__deleteEntry(menu.Parent, menu) xml_menu = self.__getXmlMenu(menu.getPath(True, True)) parent = self.__get_parent_node(xml_menu) parent.remove(xml_menu) self.menu.sort() return menu def revertMenu(self, menu): if self.getAction(menu) == "revert": self.__deleteFile(menu.Directory.DesktopEntry.filename) menu.Directory = menu.Directory.Original self.menu.sort() return menu def deleteSeparator(self, separator): self.__deleteEntry(separator.Parent, separator, after=True) self.menu.sort() return separator """ Private Stuff """ def getAction(self, entry): if isinstance(entry, Menu): if not isinstance(entry.Directory, MenuEntry): return "none" elif entry.Directory.getType() == "Both": return "revert" elif entry.Directory.getType() == "User" and ( len(entry.Submenus) + len(entry.MenuEntries) ) == 0: return "delete" elif isinstance(entry, MenuEntry): if entry.getType() == "Both": return "revert" elif entry.getType() == "User": return "delete" else: return "none" return "none" def __saveEntries(self, menu): if not menu: menu = self.menu if isinstance(menu.Directory, MenuEntry): menu.Directory.save() for entry in menu.getEntries(hidden=True): if isinstance(entry, MenuEntry): entry.save() elif isinstance(entry, Menu): self.__saveEntries(entry) def __saveMenu(self): if not os.path.isdir(os.path.dirname(self.filename)): os.makedirs(os.path.dirname(self.filename)) self.tree.write(self.filename, encoding='utf-8') def __getFileName(self, name, extension): postfix = 0 while 1: if postfix == 0: filename = name + extension else: filename = name + "-" + str(postfix) + extension if extension == ".desktop": dir = "applications" elif extension == ".directory": dir = "desktop-directories" if not filename in self.filenames and not os.path.isfile( os.path.join(xdg_data_dirs[0], dir, filename) ): self.filenames.append(filename) break else: postfix += 1 return filename def __getXmlMenu(self, path, create=True, element=None): # FIXME: we should also return the menu's parent, # to avoid looking for it later on # @see Element.getiterator() if not element: element = self.tree if "/" in path: (name, path) = path.split("/", 1) else: name = path path = "" found = None for node in element.findall("Menu"): name_node = node.find('Name') if name_node.text == name: if path: found = self.__getXmlMenu(path, create, node) else: found = node if found: break if not found and create: node = self.__addXmlMenuElement(element, name) if path: found = self.__getXmlMenu(path, create, node) else: found = node return found def __addXmlMenuElement(self, element, name): menu_node = etree.SubElement('Menu', element) name_node = etree.SubElement('Name', menu_node) name_node.text = name return menu_node def __addXmlTextElement(self, element, name, text): node = etree.SubElement(name, element) node.text = text return node def __addXmlFilename(self, element, filename, type_="Include"): # remove old filenames includes = element.findall('Include') excludes = element.findall('Exclude') rules = includes + excludes for rule in rules: #FIXME: this finds only Rules whose FIRST child is a Filename element if rule[0].tag == "Filename" and rule[0].text == filename: element.remove(rule) # shouldn't it remove all occurences, like the following: #filename_nodes = rule.findall('.//Filename'): #for fn in filename_nodes: #if fn.text == filename: ##element.remove(rule) #parent = self.__get_parent_node(fn) #parent.remove(fn) # add new filename node = etree.SubElement(type_, element) self.__addXmlTextElement(node, 'Filename', filename) return node def __addXmlMove(self, element, old, new): node = etree.SubElement("Move", element) self.__addXmlTextElement(node, 'Old', old) self.__addXmlTextElement(node, 'New', new) return node def __addXmlLayout(self, element, layout): # remove old layout for node in element.findall("Layout"): element.remove(node) # add new layout node = etree.SubElement("Layout", element) for order in layout.order: if order[0] == "Separator": child = etree.SubElement("Separator", node) elif order[0] == "Filename": child = self.__addXmlTextElement(node, "Filename", order[1]) elif order[0] == "Menuname": child = self.__addXmlTextElement(node, "Menuname", order[1]) elif order[0] == "Merge": child = etree.SubElement("Merge", node) child.attrib["type"] = order[1] return node def __addLayout(self, parent): layout = Layout() layout.order = [] layout.show_empty = parent.Layout.show_empty layout.inline = parent.Layout.inline layout.inline_header = parent.Layout.inline_header layout.inline_alias = parent.Layout.inline_alias layout.inline_limit = parent.Layout.inline_limit layout.order.append(["Merge", "menus"]) for entry in parent.Entries: if isinstance(entry, Menu): layout.parseMenuname(entry.Name) elif isinstance(entry, MenuEntry): layout.parseFilename(entry.DesktopFileID) elif isinstance(entry, Separator): layout.parseSeparator() layout.order.append(["Merge", "files"]) parent.Layout = layout return layout def __addEntry(self, parent, entry, after=None, before=None): if after or before: if after: index = parent.Entries.index(after) + 1 elif before: index = parent.Entries.index(before) parent.Entries.insert(index, entry) else: parent.Entries.append(entry) xml_parent = self.__getXmlMenu(parent.getPath(True, True)) if isinstance(entry, MenuEntry): parent.MenuEntries.append(entry) entry.Parents.append(parent) self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include") elif isinstance(entry, Menu): parent.addSubmenu(entry) if after or before: self.__addLayout(parent) self.__addXmlLayout(xml_parent, parent.Layout) def __deleteEntry(self, parent, entry, after=None, before=None): parent.Entries.remove(entry) xml_parent = self.__getXmlMenu(parent.getPath(True, True)) if isinstance(entry, MenuEntry): entry.Parents.remove(parent) parent.MenuEntries.remove(entry) self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude") elif isinstance(entry, Menu): parent.Submenus.remove(entry) if after or before: self.__addLayout(parent) self.__addXmlLayout(xml_parent, parent.Layout) def __deleteFile(self, filename): try: os.remove(filename) except OSError: pass try: self.filenames.remove(filename) except ValueError: pass def __remove_whitespace_nodes(self, node): for child in node: text = child.text.strip() if not text: child.text = '' tail = child.tail.strip() if not tail: child.tail = '' if len(child): self.__remove_whilespace_nodes(child) def __get_parent_node(self, node): # elements in ElementTree doesn't hold a reference to their parent for parent, child in self.__iter_parent(): if child is node: return child def __iter_parent(self): for parent in self.tree.getiterator(): for child in parent: yield parent, child ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603024935.0 pyxdg-0.27/xdg/Mime.py0000664000175000017500000006320000000000000016722 0ustar00takluyvertakluyver00000000000000""" This module is based on a rox module (LGPL): http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log This module provides access to the shared MIME database. types is a dictionary of all known MIME types, indexed by the type name, e.g. types['application/x-python'] Applications can install information about MIME types by storing an XML file as /packages/.xml and running the update-mime-database command, which is provided by the freedesktop.org shared mime database package. See http://www.freedesktop.org/standards/shared-mime-info-spec/ for information about the format of these files. (based on version 0.13) """ import os import re import stat import sys import fnmatch from xdg import BaseDirectory import xdg.Locale from xml.dom import minidom, XML_NAMESPACE from collections import defaultdict FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info' types = {} # Maps MIME names to type objects exts = None # Maps extensions to types globs = None # List of (glob, type) pairs literals = None # Maps liternal names to types magic = None PY3 = (sys.version_info[0] >= 3) def _get_node_data(node): """Get text of XML node""" return ''.join([n.nodeValue for n in node.childNodes]).strip() def lookup(media, subtype = None): """Get the MIMEtype object for the given type. This remains for backwards compatibility; calling MIMEtype now does the same thing. The name can either be passed as one part ('text/plain'), or as two ('text', 'plain'). """ return MIMEtype(media, subtype) class MIMEtype(object): """Class holding data about a MIME type. Calling the class will return a cached instance, so there is only one instance for each MIME type. The name can either be passed as one part ('text/plain'), or as two ('text', 'plain'). """ def __new__(cls, media, subtype=None): if subtype is None and '/' in media: media, subtype = media.split('/', 1) assert '/' not in subtype media = media.lower() subtype = subtype.lower() try: return types[(media, subtype)] except KeyError: mtype = super(MIMEtype, cls).__new__(cls) mtype._init(media, subtype) types[(media, subtype)] = mtype return mtype # If this is done in __init__, it is automatically called again each time # the MIMEtype is returned by __new__, which we don't want. So we call it # explicitly only when we construct a new instance. def _init(self, media, subtype): self.media = media self.subtype = subtype self._comment = None def _load(self): "Loads comment for current language. Use get_comment() instead." resource = os.path.join('mime', self.media, self.subtype + '.xml') for path in BaseDirectory.load_data_paths(resource): doc = minidom.parse(path) if doc is None: continue for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'): lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en' goodness = 1 + (lang in xdg.Locale.langs) if goodness > self._comment[0]: self._comment = (goodness, _get_node_data(comment)) if goodness == 2: return # FIXME: add get_icon method def get_comment(self): """Returns comment for current language, loading it if needed.""" # Should we ever reload? if self._comment is None: self._comment = (0, str(self)) self._load() return self._comment[1] def canonical(self): """Returns the canonical MimeType object if this is an alias.""" update_cache() s = str(self) if s in aliases: return lookup(aliases[s]) return self def inherits_from(self): """Returns a set of Mime types which this inherits from.""" update_cache() return set(lookup(t) for t in inheritance[str(self)]) def __str__(self): return self.media + '/' + self.subtype def __repr__(self): return 'MIMEtype(%r, %r)' % (self.media, self.subtype) def __hash__(self): return hash(self.media) ^ hash(self.subtype) class UnknownMagicRuleFormat(ValueError): pass class DiscardMagicRules(Exception): "Raised when __NOMAGIC__ is found, and caught to discard previous rules." pass class MagicRule: also = None def __init__(self, start, value, mask, word, range): self.start = start self.value = value self.mask = mask self.word = word self.range = range rule_ending_re = re.compile(br'(?:~(\d+))?(?:\+(\d+))?\n$') @classmethod def from_file(cls, f): """Read a rule from the binary magics file. Returns a 2-tuple of the nesting depth and the MagicRule.""" line = f.readline() #print line # [indent] '>' nest_depth, line = line.split(b'>', 1) nest_depth = int(nest_depth) if nest_depth else 0 # start-offset '=' start, line = line.split(b'=', 1) start = int(start) if line == b'__NOMAGIC__\n': raise DiscardMagicRules # value length (2 bytes, big endian) if sys.version_info[0] >= 3: lenvalue = int.from_bytes(line[:2], byteorder='big') else: lenvalue = (ord(line[0])<<8)+ord(line[1]) line = line[2:] # value # This can contain newlines, so we may need to read more lines while len(line) <= lenvalue: line += f.readline() value, line = line[:lenvalue], line[lenvalue:] # ['&' mask] if line.startswith(b'&'): # This can contain newlines, so we may need to read more lines while len(line) <= lenvalue: line += f.readline() mask, line = line[1:lenvalue+1], line[lenvalue+1:] else: mask = None # ['~' word-size] ['+' range-length] ending = cls.rule_ending_re.match(line) if not ending: # Per the spec, this will be caught and ignored, to allow # for future extensions. raise UnknownMagicRuleFormat(repr(line)) word, range = ending.groups() word = int(word) if (word is not None) else 1 range = int(range) if (range is not None) else 1 return nest_depth, cls(start, value, mask, word, range) def maxlen(self): l = self.start + len(self.value) + self.range if self.also: return max(l, self.also.maxlen()) return l def match(self, buffer): if self.match0(buffer): if self.also: return self.also.match(buffer) return True def match0(self, buffer): l=len(buffer) lenvalue = len(self.value) for o in range(self.range): s=self.start+o e=s+lenvalue if l [(priority, rule), ...] def merge_file(self, fname): """Read a magic binary file, and add its rules to this MagicDB.""" with open(fname, 'rb') as f: line = f.readline() if line != b'MIME-Magic\0\n': raise IOError('Not a MIME magic file') while True: shead = f.readline().decode('ascii') #print(shead) if not shead: break if shead[0] != '[' or shead[-2:] != ']\n': raise ValueError('Malformed section heading', shead) pri, tname = shead[1:-2].split(':') #print shead[1:-2] pri = int(pri) mtype = lookup(tname) try: rule = MagicMatchAny.from_file(f) except DiscardMagicRules: self.bytype.pop(mtype, None) rule = MagicMatchAny.from_file(f) if rule is None: continue #print rule self.bytype[mtype].append((pri, rule)) def finalise(self): """Prepare the MagicDB for matching. This should be called after all rules have been merged into it. """ maxlen = 0 self.alltypes = [] # (priority, mimetype, rule) for mtype, rules in self.bytype.items(): for pri, rule in rules: self.alltypes.append((pri, mtype, rule)) maxlen = max(maxlen, rule.maxlen()) self.maxlen = maxlen # Number of bytes to read from files self.alltypes.sort(key=lambda x: x[0], reverse=True) def match_data(self, data, max_pri=100, min_pri=0, possible=None): """Do magic sniffing on some bytes. max_pri & min_pri can be used to specify the maximum & minimum priority rules to look for. possible can be a list of mimetypes to check, or None (the default) to check all mimetypes until one matches. Returns the MIMEtype found, or None if no entries match. """ if possible is not None: types = [] for mt in possible: for pri, rule in self.bytype[mt]: types.append((pri, mt, rule)) types.sort(key=lambda x: x[0]) else: types = self.alltypes for priority, mimetype, rule in types: #print priority, max_pri, min_pri if priority > max_pri: continue if priority < min_pri: break if rule.match(data): return mimetype def match(self, path, max_pri=100, min_pri=0, possible=None): """Read data from the file and do magic sniffing on it. max_pri & min_pri can be used to specify the maximum & minimum priority rules to look for. possible can be a list of mimetypes to check, or None (the default) to check all mimetypes until one matches. Returns the MIMEtype found, or None if no entries match. Raises IOError if the file can't be opened. """ with open(path, 'rb') as f: buf = f.read(self.maxlen) return self.match_data(buf, max_pri, min_pri, possible) def __repr__(self): return '' % len(self.alltypes) class GlobDB(object): def __init__(self): """Prepare the GlobDB. It can't actually be used until .finalise() is called, but merge_file() can be used to add data before that. """ # Maps mimetype to {(weight, glob, flags), ...} self.allglobs = defaultdict(set) def merge_file(self, path): """Loads name matching information from a globs2 file."""# allglobs = self.allglobs with open(path) as f: for line in f: if line.startswith('#'): continue # Comment fields = line[:-1].split(':') weight, type_name, pattern = fields[:3] weight = int(weight) mtype = lookup(type_name) if len(fields) > 3: flags = fields[3].split(',') else: flags = () if pattern == '__NOGLOBS__': # This signals to discard any previous globs allglobs.pop(mtype, None) continue allglobs[mtype].add((weight, pattern, tuple(flags))) def finalise(self): """Prepare the GlobDB for matching. This should be called after all files have been merged into it. """ self.exts = defaultdict(list) # Maps extensions to [(type, weight),...] self.cased_exts = defaultdict(list) self.globs = [] # List of (regex, type, weight) triplets self.literals = {} # Maps literal names to (type, weight) self.cased_literals = {} for mtype, globs in self.allglobs.items(): mtype = mtype.canonical() for weight, pattern, flags in globs: cased = 'cs' in flags if pattern.startswith('*.'): # *.foo -- extension pattern rest = pattern[2:] if not ('*' in rest or '[' in rest or '?' in rest): if cased: self.cased_exts[rest].append((mtype, weight)) else: self.exts[rest.lower()].append((mtype, weight)) continue if ('*' in pattern or '[' in pattern or '?' in pattern): # Translate the glob pattern to a regex & compile it re_flags = 0 if cased else re.I pattern = re.compile(fnmatch.translate(pattern), flags=re_flags) self.globs.append((pattern, mtype, weight)) else: # No wildcards - literal pattern if cased: self.cased_literals[pattern] = (mtype, weight) else: self.literals[pattern.lower()] = (mtype, weight) # Sort globs by weight & length self.globs.sort(reverse=True, key=lambda x: (x[2], len(x[0].pattern)) ) def first_match(self, path): """Return the first match found for a given path, or None if no match is found.""" try: return next(self._match_path(path))[0] except StopIteration: return None def all_matches(self, path): """Return a list of (MIMEtype, glob weight) pairs for the path.""" return list(self._match_path(path)) def _match_path(self, path): """Yields pairs of (mimetype, glob weight).""" leaf = os.path.basename(path) # Literals (no wildcards) if leaf in self.cased_literals: yield self.cased_literals[leaf] lleaf = leaf.lower() if lleaf in self.literals: yield self.literals[lleaf] # Extensions ext = leaf while 1: p = ext.find('.') if p < 0: break ext = ext[p + 1:] if ext in self.cased_exts: for res in self.cased_exts[ext]: yield res ext = lleaf while 1: p = ext.find('.') if p < 0: break ext = ext[p+1:] if ext in self.exts: for res in self.exts[ext]: yield res # Other globs for (regex, mime_type, weight) in self.globs: if regex.match(leaf): yield (mime_type, weight) # Some well-known types text = lookup('text', 'plain') octet_stream = lookup('application', 'octet-stream') inode_block = lookup('inode', 'blockdevice') inode_char = lookup('inode', 'chardevice') inode_dir = lookup('inode', 'directory') inode_fifo = lookup('inode', 'fifo') inode_socket = lookup('inode', 'socket') inode_symlink = lookup('inode', 'symlink') inode_door = lookup('inode', 'door') app_exe = lookup('application', 'executable') _cache_uptodate = False def _cache_database(): global globs, magic, aliases, inheritance, _cache_uptodate _cache_uptodate = True aliases = {} # Maps alias Mime types to canonical names inheritance = defaultdict(set) # Maps to sets of parent mime types. # Load aliases for path in BaseDirectory.load_data_paths(os.path.join('mime', 'aliases')): with open(path, 'r') as f: for line in f: alias, canonical = line.strip().split(None, 1) aliases[alias] = canonical # Load filename patterns (globs) globs = GlobDB() for path in BaseDirectory.load_data_paths(os.path.join('mime', 'globs2')): globs.merge_file(path) globs.finalise() # Load magic sniffing data magic = MagicDB() for path in BaseDirectory.load_data_paths(os.path.join('mime', 'magic')): magic.merge_file(path) magic.finalise() # Load subclasses for path in BaseDirectory.load_data_paths(os.path.join('mime', 'subclasses')): with open(path, 'r') as f: for line in f: sub, parent = line.strip().split(None, 1) inheritance[sub].add(parent) def update_cache(): if not _cache_uptodate: _cache_database() def get_type_by_name(path): """Returns type of file by its name, or None if not known""" update_cache() return globs.first_match(path) def get_type_by_contents(path, max_pri=100, min_pri=0): """Returns type of file by its contents, or None if not known""" update_cache() return magic.match(path, max_pri, min_pri) def get_type_by_data(data, max_pri=100, min_pri=0): """Returns type of the data, which should be bytes.""" update_cache() return magic.match_data(data, max_pri, min_pri) def _get_type_by_stat(st_mode): """Match special filesystem objects to Mimetypes.""" if stat.S_ISDIR(st_mode): return inode_dir elif stat.S_ISCHR(st_mode): return inode_char elif stat.S_ISBLK(st_mode): return inode_block elif stat.S_ISFIFO(st_mode): return inode_fifo elif stat.S_ISLNK(st_mode): return inode_symlink elif stat.S_ISSOCK(st_mode): return inode_socket return inode_door def get_type(path, follow=True, name_pri=100): """Returns type of file indicated by path. This function is *deprecated* - :func:`get_type2` is more accurate. :param path: pathname to check (need not exist) :param follow: when reading file, follow symbolic links :param name_pri: Priority to do name matches. 100=override magic This tries to use the contents of the file, and falls back to the name. It can also handle special filesystem objects like directories and sockets. """ update_cache() try: if follow: st = os.stat(path) else: st = os.lstat(path) except: t = get_type_by_name(path) return t or text if stat.S_ISREG(st.st_mode): # Regular file t = get_type_by_contents(path, min_pri=name_pri) if not t: t = get_type_by_name(path) if not t: t = get_type_by_contents(path, max_pri=name_pri) if t is None: if stat.S_IMODE(st.st_mode) & 0o111: return app_exe else: return text return t else: return _get_type_by_stat(st.st_mode) def get_type2(path, follow=True): """Find the MIMEtype of a file using the XDG recommended checking order. This first checks the filename, then uses file contents if the name doesn't give an unambiguous MIMEtype. It can also handle special filesystem objects like directories and sockets. :param path: file path to examine (need not exist) :param follow: whether to follow symlinks :rtype: :class:`MIMEtype` .. versionadded:: 1.0 """ update_cache() try: st = os.stat(path) if follow else os.lstat(path) except OSError: return get_type_by_name(path) or octet_stream if not stat.S_ISREG(st.st_mode): # Special filesystem objects return _get_type_by_stat(st.st_mode) mtypes = sorted(globs.all_matches(path), key=(lambda x: x[1]), reverse=True) if mtypes: max_weight = mtypes[0][1] i = 1 for mt, w in mtypes[1:]: if w < max_weight: break i += 1 mtypes = mtypes[:i] if len(mtypes) == 1: return mtypes[0][0] possible = [mt for mt,w in mtypes] else: possible = None # Try all magic matches try: t = magic.match(path, possible=possible) except IOError: t = None if t: return t elif mtypes: return mtypes[0][0] elif stat.S_IMODE(st.st_mode) & 0o111: return app_exe else: return text if is_text_file(path) else octet_stream def is_text_file(path): """Guess whether a file contains text or binary data. Heuristic: binary if the first 32 bytes include ASCII control characters. This rule may change in future versions. .. versionadded:: 1.0 """ try: f = open(path, 'rb') except IOError: return False with f: return _is_text(f.read(32)) if PY3: def _is_text(data): return not any(b <= 0x8 or 0xe <= b < 0x20 or b == 0x7f for b in data) else: def _is_text(data): return not any(b <= '\x08' or '\x0e' <= b < '\x20' or b == '\x7f' \ for b in data) _mime2ext_cache = None _mime2ext_cache_uptodate = False def get_extensions(mimetype): """Retrieve the set of filename extensions matching a given MIMEtype. Extensions are returned without a leading dot, e.g. 'py'. If no extensions are registered for the MIMEtype, returns an empty set. The extensions are stored in a cache the first time this is called. .. versionadded:: 1.0 """ global _mime2ext_cache, _mime2ext_cache_uptodate update_cache() if not _mime2ext_cache_uptodate: _mime2ext_cache = defaultdict(set) for ext, mtypes in globs.exts.items(): for mtype, prio in mtypes: _mime2ext_cache[mtype].add(ext) _mime2ext_cache_uptodate = True return _mime2ext_cache[mimetype] def install_mime_info(application, package_file): """Copy 'package_file' as ``~/.local/share/mime/packages/.xml.`` If package_file is None, install ``/.xml``. If already installed, does nothing. May overwrite an existing file with the same name (if the contents are different)""" application += '.xml' with open(package_file) as f: new_data = f.read() # See if the file is already installed package_dir = os.path.join('mime', 'packages') resource = os.path.join(package_dir, application) for x in BaseDirectory.load_data_paths(resource): try: with open(x) as f: old_data = f.read() except: continue if old_data == new_data: return # Already installed global _cache_uptodate _cache_uptodate = False # Not already installed; add a new copy # Create the directory structure... new_file = os.path.join(BaseDirectory.save_data_path(package_dir), application) # Write the file... with open(new_file, 'w') as f: f.write(new_data) # Update the database... command = 'update-mime-database' if os.spawnlp(os.P_WAIT, command, command, BaseDirectory.save_data_path('mime')): os.unlink(new_file) raise Exception("The '%s' command returned an error code!\n" \ "Make sure you have the freedesktop.org shared MIME package:\n" \ "http://standards.freedesktop.org/shared-mime-info/" % command) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603024935.0 pyxdg-0.27/xdg/RecentFiles.py0000664000175000017500000001405200000000000020237 0ustar00takluyvertakluyver00000000000000""" Implementation of the XDG Recent File Storage Specification http://standards.freedesktop.org/recent-file-spec """ import xml.dom.minidom, xml.sax.saxutils import os, time, fcntl from xdg.Exceptions import ParsingError class RecentFiles: def __init__(self): self.RecentFiles = [] self.filename = "" def parse(self, filename=None): """Parse a list of recently used files. filename defaults to ``~/.recently-used``. """ if not filename: filename = os.path.join(os.getenv("HOME"), ".recently-used") try: doc = xml.dom.minidom.parse(filename) except IOError: raise ParsingError('File not found', filename) except xml.parsers.expat.ExpatError: raise ParsingError('Not a valid .menu file', filename) self.filename = filename for child in doc.childNodes: if child.nodeType == xml.dom.Node.ELEMENT_NODE: if child.tagName == "RecentFiles": for recent in child.childNodes: if recent.nodeType == xml.dom.Node.ELEMENT_NODE: if recent.tagName == "RecentItem": self.__parseRecentItem(recent) self.sort() def __parseRecentItem(self, item): recent = RecentFile() self.RecentFiles.append(recent) for attribute in item.childNodes: if attribute.nodeType == xml.dom.Node.ELEMENT_NODE: if attribute.tagName == "URI": recent.URI = attribute.childNodes[0].nodeValue elif attribute.tagName == "Mime-Type": recent.MimeType = attribute.childNodes[0].nodeValue elif attribute.tagName == "Timestamp": recent.Timestamp = int(attribute.childNodes[0].nodeValue) elif attribute.tagName == "Private": recent.Prviate = True elif attribute.tagName == "Groups": for group in attribute.childNodes: if group.nodeType == xml.dom.Node.ELEMENT_NODE: if group.tagName == "Group": recent.Groups.append(group.childNodes[0].nodeValue) def write(self, filename=None): """Write the list of recently used files to disk. If the instance is already associated with a file, filename can be omitted to save it there again. """ if not filename and not self.filename: raise ParsingError('File not found', filename) elif not filename: filename = self.filename with open(filename, "w") as f: fcntl.lockf(f, fcntl.LOCK_EX) f.write('\n') f.write("\n") for r in self.RecentFiles: f.write(" \n") f.write(" %s\n" % xml.sax.saxutils.escape(r.URI)) f.write(" %s\n" % r.MimeType) f.write(" %s\n" % r.Timestamp) if r.Private == True: f.write(" \n") if len(r.Groups) > 0: f.write(" \n") for group in r.Groups: f.write(" %s\n" % group) f.write(" \n") f.write(" \n") f.write("\n") fcntl.lockf(f, fcntl.LOCK_UN) def getFiles(self, mimetypes=None, groups=None, limit=0): """Get a list of recently used files. The parameters can be used to filter by mime types, by group, or to limit the number of items returned. By default, the entire list is returned, except for items marked private. """ tmp = [] i = 0 for item in self.RecentFiles: if groups: for group in groups: if group in item.Groups: tmp.append(item) i += 1 elif mimetypes: for mimetype in mimetypes: if mimetype == item.MimeType: tmp.append(item) i += 1 else: if item.Private == False: tmp.append(item) i += 1 if limit != 0 and i == limit: break return tmp def addFile(self, item, mimetype, groups=None, private=False): """Add a recently used file. item should be the URI of the file, typically starting with ``file:///``. """ # check if entry already there if item in self.RecentFiles: index = self.RecentFiles.index(item) recent = self.RecentFiles[index] else: # delete if more then 500 files if len(self.RecentFiles) == 500: self.RecentFiles.pop() # add entry recent = RecentFile() self.RecentFiles.append(recent) recent.URI = item recent.MimeType = mimetype recent.Timestamp = int(time.time()) recent.Private = private if groups: recent.Groups = groups self.sort() def deleteFile(self, item): """Remove a recently used file, by URI, from the list. """ if item in self.RecentFiles: self.RecentFiles.remove(item) def sort(self): self.RecentFiles.sort() self.RecentFiles.reverse() class RecentFile: def __init__(self): self.URI = "" self.MimeType = "" self.Timestamp = "" self.Private = False self.Groups = [] def __cmp__(self, other): return cmp(self.Timestamp, other.Timestamp) def __lt__ (self, other): return self.Timestamp < other.Timestamp def __eq__(self, other): return self.URI == str(other) def __str__(self): return self.URI ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1603032497.0 pyxdg-0.27/xdg/__init__.py0000664000175000017500000000025300000000000017571 0ustar00takluyvertakluyver00000000000000__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ] __version__ = "0.27" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1479086820.0 pyxdg-0.27/xdg/util.py0000664000175000017500000000533300000000000017013 0ustar00takluyvertakluyver00000000000000import sys PY3 = sys.version_info[0] >= 3 if PY3: def u(s): return s else: # Unicode-like literals def u(s): return s.decode('utf-8') try: # which() is available from Python 3.3 from shutil import which except ImportError: import os # This is a copy of which() from Python 3.3 def which(cmd, mode=os.F_OK | os.X_OK, path=None): """Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such file. `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result of os.environ.get("PATH"), or can be overridden with a custom search path. """ # Check that a given file can be accessed with the correct mode. # Additionally check that `file` is not a directory, as on Windows # directories pass the os.access check. def _access_check(fn, mode): return (os.path.exists(fn) and os.access(fn, mode) and not os.path.isdir(fn)) # If we're given a path with a directory part, look it up directly rather # than referring to PATH directories. This includes checking relative to the # current directory, e.g. ./script if os.path.dirname(cmd): if _access_check(cmd, mode): return cmd return None path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep) if sys.platform == "win32": # The current directory takes precedence on Windows. if not os.curdir in path: path.insert(0, os.curdir) # PATHEXT is necessary to check on Windows. pathext = os.environ.get("PATHEXT", "").split(os.pathsep) # See if the given file matches any of the expected path extensions. # This will allow us to short circuit when given "python.exe". # If it does match, only test that one, otherwise we have to try # others. if any(cmd.lower().endswith(ext.lower()) for ext in pathext): files = [cmd] else: files = [cmd + ext for ext in pathext] else: # On other platforms you don't have things like PATHEXT to tell you # what file suffixes are executable, so just pass on cmd as-is. files = [cmd] seen = set() for dir in path: normdir = os.path.normcase(dir) if not normdir in seen: seen.add(normdir) for thefile in files: name = os.path.join(dir, thefile) if _access_check(name, mode): return name return None