pax_global_header00006660000000000000000000000064121400236030014501gustar00rootroot0000000000000052 comment=d3546ba3f72c23614154d70259e01f3bb4967d7c gpyconf-0.2/000077500000000000000000000000001214002360300130075ustar00rootroot00000000000000gpyconf-0.2/.gitignore000066400000000000000000000000221214002360300147710ustar00rootroot00000000000000*.pyc *.xml *.ini gpyconf-0.2/CONTRIBUTORS000066400000000000000000000002601214002360300146650ustar00rootroot00000000000000Kristoffer Kleine Maintainer (2013-) Sebastian Billaudelle : contrib/gtk (2010) new version of GTK frontend (2010) gpyconf-0.2/LICENSE000066400000000000000000000645531214002360300140310ustar00rootroot00000000000000 Copyright (c) 2008-2012 Jonas Haag . 2013 Kristoffer Kleine All rights reserved. License: 2-clause-BSD and LGPL http://github.com/kkris/gpyconf gpyconf is dual licensed under the LGPL (GNU Lesser General Public License) and a slightly modified 2-clause BSD (Berkley Software Distribution) license. You may use gpyconf as if it was licensed under one of the above but not with both licenses at once. If you chose the LGPL license, redistributions and redistributions of redistributions of gpyconf have to fulfill the LGPL conditions. If you chose the BSD-like license, redistributions and redistributions of redistributions have to fulfill the BSD-like conditions. Both the LGPL and the modified 2-clause BSD license follow. The 2-clause Berkley Software Distribution license ================================================== Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. [It follows the part I added] * Redistributions of source code that include documentation must retain the above copyright and an acknowledgement similar to "This software uses gpyconf by Jonas Haag (http://gpyconf.lophus.org)" in the documentation. You're free to change the wording if it doesn't change the meaning of that sentence. [End of the part I added] THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The GNU Lesser General Public License ===================================== Version 2.1, February 1999 Copyright (C) 1991, 1999 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 Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] 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 Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, 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 and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these 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 other code 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. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. 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, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser 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 combine 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) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) 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. d) 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. e) 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 materials to be 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 with 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 Lesser 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. gpyconf-0.2/TODO000066400000000000000000000004271214002360300135020ustar00rootroot00000000000000 Version 0.2 ------------- * Documentation for - DictField - ListField - GTK+ Frontend * Documentation: Overview on which backend can handle which types correctly * Testsuite for backends - including unicode/utf8 tests Version 0.3 ------------- * i18n support gpyconf-0.2/docs/000077500000000000000000000000001214002360300137375ustar00rootroot00000000000000gpyconf-0.2/docs/Makefile000066400000000000000000000056421214002360300154060ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf build/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) build/html @echo @echo "Build finished. The HTML pages are in build/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) build/dirhtml @echo @echo "Build finished. The HTML pages are in build/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) build/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in build/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) build/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in build/qthelp, like this:" @echo "# qcollectiongenerator build/qthelp/gpyconf.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile build/qthelp/gpyconf.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex @echo @echo "Build finished; the LaTeX files are in build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes @echo @echo "The overview file is in build/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in build/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) build/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in build/doctest/output.txt." gpyconf-0.2/docs/source/000077500000000000000000000000001214002360300152375ustar00rootroot00000000000000gpyconf-0.2/docs/source/_ext/000077500000000000000000000000001214002360300161765ustar00rootroot00000000000000gpyconf-0.2/docs/source/_ext/emitrole.py000066400000000000000000000001371214002360300203710ustar00rootroot00000000000000from docutils import nodes def setup(app): app.add_generic_role('signal', nodes.emphasis) gpyconf-0.2/docs/source/architecture.rst000066400000000000000000000121731214002360300204570ustar00rootroot00000000000000Model, View, Controller --- architecture theory =============================================== In it's basic architecture, gpyconf follows the MVC paradigm. This section explains the basic idea behind the Model, View, Controller pattern and points out in what way gpyconf follows that pattern. If you're familiar with the MVC idea, you can confidently skip the very next subsection. The MVC pattern --------------- The MVC pattern is commonly used architecture pattern for software. It separates business logic and database *Models* from the end-user *View* (like a :abbr:`GUI (Graphical User Interface)` application or a web interface). To connect those two components, a *Controller* is used. Take a web service like `Twitter `_ for example: The database containing tweets and user data records represents the *Model*, while Twitter's web site users use to communicate represents the *View*. The resulting advantages are great: Every component can be used independently. (Which is, basically, the idea behind `Modularity `_.) Basically, a MVC-based software runs through an operation sequence like this: 1. The user gives some input to the View/frontend (e.g., clicks on an :guilabel:`Add to shopping cart` button) 2. The frontend informs the Controller about that action (mostly this communication is handled using signals) 3. The controller decides what to do with that input 4. In this case, the Controller tells the Model/database backend about that purchase 5. The backend decides what to do with that information (in our case, it stores that information for further interactions) 6. (*Optional*) The Controller tells the frontend to update the presentation (e.g. because the total of sums of all offered products changed) 7. (*Optional*) The frontend updates the presentation with the information given by the Controller. 8. The frontend waits for further interactions by the user or signals by the Controller component (which restarts the cycle) At first sight this appears to be an gratuitous complication to have, but it makes sense if you're planning to replace one of the three components. For example, it is very common to replace the View component with another one or to have two or more frontends. Look at our Twitter example: If Twitter wouldn't follow the MVC pattern, no 3rd-party-clients (like all that iPhone and IM-like software) would exist, because Twitter couldn't offer an :abbr:`API (Application Programming Interface)` (which is, strictly speaking, a View, too). You can see that writing your software based on the MVC pattern is a nice thing to have, but let's face the facts: It's a huge effort for programmers to implement the Model, View and Controller components completely independently (think about all the abstraction layers, the communication and all that stuff). Therefore, the very vast majority of (modern) software declared as MVC software does not *strictly* follow the MVC pattern. And so gpyconf does. gpyconf's architecture ---------------------- As told before, gpyconf (more or less) follows the MVC pattern. It's architecture is split in Model (your configuration option definition), backend (which stores the options, e.g. into a file or in a database), frontend (interaction with your users, typically using a configuration dialog) and Controller (which connects the mentioned components and presents the interface to you, the application developer). As communication between those components, signals are used (see the :doc:`signals` section for more about this). The following digraph illustrates this architecture. .. graphviz:: digraph { rankdir=LR; User -> Frontend [color="#DCA133", label="interacts with"]; Frontend -> Controller [color="#1AD41A", dir=both]; Controller -> Backend [color="#1AD41A", dir=both]; Developer -> Model [color="#8B6914", label="defines"]; Model -> Controller -> Backend [color="#8B6914"]; Model[shape=plaintext]; User[shape=rect]; Developer[shape=rect]; Frontend[shape=circle]; Backend[shape=circle]; Controller[shape=doublecircle] } In words: You -- the developer -- define your configuration model (using :doc:`fields `). Every configuration option represents a primitive (:class:`bool`, :class:`string`, :class:`int` etc) or non-primitive (:class:`list`, table) datatype. The :class:`gpyconf.Configuration` class passes this model definition the backend and the frontend. The backend reads stored values from it's storage (file, database, ...) and passes them to the Controller class. The Controller class updates the field's values with that new value. Then, if the frontend is runned (*Running the frontend* means, for example, building up the GUI and showing a configuration dialog window), and the user interacts with that frontend (e.g. changes a field's value or presses the :guilabel:`Save` button), the configuration class decides what to do next. Let's say, for example, the user pressed the :guilabel:`Save and close` button. The Controller class should now make the backend save the field's values and then "shut down" the frontend. gpyconf-0.2/docs/source/conf.py000066400000000000000000000147251214002360300165470ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # gpyconf documentation build configuration file, created by # sphinx-quickstart on Wed Aug 19 22:09:56 2009. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os sys.path.append(os.path.abspath('_ext')) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.graphviz', 'emitrole'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'gpyconf' copyright = u'2008-2010 Jonas Haag' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.2' # The full version, including alpha/beta/rc tags. release = '0.2b' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'gpyconfdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'gpyconf.tex', u'gpyconf documentation', u'Jonas Haag', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'http://docs.python.org/': None} gpyconf-0.2/docs/source/fields.rst000066400000000000000000000020471214002360300172420ustar00rootroot00000000000000Configuration option Fields =========================== Fields are -- at least for developers that *use* gpyconf, not develop costum stuff for it -- the main thing you will work with. A field represents a configuration option. Every configuration option has at least * a type (representing a python datatype like :class:`str`, :class:`int`, :class:`list`) and * a name for internal use Furthermore, a field represents a frontend *widget* (:abbr:`GUI (Graphical User Interface)` representation of a field). To get more information on about how to use fields, please refer to the :doc:`quickstart` section or, for detailed instructions, see the :doc:`usage` manual. API documentation for the :class:`Field` class and a list of all fields follows. The :class:`Field` base class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: gpyconf.fields.base.Field :members: :exclude-members: get_value, set_value, validation_error, setfromconf, on_initialized Included fields ~~~~~~~~~~~~~~~ .. automodule:: gpyconf.fields.fields :members: gpyconf-0.2/docs/source/index.rst000066400000000000000000000026341214002360300171050ustar00rootroot00000000000000gpyconf documentation ===================== Table of Contents ----------------- .. toctree:: :maxdepth: 3 quickstart usage architecture mvc signals fields own stuff about gpyconf ------------- gpyconf is a configuration framework based on gtk. With gpyconf, you can have configuration handling for your Python application within seconds, including different ways to store that configuration options (:doc:`Backends `) and a pretty, auto-generated (of course, :doc:`replaceable `) preferences dialog. gpyconf's models and model definitions are inspired by `django `_'s, therefore, gpyconf follows the commonly used and time-tested `Model-View-Controller (MVC) `_ pattern. If you're impatient and/or want to see results quickly, go on reading the :doc:`quickstart` and :doc:`usage` pages. The :doc:`architecture` section gives you information on gpyconf's underlying MVC architecture theory. The :doc:`mvc` section gives you detailed information about each of the three MVC components and contains API documentation so you will be able to implement your own Model or View (or Controller). The last (but not least important) section tells you how to :doc:`install ` and run gpyconf on your machine. Indices and tables ------------------ * :ref:`genindex` * :ref:`modindex` * :ref:`search` gpyconf-0.2/docs/source/mvc.rst000066400000000000000000000027621214002360300165650ustar00rootroot00000000000000Components ========== All three MVC components inherit from the :class:`mvc.MVCComponent` class which adds the following features to them: 1. **Signal-emitting and connecting:** Implements the :class:`gpyconf.events.GSignals` class. You can use the :meth:`emit ` method to emit signals defined in :attr:`__events__ `. To that signals can be connected using the :meth:`connect ` method. For more about this, see the :doc:`signals` section. 2. **Factory wrappers**: Using the :meth:`with_arguments ` classmethod, you can create a wrapper to this class which takes additional arguments. When that wrapper is called, these additional arguments are added to the arguments scope. For more about this, see this method's documentation. *Whenever* you want to pass additional arguments to a frontend or backend, use this classmethod instead of instantiating that class directly, because gpyconf expects the frontend and backend classes to be callable (hence, it does not expect *instances*, but *classes*). Example:: class MyConfiguration(gpyconf.Configuration): backend = ConfigParserBackend.with_arguments(filename='myconf.ini') Components base class ~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: gpyconf.mvc.MVCComponent :members: The three components ~~~~~~~~~~~~~~~~~~~~ .. toctree:: mvc/backends mvc/frontends mvc/configuration gpyconf-0.2/docs/source/mvc/000077500000000000000000000000001214002360300160245ustar00rootroot00000000000000gpyconf-0.2/docs/source/mvc/backends.rst000066400000000000000000000033171214002360300203340ustar00rootroot00000000000000Backends --- Interface to the storage ===================================== What is a backend? ~~~~~~~~~~~~~~~~~~ ... In your :doc:`Configuration definition `, you can overwrite the default backend (which is :class:`gpyconf.backends.configparser.ConfigParserBackend`) using the :attr:`backend` attribute. Example:: class MyConfiguration(Configuration): backend = backends._json.JSONBackend This tells the Controller to use the :class:`JSONBackend`, which stores options in a JSON file. Of course you can define and use your very own backends. To do so, simply define a class inheriting from :class:`Backend` and implement all methods. See :doc:`/own` for more information about using self-built backends. .. note:: If your developing your own backend, you can choose it to run in "compatibility mode". In this mode, all values to store are converted to the :class:`unicode` datatype before calling the :meth:`set_option ` method. Similarly, they will be converted back to their original datatype when reading them. Although not using the compatiblity mode might have considerable advantages (database backends do lots of optimization on this score), using the compatiblity mode is much more comfortable (because you do not have to care about datatypes) and required by some backends that don't support other datatypes (e.g. the default backend, the :class:`ConfigParserBackend `). Included backends ~~~~~~~~~~~~~~~~~ .. automodule:: gpyconf.backends.configparser :members: .. automodule:: gpyconf.backends._json :members: API documentation ~~~~~~~~~~~~~~~~~ .. automodule:: gpyconf.backends :members: gpyconf-0.2/docs/source/mvc/configuration.rst000066400000000000000000000002511214002360300214230ustar00rootroot00000000000000Controller --- Interface to the developer ----------------------------------------- .. automodule:: gpyconf.gpyconf :members: :exclude-members: ConfigurationMeta gpyconf-0.2/docs/source/mvc/frontends.rst000066400000000000000000000021721214002360300205620ustar00rootroot00000000000000Frontends --- Interface to the user =================================== What is a frontend? ~~~~~~~~~~~~~~~~~~~ ... Typically, frontends are configuration dialogs your users can interact with, but nobody bars you from using a web interface or your `coffee maker `_ as frontend. In your :doc:`Configuration definition `, you can overwrite the default frontend (which is a :class:`GtkConfigurationWindow `) using the :attr:`frontend` attribute. Example:: class MyConfiguration(Configuration): frontend = YourCrazyWebInterface This tells the Controller to use the :class:`YourCrazyWebInterface` class. See see that you can define and use your very own frontends. To do so, simply define a class inheriting from :class:`Frontend` and implement all methods. See :doc:`/own` for more information about using self-built frontends. Included frontends ~~~~~~~~~~~~~~~~~~ .. toctree :: :maxdepth: 1 GTK+ Frontend (default) API documentation ~~~~~~~~~~~~~~~~~ .. automodule:: gpyconf.frontends :members: :undoc-members: gpyconf-0.2/docs/source/mvc/frontends/000077500000000000000000000000001214002360300200265ustar00rootroot00000000000000gpyconf-0.2/docs/source/mvc/frontends/gtk.rst000066400000000000000000000002151214002360300213430ustar00rootroot00000000000000GTK+ Configuration Window Frontend ================================== .. automodule:: gpyconf.frontends.gtk :members: :undoc-members: gpyconf-0.2/docs/source/own.rst000066400000000000000000000001301214002360300165660ustar00rootroot00000000000000Writing your own Fields, Views and Backends =========================================== gpyconf-0.2/docs/source/quickstart.rst000066400000000000000000000104731214002360300201700ustar00rootroot00000000000000Quickstart --- gpyconf in 3 minutes =================================== For using gpyconf you do not need to know and understand the :doc:`MVC ` pattern or anything of gpyconf's architecture, you can simply use it as it is. Defining your Model ------------------- Let's say you've written some amazing multi protocol instant messenger and you want your users to enter the following information: * :attr:`im_service` -- selection of three messaging protocols (*required*) * :attr:`nickname` -- a :class:`string` (*required*) * :attr:`password` -- a :class:`string` (*required*) * :attr:`country` -- the country they live in (*optional*) Well then, :doc:`install ` gpyconf, create a new Python module and fill it with the following code:: import gpyconf class UserOptions(gpyconf.Configuration): im_service = gpyconf.fields.MultiOptionField('Service', options=( ('xmpp', 'Jabber/XMPP'), ('icq', 'ICQ'), ('msn', 'MSN') )) nickname = gpyconf.fields.CharField('Nickname', blank=False) password = gpyconf.fields.PasswordField('Password', blank=False) country = gpyconf.fields.MultiOptionField('I live in', options=( ('us', 'United States of America'), ('gb', 'Great Britian'), ('de', 'Germany'), ('at', 'Austria') )) useroptions = UserOptions() print useroptions.nickname # will print you an empty line on first start, then the name you stored print useroptions.country # will print you 'us' on first start, then the code of the country you chose useroptions.run_frontend() # show the window If you know the `django webframework `_, you're most likely to notice the great similarity between gpyconf's and django's model definitions. If you're not familiar with django or it's models, let me explain how it works: You can define fields in your :class:`gpyconf.Configuration` subclass (in this case, it's named :class:`UserOptions`) that represent primitive or non-primitive (Python) datatypes (like :class:`bool`, :class:`string`, :class:`int`, but also stuff like tables and dropdowns (which may be used as :class:`list`)). .. note:: The ``blank=True`` arguments passed to the :class:`CharFields ` enforce that those fields musn't be empty ("blank") when saving the options. Most fields mustn't be blank by default, but for :class:`CharFields ` you have to set this option manually ("empty strings are strings, too"). See :doc:`fields` for an overview of predefined fields. If you want to use your very own fields, please refer to :doc:`own`. Using the API ------------- The API is very simple, but might be a little but confusing for newbies. Regarding the print output, you probably have expected something like "gpyconf.fields.Field instance at " to appear on the screen, but both times you didn't get that stuff. It's because after initialisation, accessing a field attribute doesn't return the field instance itself, but **it's value**. You can still access that field via ``.fields.yourfield``, but you won't need this. Regarding the API, after initialisation, :: myconfigurationinstance.fields.myfield.value is the same as :: myconfigurationinstance.myfield At this point you're almost done -- that's it! You can now take that stuff and put it in your application. :doc:`Let me know ` if you succeeded! Using Signals ------------- Every component of the gpyconf framework (hence, the :doc:`Backend `, the :doc:`Frontend ` and the :doc:`Controller `) supports and emits signals/events. You can listen to that signals simply by connecting them to a function. For more information about this, see :doc:`signals`. Example:: # ... def on_any_field_value_changed(sender_instance, field, new_value): print "Value of %s changed to %s" % (field.field_var, new_value) useroptions.connect('field-value-changed', on_any_field_value_changed) This connects the :func:`on_any_field_value_changed` function the 'field-value-changed' signal, which is emitted when the user changed some value using the frontend. For a list of allowed events/signals for each component refer to their documentation. gpyconf-0.2/docs/source/signals.rst000066400000000000000000000073411214002360300174360ustar00rootroot00000000000000Signals and Events --- The glue =============================== Some theory (yes, again) ~~~~~~~~~~~~~~~~~~~~~~~~ As told before, the three MVC components (the Model, the View and the Controller) can live for their own independently of both of the others. *But wait -- if they are independent, they cannot know of each other!* Yes, that's true -- none of the three components knows of each other, but there's a way they communicate: Signals. Each component "emits" signals at specific points in the code, for example, the :signal:`field-value-changed` signal (which is emitted by the Controller class) is emitted when a field changed it's value. The Controller class connects to signals the backend and the frontend emit; it kinda establishes a connection between them ("routing" over the Controller class). The event system gpyconf uses is compatible to the GObject/GSignals system. I decided to make the event system compatible to that API because frontends will mostly be written using GTK (which uses the GObject GSignals system) but it wouldn't be straightforward to make gpyconf dependent on that library. Connect to signals ~~~~~~~~~~~~~~~~~~ Of course, not only those three components can connect to each other, your application code, can do so, too. Do this by :: component.connect(your_signal, your_callback) Whenever ``your_signal`` is emitted, ``your_callback`` is called with the sender class as first argument and the arguments passed to that emit call. For example, we want to connect a callback to the :signal:`field-value-changed` signal emitted by the Controller class. :: class MyConfiguration(Configuration): ... myconf = MyConfiguration() def my_callback(sender, infected_field, new_value): print "The Controller reported that %s changed its value to %s" % ( infected_field, new_value) myconf.connect('field-value-changed', my_callback) .. note:: For a complete list of signals you can connect to, see the documentation for the :doc:`Configuration `, :doc:`Frontend ` and :doc:`Backend ` classes. Emit signals ~~~~~~~~~~~~ If you want to use signals in your own classes (for example, if you're writing your own backend or frontend or modifying some of them or just want to play around), you have to inherit your class from the :class:`GSignals` class. As told before, gpyconf's event system implements a GSignals-compatible API (by offering the :meth:`emit ` and :meth:`connect ` methods), so you can use your classes like they were inheriting from :class:`gobject.GObject`. Here's an example of how to emit signals:: class MyGreatFrontend(Frontend): def __init__(self): Frontend.__init__(self) self.add_signal('my-great-signal') def emit_something(self): self.emit('my-great-signal', 42) .. note:: All signals you want to use in your classes have to be defined in the :attr:`__event__` list *before* initializing the :class:`GSignals` class. .. warning:: If you're inheriting from another class that defines that :attr:`__event__` list itself and you don't want to lose this list, you can add signals using the :meth:`add_signal ` and :meth:`add_signals ` methods belatedly. Remember that you (or any code you use, including gpyconf) can not connect to any signal before it was defined in the :attr:`__events__` list or added to that list using the named methods! API documentation ~~~~~~~~~~~~~~~~~ .. autoclass:: gpyconf.events.GSignals :members: .. autoclass:: gpyconf.events.GEventRegister .. autoclass:: gpyconf.events.EventRegister :members: .. autoclass:: gpyconf.events.InvalidEvent gpyconf-0.2/docs/source/stuff.rst000066400000000000000000000003561214002360300171240ustar00rootroot00000000000000Other Stuff =========== .. toctree:: :maxdepth: 2 stuff/installation stuff/feedback stuff/coding_guidlines stuff/logging Exceptions ---------- .. automodule:: gpyconf._internal.exceptions :members: :undoc-members: gpyconf-0.2/docs/source/stuff/000077500000000000000000000000001214002360300163665ustar00rootroot00000000000000gpyconf-0.2/docs/source/stuff/coding_guidlines.rst000066400000000000000000000032661214002360300224350ustar00rootroot00000000000000Coding Guidlines ================ When contributing code to gpyconf or submitting your backends or frontends, your code should fit into this guidlines. Basic coding style ~~~~~~~~~~~~~~~~~~ 1. Follow :pep:`8`. 2. Use relative imports whereever you can. See "Relative imports" subsection. 3. Do *NOT* mix GUI logic with program logic, use :doc:`signals ` to connect those two components. 4. Use pseudo-constants instead of hardcoding fix values defined at compile time. Relative Imports ~~~~~~~~~~~~~~~~ It is absolutely essential that your code uses relative imports wherever it is possible. Do not use :: from gpyconf.fields import Field when you're developing a gpyconf compontents, use imports like :: from ..fields import Field Furthermore, use relative imports for importing modules that live in the same directory namespace. If your directory tree looks like this :: mymodule __init__.py foo.py bar.py and you want to import stuff from `foo.py` in `bar.py`, use :: from .foo import yourstuff or :: from . import foo If you want to import stuff defined in the `__init__.py` file, import it with :: from . import yourotherstuff Whitespace usage ~~~~~~~~~~~~~~~~ As defined in PEP8, use whitespace to separate * operators (``1 + 2``, not ``1+2``) * variable names and variable values (``a = b``, not ``a=b``) * arguments (``42, foo=bar``, not ``42,foo=bar``) Furthermore, * between class definitions, put two empty lines (if they belong together logically, put only one line) * don't put any empty lines between class headers, their docstrings and the first class member * use empty lines (sparingly!) to separate code logically gpyconf-0.2/docs/source/stuff/feedback.rst000066400000000000000000000020721214002360300206450ustar00rootroot00000000000000Contribution and Feedback ========================= Feedback ~~~~~~~~ I'm always open to feedback in any form. Please do not consider to contact me. You can find me on `Bitbucket`_ and `Jabber`_. If you've got some problem with gpyconf or have ideas how to improve it, please tell me about! If you think you've found some bug in gpyconf (which is in all likelihood to be the reason for most problems using gpyconf), please `file a bug`_ on Bitbucket. (You can also file bugs describing feature requests, not only problems.). .. _file a bug: http://bitbucket.org/Dauerbaustelle/gpyconf/issues/new/ .. _Bitbucket: http://bitbucket.org/Dauerbaustelle .. _Jabber: xmpp://dauerbaustelle@jabber.lophus.org Contribution ~~~~~~~~~~~~ Are you willing to contribute to gpyconf or have you written a frontend or backend and want to share it with other users? I'm always open to new code, if it appears to be useful for others. Please contact me using the addresses named above. (You might want to modify your code so that it fits into the :doc:`coding_guidlines` before publishing it.) gpyconf-0.2/docs/source/stuff/installation.rst000066400000000000000000000033351214002360300216250ustar00rootroot00000000000000Installation ============ Currently, there aren't any public releases of gpyconf. Althought gpyconf is developed rapidly and therefore it's likely to be very unstable most time, there are some development snapshots provided that should be save to use. Get the code... ~~~~~~~~~~~~~~~ Last stable version -------------------- You can get the last stable (more precisely, *working*) copy of gpyconf with:: hg clone http://bitbucket.org/Dauerbaustelle/gpyconf-stable Bitbucket also provides archives that you can download if you're not familiar with Mercurial: * `gpyconf-stable as zip`_ * `gpyconf-stable as gz`_ * `gpyconf-stable as bz2`_ .. _gpyconf-stable as zip: http://bitbucket.org/Dauerbaustelle/gpyconf-stable/get/tip.zip .. _gpyconf-stable as gz: http://bitbucket.org/Dauerbaustelle/gpyconf-stable/get/tip.gz .. _gpyconf-stable as bz2: http://bitbucket.org/Dauerbaustelle/gpyconf-stable/get/tip.bz2 Development version ------------------- That current development state can be downloaded with:: hg clone http://bitbucket.org/Dauerbaustelle/gpyconf .. warning:: This version is *ONLY* for developers; do *NOT* try to use it in productive environment! The development version is very likely to be broken and may even destroy or corrupt data on your machine (where "data" covers [hopefully] only gpyconf-related data). Use one of the stable releases instead. Old stable releases ------------------- There aren't any old releases yet. ... and install it! ~~~~~~~~~~~~~~~~~~~ gpyconf uses `setuptools`_, you can install it like any other python package using setuptools (the following command has to be executed as super user):: python setup.py install .. _setuptools: http://pypi.python.org/pypi/setuptools gpyconf-0.2/docs/source/stuff/logging.rst000066400000000000000000000024461214002360300205540ustar00rootroot00000000000000Logging ======= gpyconf uses logging all around; the logging output is printed to ``stdout``. By default, the logging level is set to `warning`, which only shows warnings and errors. You may redefine that level in your configuration definition using the :attr:`logging_level` attribute. You can choose between the following levels: +-----------+--------------------------------------------------------------+ | Level | Description | +===========+==============================================================+ | `error` | Shows only errors | +-----------+--------------------------------------------------------------+ | `warning` | Shows errors and warnings | +-----------+--------------------------------------------------------------+ | `debug` | Shows errors, warnings and debug messages | +-----------+--------------------------------------------------------------+ | `info` | Shows everything (errors, warnings, debug and info messages) | +-----------+--------------------------------------------------------------+ This example shows how to set your debug level to `debug`:: class MyConfiguration(Configuration): logging_level = 'debug' gpyconf-0.2/docs/source/usage.rst000066400000000000000000000240021214002360300170730ustar00rootroot00000000000000Usage Guide =========== In the following I will explain how to use gpyconf within your application. I will go into detail about how to basically use the framework, talk about the API, tell you how to change the used backend or frontend, explain how to use signals and so on. If you're in a hurry, you're better off with the :doc:`Quickstart manual `, because things will probably go into detail here and you might not be interested in them. Thinking about your configuration model ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The first step when you start using gpyconf with your application is to think about *what you actually need*. Sure, you want your configuration handled, but before you can start off you need to think about what you want to store. Say you've written an amazing OGG audio player software and you want your users to be able to customize the player's behaviour and style. Let's keep it simple; you'll offer these three options: 1. Crossfading time --- Let the user set the time in seconds your player will use to cross-fade the next song 2. Show album art --- Let the user enable or disable the display of album covers within the player 3. Skin --- Let the user chose between some skins delivered with the player to customize the player's appearance So you've thought of what you need to store, next step will be to think of *how* you would like to store this. Using gpyconf, every configuration option has to have a datatype, so let's work them out: 1. Crossfading time --- an integer between 0 (disabled) and 30 seconds should be nice 2. Show album art --- classical boolean option (there's only *"Yes, I wan't to have this"* and *"No, I don't want to have this"*) 3. Skin --- A string name chosen from a list of available values (skins) Defining the configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~ The next step is to turn that model idea into code. Let's take the example worked out above and write it down:: import gypconf class MyAudioPlayerConfiguration(gpyconf.Configuration): crossfading_time = gpyconf.fields.IntegerField('Crossfading time', min=0, max=30) show_album_art = gpyconf.fields.BooleanField('Show album art', default=True) skin = gpyconf.fields.MultiOptionField('Skin', options=( ('default', 'Ugly default skin'), ('jukebox', 'Jukebox'), ('girlish', 'Girlish'))) Regarding this code example you can easy see that: 1. Every configuration is defined in form of a class derived from the :class:`Configuration ` class 2. Your configuration model is defined in so-called fields 3. These fields are defined as class attributes 4. These fields can have a label ("Crossfading time", "Skin") 5. These fields can have a default value (``default=True``) 6. These fields can be called with additional options like ``min=X``, depending on the field. That's the basics about how to turn out your theoretical option model into working Python code. The API or: Using that configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As you know, classes are just kinda prototypes (at least in Python they are), and because "gpyconf configuration classes" aren't anything special, they are prototypes, too. Therefore, if you want to use the configuration, you musn't keep it as prototype but you need to instantiate it:: configuration = MyAudioPlayerConfiguration() This constructor call loads the :doc:`Backend ` and reads stored values if available. As next step, you might not only want to *define* options, but you possibly might want to *use* (read, change and write) them, too. gpyconf offers a very, very, very simple API for using the option values: Just use them like they were normal attributes. If your option was named ``foo``, you access that option using ``configuration_instance.foo``. In our example, we would get the value of the ``crossfading_time`` using :: >>> configuration.crossfading_time 0 And the same way, we would set that option value:: configuration.crossfading_time = 2 # seconds Regarding the first attribute access you might be confused: Didn't we set the :attr:`crossfading_time` attribute to something with ``IntegerField(...)``, so it should return something like ````? You're perfectly right, it should! This is the only "magic" thing you have to keep in mind: .. note:: Accessing a configuration classes' attribute previously defined as field does not return the field instance itself, but **it's value**. This is very important to remember, so put that in your pipe and smoke it! You can still access the field instances using the :attr:`fields ` attribute :: >>> configuration.fields['crossfading_time'] >>> configuration.fields.crossfading_time # this is exactly the same but you probably won't need that. A short interactive python console log follows to clarify how that API works:: >>> configuration = MyAudioPlayerConfiguration() # 1 >>> configuration.crossfading_time # 2 0 >>> configuration.crossfading_time = 5 # 3 >>> configuration.crossfading_time # 4 5 >>> configuration.fields.crossfading_time # 5 >>> configuration.fields.crossfading_time.value # 6 5 >>> configuration.crossfading_time = 10 # 7 >>> configuration.fields.crossfading_time.value # 8 10 >>> configuration.skin # 9 'default' >>> configuration.skin = 'jukebox' # 10 >>> configuration.skin # 11 'jukebox' Compare the output of statement #4 and #6 and you will recognize that ``configuration_instance.foo`` is exactly the same as ``configuration_instance.fields.foo.value``, so everytime when you access an option using the API you'll get or change the field's :attr:`value ` attribute. Value validation ~~~~~~~~~~~~~~~~ To protect fields from invalid input, almost every field is limited to values of a specific scheme. For example, the :class:`IntegerField ` does only allow integer values (or any values compatible to integers, for example, strings counting only numerics and so on). Congruently, the :class:`BooleanField ` does only allow boolean values, the :class:`ColorField ` allows only colors (RGB tuples or hexadecimal numbers) and so on. Those validation checks are not runned at "run time" but when you want to save the configuration. This guarantees that no invalid values are stored. Although those checks are automatically runned when trying to save the configuration, you might want to check at "run time" wether the current fields' value is valid or not. You can do this using the :meth:`isvalid ` method:: >>> configuration.crossfading_time = 5 >>> configuration.fields.crossfading_time.isvalid() True >>> configuration.crossfading_time = 100 >>> configuration.fields.crossfading_time.isvalid() False # We set the maximum allowed value to 30, so 100 is invalid in this case Please note that validation checks are somewhat different from datatype conversions. Mostly all fields run datatype conversions on values before they "accept" them to ensure that you'll get back the datatype you expect. For example, the :class:`IntegerField ` tries to convert all values to the :class:`int` datatype. See this example:: >>> configuration.crossfading_time = '42' >>> configuration.crossfading_time 42 >>> type(configuration.crossfading_time) Naturally, not every "value" (Python object) is compatible to the :class:`int` datatype. gpyconf raises an error if we want to set completely invalid datatypes:: >>> configuration.crossfading_time = 'this.is.not.an.integer' InvalidOptionError Traceback (most recent call last) .... InvalidOptionError: 'IntegerField allows only int or float types between 0 and 30, stepped with 1, not str' .. warning:: There's a huge difference between datatype conversions the fields do every time you update their value and *real* validation checks! While converting between datatypes only very basic and stupid validation is done. For example, if you set an maxmimum value for an :class:`IntegerField `, this is ignored completely while converting to :class:`int`. To be sure that a value is a valid one for the field with it's current settings, you should *always* ask the :meth:`isvalid ` method! Switching the frontend or backend ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Thanks to gpyconf's modular architecture you can switch the frontend and/or backend components like you want to and completely independently. To do so, simply overwrite the :attr:`backend ` and/or :attr:`frontend ` attributes:: import gpyconf import gpyconf.backends.json class MyConfiguration(gpyconf.Configuration): backend = gpyconf.backends.json.JSONBackend ... or :: import gpyconf import yourmegalcoolfrontend class MyConfiguration(gpyconf.Configuration): frontend = yourmegalcoolfrontend.Frontend ... you can also change that components at class instantiation time:: MyConfiguration(frontend=YourOtherCoolFrontend) Passing arguments to those components works, too. For example if your want to pass the ``title`` option to the default GTK+ frontend, call it with the :meth:`with_arguments ` option:: class MyConfiguration(gpyconf.Configuration): frontend = gpyconf.frontends._gtk.GtkConfigurationWindow.with_arguments( title='Hello World') gpyconf-0.2/examples/000077500000000000000000000000001214002360300146255ustar00rootroot00000000000000gpyconf-0.2/examples/gedit_conf.py000066400000000000000000000027271214002360300173100ustar00rootroot00000000000000from gpyconf import Configuration, fields class GEditConfiguration(Configuration): autobreak = fields.BooleanField('Enable text wrapping', section='View', group='Text Wrapping') nowordsplit = fields.BooleanField('Do not split words over two lines', section='View', group='Text Wrapping') linenos = fields.BooleanField('Display line numbers', section='View', group='Line Numbers') highlight_line = fields.BooleanField('Highlight current line', section='View', group='Current Line') display_rightmargin = fields.BooleanField('Display right margin', section='View', group='Right Margin') rightmargin = fields.IntegerField('Right margin at column:', section='View', group='Right Margin', min=1, max=160) highlight_bracket = fields.BooleanField('Highlight matching bracked', section='View', group='Bracket Matching') tabwidth = fields.IntegerField('Tab width:', section='Editor', group='Tab Stops', default=8, min=1, max=24) use_spaces = fields.BooleanField('Inserted spaces instead of tabs', section='Editor', group='Tab Stops') autoindent = fields.BooleanField('Enable automatic indentation', section='Editor', group='Automatic Indentation') backup = fields.BooleanField('Create a backup copy of files before saving', section='Editor', group='File Saving') autosave = fields.IntegerField('Autosave every', label2='minutes', section='Editor', group='File Saving', default=8, min=1, max=100) gedit = GEditConfiguration() gedit.run_frontend() gpyconf-0.2/examples/pyroom.remake.py000066400000000000000000000027301214002360300177710ustar00rootroot00000000000000from gpyconf import Configuration from gpyconf.fields import IntegerField, BooleanField, MultiOptionField from gpyconf.fields import ColorField, FontField from gpyconf.backends.python import PythonModuleBackend themes = ((theme, theme.title()) for theme in ( 'amber', 'c64', 'darkgreen', 'locontrast', 'banker', 'cupid', 'green', 'website', 'blue', 'custom', 'grey')) class PyRoomConfiguration(Configuration): filename = 'pyroom.conf' autosave = IntegerField('Autosave every', default=2, label2='minutes', group='Autosave') line_numbering = BooleanField('Use line numbering', group='Line Numbering') show_border = BooleanField('Show border', default=True, group='Line Numbering') line_spacing = BooleanField('Line spacing', default=2, group='Line Spacing') preset = MultiOptionField('Presets:', options=themes, section='Theme', default='c64') font = FontField('Font:', section='Theme') background_color = ColorField('Background color', section='Theme') border_color = ColorField('Border color', section='Theme') text_color = ColorField('Text color', section='Theme') text_background_color = ColorField('Text background color', section='Theme') height = IntegerField('Height in %:', default=5, section='Theme') width = IntegerField('Width in %:', default=5, section='Theme') padding = IntegerField('Padding', section='Theme') pyroom_config = PyRoomConfiguration() pyroom_config.run_frontend() gpyconf-0.2/examples/unittests/000077500000000000000000000000001214002360300166675ustar00rootroot00000000000000gpyconf-0.2/examples/unittests/all_fields.py000066400000000000000000000034361214002360300213450ustar00rootroot00000000000000from gpyconf import fields from gpyconftest import Configuration import gpyconf.frontends.gtk MULTI_OPTION_FIELD_OPTIONS = ( ('foo', 'Select me for foo'), ('bar', 'Select me for bar'), (42, 'Select me for the answer to Life, the Universe, and Everything') ) def _all(module): from types import ModuleType for attr in dir(module): if attr == 'Field': continue if attr.startswith('_'): continue attr = getattr(module, attr) if isinstance(attr, ModuleType): # exclude modules: continue yield attr def call(callable): if issubclass(callable, fields.MultiOptionField): # expects parameter, extra handling ins = callable(callable._class_name, options=(MULTI_OPTION_FIELD_OPTIONS)) else: ins = callable() ins.label = ins._class_name return ins _fields = [field for field in _all(fields) if issubclass(field, fields.Field) and not field.abstract] _dict = dict((field._class_name.lower(), field) for field in map(call, _fields)) _dict['logging_level'] = 'info' _dict['frontend'] = gpyconf.frontends.gtk.ConfigurationDialog.with_arguments(title='All Fields', ignore_missing_widgets=True) AllFieldsTest = type('AllFieldsTest', (Configuration,), _dict) # generate the class if __name__ == '__main__': #from gpyconf.backends.python import PythonModuleBackend as BACKEND #from gpyconf.backends._json import JSONBackend as BACKEND from gpyconf.backends._xml import XMLBackend as BACKEND #from gpyconf.backends.configparser import ConfigParserBackend as BACKEND test = AllFieldsTest(backend=BACKEND) test.get_frontend().dialog.set_size_request(800, 600) test.run_frontend() gpyconf-0.2/examples/unittests/defaults.py000066400000000000000000000024161214002360300210530ustar00rootroot00000000000000# Tests wether all fields handle default values correctly. import unittest import gpyconf class DefaultTestConf(gpyconf.Configuration): foo = gpyconf.fields.BooleanField(default=True) bar = gpyconf.fields.CharField(default='Hello') blubb = gpyconf.fields.FileField(default='file:///home/user/foo') peng = gpyconf.fields.IntegerField(default=42, min=11, max=121) multi = gpyconf.fields.MultiOptionField(default='foo', options=( ('foo', 'Foobar'), ('bar', 'Bar') )) class DefaultTest(unittest.TestCase): def setUp(self): self.conf = DefaultTestConf() def runTest(self, new=True): from urlparse import urlparse for var, default in { 'foo' : True, 'bar' : 'Hello', 'blubb' : urlparse('file:///home/user/foo'), 'peng' : 42 }.iteritems(): self.assertEqual(getattr(self.conf, var), default) if new: for var, new in { 'foo' : False, 'bar' : 'Hi there', 'blubb' : 'blaaah', 'peng' : 45 }.iteritems(): setattr(self.conf, var, new) self.conf.reset() self.runTest(new=False) if __name__ == '__main__': unittest.main() gpyconf-0.2/examples/unittests/fontfield.py000066400000000000000000000013531214002360300212150ustar00rootroot00000000000000# Tests the gpyconf.fields.FontField, particularly saving. import unittest import gpyconf import gpyconftest class FontFieldTestConf(gpyconftest.Configuration): f = gpyconf.fields.FontField() class FontFieldTestCase(unittest.TestCase): def setUp(self): self.conf = FontFieldTestConf() def runTest(self): value = self.conf.f self.conf.save() del self.conf self.setUp() self.assertEqual(value, self.conf.f) print "don't change the value!" self.conf.run_frontend() self.assertEqual(value, self.conf.f) print "now change it" self.conf.run_frontend() self.assertNotEqual(value, self.conf.f) if __name__ == '__main__': unittest.main() gpyconf-0.2/examples/unittests/gpyconftest.py000066400000000000000000000002271214002360300216070ustar00rootroot00000000000000from gpyconf import Configuration as Conf_ from gpyconf.backends._xml import XMLBackend class Configuration(Conf_): backend = XMLBackend pass gpyconf-0.2/examples/unittests/hidden_and_editable.py000066400000000000000000000021621214002360300231500ustar00rootroot00000000000000# Tests if hidden and editable are handled correctly. import unittest import gpyconf import gpyconftest class HiddenAndEditableTestConf(gpyconftest.Configuration): editable = gpyconf.fields.IntegerField(default=42) not_editable = gpyconf.fields.IntegerField(default=42, editable=False) hidden = gpyconf.fields.IntegerField(default=42, hidden=True) logging_level = 'info' class HiddenAndEditableTestCase(unittest.TestCase): def setUp(self): self.conf = HiddenAndEditableTestConf() def runTest(self): self.conf.editable = 43 # should change try: self.conf.not_editable = 43 except AttributeError: pass else: raise RuntimeError("Value set but field is not editable") self.assertEqual(self.conf.fields.not_editable.default, self.conf.not_editable) # value mustn't have changed self.conf.run_frontend() self.assertEqual(self.conf.fields.hidden.default, self.conf.hidden) # value shouln't have changed (frontend shouldn't show this field) if __name__ == '__main__': unittest.main() gpyconf-0.2/examples/unittests/inheritance.py000066400000000000000000000031731214002360300215360ustar00rootroot00000000000000import unittest import math from gpyconf import Configuration from gpyconf.fields import IntegerField, CharField, FloatField, NumberField from gpyconf.fields.base import Field class ConfigurationSuperclass(Configuration): field1_from_superclass = IntegerField() class ConfigurationSuperclass2(ConfigurationSuperclass): field1_from_superclass2 = CharField() class InheritedConfiguration(ConfigurationSuperclass2): field1_from_subclass = FloatField() class InheritanceTest(unittest.TestCase): def test_configuration_inheritance(self): self.config = InheritedConfiguration() self.assert_('field1_from_superclass' in self.config.fields) self.assert_('field1_from_superclass2' in self.config.fields) self.assert_('field1_from_subclass' in self.config.fields) self.config.field1_from_superclass = 42 self.config.field1_from_superclass2 = 'hello world' self.config.field1_from_subclass = round(math.pi, 10) self.config.save() del self.config self.config = InheritedConfiguration() self.assertEqual(self.config.field1_from_superclass, 42) self.assertEqual(self.config.field1_from_superclass2, 'hello world') self.assertEqual(self.config.field1_from_subclass, round(math.pi, 10)) def test_field_inheritance(self): self.assertTrue(Field.abstract) class SubField(Field): pass self.assertFalse(SubField.abstract) self.assertTrue(NumberField.abstract) self.assertFalse(IntegerField.abstract) self.assertFalse(FloatField.abstract) if __name__ == '__main__': unittest.main() gpyconf-0.2/examples/unittests/multioptionfield.py000066400000000000000000000026601214002360300226340ustar00rootroot00000000000000# coding: utf-8 # Tests various aspects of the gpyconf.fields.MultiOptionField. # NOTE: # If there's one error and you're using the ConfigParser backend, # everything's alright (string-based backends can't store lists) from itertools import izip import gpyconf import gpyconftest from datetime import datetime import unittest OPTIONS1 = { 'Some label with ünicode' : 'string', 'asdasd' : 43, 'foo' : datetime.now(), 'bar' : range(5) } OPTIONS2 = ( (12.1, 'hi'), (12.2, 'there'), ([1,2,3], 'foo') # fails ) class MultiOptionFieldTestConf(gpyconftest.Configuration): a = gpyconf.fields.MultiOptionField(options=izip(OPTIONS1.values(), OPTIONS1.keys())) b = gpyconf.fields.MultiOptionField(options=OPTIONS2) class MultiOptionFieldTestCase(unittest.TestCase): def setUp(self): self.conf = MultiOptionFieldTestConf() def runTest(self): stored = {} for _field in ('a', 'b'): field = self.conf.fields[_field] for value in field.values: field.value = value self.assert_(field.value == value == getattr(self.conf, _field)) stored[_field] = value self.conf.save() del self.conf self.setUp() for _field in ('a', 'b'): field = self.conf.fields[_field] self.assertEqual(getattr(self.conf, _field), stored[_field]) if __name__ == '__main__': unittest.main() gpyconf-0.2/examples/unittests/mutable_fields.py000066400000000000000000000043711214002360300222250ustar00rootroot00000000000000import unittest import gpyconf.fields import gpyconf._internal.exceptions from gpyconftest import Configuration class TestMutableFields(unittest.TestCase): def test_listfield(self): class Config1(Configuration): field = gpyconf.fields.ListField() conf = Config1() self.assertEqual(conf.field, []) list1 = [1, 2, 3, 4, 99, 'a', 'b', 'c'] conf.field = list1 conf.save() del conf conf = Config1() if conf.backend.__name__ == 'XMLBackend': self.assertEqual(conf.field, list1) else: # no types, everything should be unicode after # serialization and deserialization self.assertEqual(conf.field, map(unicode, list1)) def test_typed_listfield(self): class Config2(Configuration): field = gpyconf.fields.ListField(item_type=float) conf = Config2() list1 = [1.2, 1.3, 42.42, 1.00000978, 3e-300] conf.field = list1 conf.save() del conf conf = Config2() self.assertEqual(conf.field, list1) conf.field = ['a', 'b', 'c'] self.assertRaises(gpyconf._internal.exceptions.InvalidOptionError, conf.save) def test_fixed_length_listfield(self): class Config3(Configuration): field = gpyconf.fields.ListField(length=7) conf = Config3() conf.field = [] self.assert_(not conf.fields.field.isvalid()) conf.field = [1, 2, 3] self.assert_(not conf.fields.field.isvalid()) conf.field = range(7) self.assert_(conf.fields.field.isvalid()) def test_typed_and_fixed_length_listfield(self): class Config4(Configuration): field = gpyconf.fields.ListField(item_type=int, length=3) conf = Config4() conf.field = [1, 2] self.assert_(not conf.fields.field.isvalid()) conf.field = [1, 2, 3.000] self.assert_(not conf.fields.field.isvalid()) conf.field = range(3) self.assert_(conf.fields.field.isvalid()) conf.save() del conf conf = Config4() self.assert_(all(isinstance(item, int) for item in conf.field)) if __name__ == '__main__': unittest.main() gpyconf-0.2/examples/unittests/runtests000077500000000000000000000002021214002360300204760ustar00rootroot00000000000000#!/bin/sh cd /home/jonas/dev/projects/gpyconf/examples/unittests find -name "*.py" -exec python {} > /dev/null \; rm *ini rm *xml gpyconf-0.2/examples/unittests/save.py000066400000000000000000000011451214002360300202000ustar00rootroot00000000000000# Tests wether saving and reading configuration works correctly. import unittest class StorageTestCase(unittest.TestCase): def setUp(self): from all_fields import AllFieldsTest self._class = AllFieldsTest self.options = {} def runTest(self): ins = self._class() for var, field in ins.fields.iteritems(): self.options[var] = field.value ins.save() del ins ins = self._class() for var in self.options: self.assertEqual(self.options[var], getattr(ins, var)) if __name__ == '__main__': unittest.main() gpyconf-0.2/examples/unittests/signals.py000066400000000000000000000010231214002360300206750ustar00rootroot00000000000000# Tests wether signal communication works. import unittest class StorageTestCase(unittest.TestCase): def setUp(self): from all_fields import AllFieldsTest self.conf = AllFieldsTest() self.conf.connect('field-value-changed', self.field_value_changed_cb) def field_value_changed_cb(self, sender, field_name, new_value): self.assertEqual(self.conf.fields[field_name].value, new_value) def runTest(self): self.conf.run_frontend() if __name__ == '__main__': unittest.main() gpyconf-0.2/examples/unittests/xmlbackend_and_dict-_and_list_field.py000066400000000000000000000015321214002360300262740ustar00rootroot00000000000000# coding: utf-8 from gpyconf import Configuration, fields from gpyconf.backends._xml import XMLBackend class Conf(Configuration): backend = XMLBackend a = fields.DictField() b = fields.DictField(keys={'a_dict' : dict, 'a_list' : list, 'a_int' : int}) c = fields.ListField() d = fields.ListField(item_type=dict) c = Conf() c.a = c_a = { 'aasdasd' : (1,2,3), 'asd' : u'baaaräää', # this can't work in XML: # 5: 'elf', # 4 : 42.4222 } c.c = c_c = ['a', 'b', {'foo' : 'bar'}, {'baz': 1, 'peng': 2}] c.d = c_d = [{'a': 1, 'b':2}, {'c':3, 'd':'foo'}] c.b = c_b = { 'a_dict' : c.a, 'a_list' : c.c, 'a_int' : 42 } c.save() #from xml.dom.minidom import parse #print parse(c.backend_instance.file).toprettyxml() del c c = Conf() assert c.a == c_a assert c.b == c_b assert c.c == c_c assert c.d == c_d gpyconf-0.2/gpyconf/000077500000000000000000000000001214002360300144545ustar00rootroot00000000000000gpyconf-0.2/gpyconf/__init__.py000066400000000000000000000000261214002360300165630ustar00rootroot00000000000000from gpyconf import * gpyconf-0.2/gpyconf/_internal/000077500000000000000000000000001214002360300164275ustar00rootroot00000000000000gpyconf-0.2/gpyconf/_internal/__init__.py000066400000000000000000000000011214002360300205270ustar00rootroot00000000000000 gpyconf-0.2/gpyconf/_internal/dicts.py000066400000000000000000000071611214002360300201140ustar00rootroot00000000000000# Contains various dictionary-derivated datastructures. from UserDict import DictMixin from collections import defaultdict try: from collections import OrderedDict as ordereddict except ImportError: class ordereddict(dict, DictMixin): def __init__(self, iterable=None, **kwiterable): try: self.__end except AttributeError: self.clear() self.update(iterable, **kwiterable) def clear(self): self.__end = end = [] end += [None, end, end] # sentinel node for doubly linked list self.__map = {} # key --> [key, prev, next] dict.clear(self) def __setitem__(self, key, value): if key not in self: end = self.__end curr = end[1] curr[2] = end[1] = self.__map[key] = [key, curr, end] dict.__setitem__(self, key, value) def __delitem__(self, key): dict.__delitem__(self, key) key, prev, next = self.__map.pop(key) prev[2] = next next[1] = prev def __iter__(self): end = self.__end curr = end[2] while curr is not end: yield curr[0] curr = curr[2] def __reversed__(self): end = self.__end curr = end[1] while curr is not end: yield curr[0] curr = curr[1] def popitem(self, last=True): if not self: raise KeyError('dictionary is empty') key = reversed(self).next() if last else iter(self).next() return key, self.pop(key) def __reduce__(self): items = [[k, self[k]] for k in self] tmp = self.__map, self.__end del self.__map, self.__end inst_dict = vars(self).copy() self.__map, self.__end = tmp if inst_dict: return (self.__class__, (items,), inst_dict) return self.__class__, (items,) def keys(self): return list(self) setdefault = DictMixin.setdefault update = DictMixin.update pop = DictMixin.pop values = DictMixin.values items = DictMixin.items iterkeys = DictMixin.iterkeys itervalues = DictMixin.itervalues iteritems = DictMixin.iteritems def __repr__(self): if not self: return '%s()' % (self.__class__.__name__,) return '%s(%r)' % (self.__class__.__name__, self.items()) def copy(self): return self.__class__(self) @classmethod def fromkeys(cls, iterable, value=None): return cls((k, value) for k in iterable) def __eq__(self, other): if isinstance(other, ordereddict): return len(self)==len(other) and \ all(p==q for p, q in zip(self.items(), other.items())) return dict.__eq__(self, other) def __ne__(self, other): return not self == other class ordereddefaultdict(ordereddict, defaultdict): def __init__(self, default_factory, *args, **kwargs): defaultdict.__init__(self, default_factory) ordereddict.__init__(self, *args, **kwargs) class dotaccessdict(dict): def __getattr__(self, attr): try: return self[attr] except KeyError: raise AttributeError(attr) class FieldsDict(ordereddict, dotaccessdict): @property def name_value_dict(self): return dict(((name, field.value) for name, field in self.iteritems())) gpyconf-0.2/gpyconf/_internal/exceptions.py000066400000000000000000000007461214002360300211710ustar00rootroot00000000000000# Contains all gpyconf-related exceptions. class GPyConfException(Exception): """ Base class for all gpyconf exceptions """ class InvalidOptionError(GPyConfException): """ Raised if the option of a field is invalid or blank """ def __init__(self, field, message): GPyConfException.__init__(self, message) class MissingOption(GPyConfException): """ Raised by :meth:`Backend.get_option` if there's no such option and no default value was given. """ gpyconf-0.2/gpyconf/_internal/logging.py000066400000000000000000000064421214002360300204350ustar00rootroot00000000000000# coding: utf-8 # %FILEHEADER% # gpyconf's logging system. from __future__ import print_function import os from operator import itemgetter from textwrap import wrap as wordwrap # debug levels LEVELS = ( (0, 'INFO'), (1, 'DEBUG'), (2, 'WARNING'), (3, 'ERROR'), ) DEFAULT_LOGFILE = os.path.expanduser('~/.%(classname)s_gypconf.log') DEFAULT_FORMAT = 'gpyconf::%(level)s: %(message)s' DEFAULT_VERBOSE_FORMAT = 'gpyconf(%%(classname)s)%s' % DEFAULT_FORMAT[7:] class DefaultFormatter(object): """ A default debug string formatter """ _ljust_to = max([len(s) for i, s in LEVELS])+2 def __init__(self, verbose=False): self.verbose = verbose def format(self, level, message, classname=None, field=None): _message = ':'.join((level[1],)).ljust(self._ljust_to) if classname is not None and self.verbose: _message += '(%s)' % classname if field is not None: _message += "%s '%s': " % (field._class_name, field.field_var) message = ('\n'+' '*self._ljust_to).join(wordwrap(message, 79-self._ljust_to)) return _message + message + '\n' class Logger(object): def __init__(self, classname, level=None, verbose=False, formatter=DefaultFormatter, use_file=False, file=None, use_stdout=True): self.level = level if level is not None else LEVELS[2] self.classname = classname self.file = file if file is not None else \ DEFAULT_LOGFILE % {'classname' : classname} self.formatter = formatter(verbose=verbose) self.use_file = use_file self.use_stdout = use_stdout if not (use_file or use_stdout): raise TypeError('No logging output specified. Use at least one of ' 'stdout or file output') @property def level(self): return self._level @level.setter def level(self, level): if level in LEVELS: self._level = level # level given as tuple of (index, name) else: try: self._level = LEVELS[level] # level given as index except (TypeError, IndexError): level_names = map(itemgetter(1), LEVELS) try: self._level = LEVELS[level_names.index(level.upper())] except (ValueError, AttributeError): raise TypeError("Invalid `level` parameter given") def _print(self, *args, **kwargs): if self.use_stdout: print(*args, **kwargs) if self.use_file: print(file=self.file, *args, **kwargs) def info(self, *args, **kwargs): self.log(level=LEVELS[0], *args, **kwargs) def debug(self, *args, **kwargs): self.log(level=LEVELS[1], *args, **kwargs) def warning(self, *args, **kwargs): self.log(level=LEVELS[2], *args, **kwargs) def error(self, *args, **kwargs): self.log(level=LEVELS[3], *args, **kwargs) def log(self, message, level=None, field=None): if level is None: raise TypeError('Buuh!') if level[0] < self.level[0]: # lower level, throw the message away return self._print(self.formatter.format(level, message, field=field, classname=self.classname)) gpyconf-0.2/gpyconf/_internal/serializers.py000066400000000000000000000030631214002360300213370ustar00rootroot00000000000000# TODO: see wether non-printable ASCII SEQUENCEs work with JSON/ConfigParser/... # -> if yes, prefer those over the following sequences. LIST_JOIN_SEQUENCE = '[:NEXT ITEM:]' DICT_KEY_VALUE_JOIN_SEQUENCE = '[:VALUE:]' DICT_PAIR_JOIN_SEQEUNCE = '[:NEXT PAIR:]' def _(o): return map(lambda x:unicode(x) if not isinstance(x, bool) else unicode(int(x)), o) def serialize_list(list): """ Serializes a list/tuple/iterable to a string. """ return LIST_JOIN_SEQUENCE.join(_(list)) def unserialize_list(string, itemtype=unicode): """ Unserializes a serialized list/tuple/iterable to a list using ``itemtype`` as type for each item. """ if not string: return list() if itemtype is bool: itemtype = lambda x:bool(int(x)) return map(itemtype, string.split(LIST_JOIN_SEQUENCE)) def serialize_dict(dict): """ Serializes a dict to a string """ return DICT_PAIR_JOIN_SEQEUNCE.join( DICT_KEY_VALUE_JOIN_SEQUENCE.join(_((k, v))) for k, v in dict.iteritems()) def unserialize_dict(string, typemap=None): """ Unserializes a serialized dict to a dict using value types for keys defined in ``typemap``. """ typemap = typemap or {} if not string: return typemap def fixbool(type): if type is bool: return lambda x:bool(int(x)) else: return type return dict((k, fixbool(typemap.get(k, str))(v)) for k, v in (pair.split(DICT_KEY_VALUE_JOIN_SEQUENCE) for pair in string.split(DICT_PAIR_JOIN_SEQEUNCE))) gpyconf-0.2/gpyconf/_internal/utils.py000066400000000000000000000031731214002360300201450ustar00rootroot00000000000000# %FILEHEADER% # Contains various utilities used within gpyconf. class NONE: def __repr__(self): return 'NONE' class DEFAULT: def __repr__(self): return 'DEFAULT' def isiterable(iterable, include_strings=False): if isinstance(iterable, basestring): return include_strings try: iter(iterable) except TypeError: return False else: return True def create_empty_file(file): open(file, 'w').close() def escape_filename(name, return_escaped_char=False): e = ['/', '\\'] if '\\' in name: # windows e.reverse() if return_escaped_char: return e[0] return name.replace(*e) def filename_from_classname(klass, ext=''): import re import types if isinstance(klass, str): name = klass elif isinstance(klass, types.ClassType): name = klass.__name__ else: name = klass.__class__.__name__ filename = re.sub('([a-z])([A-Z])', '\g<1>_\g<2>', name).lower() # CamelCase => camel_case return ('%s.%s' % (filename, ext)).rstrip('.') class RGBTuple(tuple): """ Tuple for RGB values """ @classmethod def from_hexstring(cls, value): """ Returns a RGB tuple from hexstring ('#RRGGBB') """ value = value.lstrip('#') return cls(int(value[i:i+2], 16) for i in (0, 2, 4)) def to_string(self): """ Returns the hexstring representation ('#RRGGBB') of ``self``""" def _exp(v): return v if len(v) == 2 else '0'+v return '#' + ''.join(map(lambda x:_exp(hex(x)[2:]).upper(), self)) def __str__(self): return self.to_string() gpyconf-0.2/gpyconf/backends/000077500000000000000000000000001214002360300162265ustar00rootroot00000000000000gpyconf-0.2/gpyconf/backends/__init__.py000066400000000000000000000045261214002360300203460ustar00rootroot00000000000000# coding: utf-8 # %FILEHEADER% from itertools import izip from ..mvc import MVCComponent from .._internal.utils import NONE from .._internal.exceptions import MissingOption class Backend(MVCComponent): """ Abstract base class for all configuration backends. If you want to define your own backend, inherit from this class and implement all methods and members. :param backref: Backref to the :class:`gpyconf.Configuration` instance The :signal:`saved` signal is emitted when the backend is finished with saving; the :signal:`read` signal is emitted when the backend is finished with reading them ("read" is past here). """ #: :const:`True` if this backend should run in compatibility mode #: (defaults to :const:`False`). compatibility_mode = False __events__ = ('saved', 'read') def __init__(self, backref): MVCComponent.__init__(self) self.backref = backref def read(self): """ Reads the configuration from the storage (file, database, ...) """ raise NotImplementedError() def save(self): """ Saves the configuration to the storage """ raise NotImplementedError() def set_option(self, name, value): """ Sets option ``name`` to ``value``. """ raise NotImplementedError() def get_option(self, option, default=NONE): """ Returns the value of ``option``. If ``option`` doesn't exist, returns ``default`` if set else raises :exc:`MissingOption`. """ raise NotImplementedError() def reset_all(self): """ Resets all options. """ # This can be done e.g. by deleting the storage file. raise NotImplementedError() @property def options(self): """ A list of all options identifiers. """ # (Could be implemented using a simple attribute, too) raise NotImplementedError() @property def tree(self): """ A configuration tree like this:: { 'option1' : 'myvalue1', 'option2' : 'myvalue2', 'option3' : 3, 'option4' : {'mykey' : 'myvalue'} } (Using standard methods, subclasses don't have to override this property) """ return dict(izip(self.options, map(self.get_option, self.options))) gpyconf-0.2/gpyconf/backends/_json.py000066400000000000000000000021661214002360300177150ustar00rootroot00000000000000# %FILEHEADER% """ The json backend docstring """ from .filebased import FileBasedBackend from . import NONE, MissingOption try: import json except ImportError: import simplejson as json class JSONBackend(FileBasedBackend): """ Backend based on `JSON `_ files """ initial_file_content = '{}' def __init__(self, backref): FileBasedBackend.__init__(self, backref, extension='json') def read(self): with open(self.file) as fobj: self.json_tree = json.load(fobj) or {} def save(self): with open(self.file, 'w') as fobj: json.dump(self.tree, fobj, indent=4) def set_option(self, name, value): self.json_tree[name] = value def get_option(self, name, default=NONE): try: return self.json_tree[self.section][name] except KeyError: if default is not NONE: return default else: raise MissingOption(name) @property def tree(self): return self.json_tree @property def options(self): return self.json_tree.keys() gpyconf-0.2/gpyconf/backends/_xml/000077500000000000000000000000001214002360300171655ustar00rootroot00000000000000gpyconf-0.2/gpyconf/backends/_xml/__init__.py000066400000000000000000000022111214002360300212720ustar00rootroot00000000000000# %FILEHEADER% from ..filebased import FileBasedBackend from .. import NONE, MissingOption from xmlserialize import serialize_to_file, unserialize_file from lxml.etree import XMLSyntaxError class XMLBackend(dict, FileBasedBackend): ROOT_ELEMENT = 'configuration' initial_file_content = '<{0}>'.format(ROOT_ELEMENT) def __init__(self, backref, extension='xml', filename=None): dict.__init__(self) FileBasedBackend.__init__(self, backref, extension, filename) def read(self): try: return unserialize_file(self.file) except XMLSyntaxError, err: self.log('Could not parse XML configuration file: %s' % err, level='error') def save(self): serialize_to_file(self, self.file, root_tag=self.ROOT_ELEMENT) def get_option(self, item): try: return self.__getitem__(item) except KeyError: raise MissingOption(item) set_option = dict.__setitem__ options = property(lambda self:self.keys()) tree = property(lambda self:self) def reset_all(self): self._create_file() self.clear() gpyconf-0.2/gpyconf/backends/configparser.py000066400000000000000000000031351214002360300212640ustar00rootroot00000000000000# %FILEHEADER% from ConfigParser import SafeConfigParser, NoOptionError from .filebased import FileBasedBackend from . import NONE, MissingOption class ConfigParserBackend(FileBasedBackend): """ Wrapper class for :class:`ConfigParser.ConfigParser`. This is the default backend. """ section = 'default_section' compatibility_mode = True def __init__(self, backref): FileBasedBackend.__init__(self, backref, 'ini') self.parser = SafeConfigParser() if not self.parser.has_section(self.section): self.parser.add_section(self.section) self.read() def read(self): with open(self.file) as fobj: self.parser.readfp(fobj) def save(self): with open(self.file, 'wb') as fobj: self.parser.write(fobj) def set_option(self, name, value): try: self.parser.set(self.section, name, value) except TypeError, e: if "option values must be strings" in e: raise TypeError("Option values must be strings, not %s" \ % type(value).__name__) else: raise TypeError(e) def get_option(self, name, default=NONE): try: return self.parser.get(self.section, name) except NoOptionError: if default is not NONE: return default else: raise MissingOption(name) @property def options(self): return self.parser.options(self.section) def reset_all(self): FileBasedBackend.reset_all(self) gpyconf-0.2/gpyconf/backends/dummy.py000066400000000000000000000021561214002360300177370ustar00rootroot00000000000000# %FILEHEADER% from __future__ import print_function from . import Backend from . import NONE, MissingOption _print = print def print(*args, **kwargs): return _print(*(["DummyBackend:"]+list(args)), **kwargs) class DummyBackend(Backend): """ A dummy backend. Does not store any values; :meth:`get_option` returns the corresponding field's current value instead of a stored one. Useful for debugging and playing around with gpyconf. """ def read(self): print("Read") def save(self): print("Save") def set_option(self, name, value): print("Set option %s to %s" % (name, value)) def get_option(self, name, default=NONE): print ("Get option %s" % name) try: return self.backref().fields[name].value except KeyError: print("Option not set, using default value %s" % default) if default: return default else: raise MissingOption def reset_all(self): print ("Reset all") @property def options(self): return self.backref().fields.keys() gpyconf-0.2/gpyconf/backends/filebased.py000066400000000000000000000016131214002360300205170ustar00rootroot00000000000000# %FILEHEADER% import os from .._internal.utils import create_empty_file, filename_from_classname from . import Backend class FileBasedBackend(Backend): """ Abstract base class for file based backends (backends that use files as storage for configuration options). """ create_new = True initial_file_content = '' def __init__(self, backref, extension='', filename=None): Backend.__init__(self, backref) self.file = filename or filename_from_classname(backref(), extension) if not os.path.exists(self.file): if self.create_new: self._create_file() else: raise IOError("No such file: %s" % self.file) def reset_all(self): self._create_file() self.read() def _create_file(self): with open(self.file, 'w') as fobj: fobj.write(self.initial_file_content) gpyconf-0.2/gpyconf/backends/python.py000066400000000000000000000072201214002360300201220ustar00rootroot00000000000000# coding: utf-8 # %FILEHEADER% # A backend dumping values to pure-python-code import __builtin__ from .filebased import FileBasedBackend from . import NONE, MissingOption from pprint import pformat, isreadable MAX_LINE_LENGTH = 80 class SpecialHandlers(object): @staticmethod def datetime(string): return 'import datetime', string @staticmethod def RGBTuple(string): return False, string class PythonModuleBackend(FileBasedBackend): initial_file_content = '__all__ = ()' def __init__(self, backref, filename=None): FileBasedBackend.__init__(self, backref, 'py', filename) self.module = PythonModule(self.file) def read(self): self.module = PythonModule.from_module(__import__(self.file.rstrip('.py'))) def save(self): print self.file with open(self.file, 'w') as fobj: fobj.write(self.module.to_code()) def set_option(self, name, value): if not isreadable(value): raise TypeError("The option '%s' (current value: '%s') can't be " "dumped" % (name, value)) self.module.attributes[name] = value if name in __builtin__.__dict__: self.emit('log', "The option '%s' overwrites the builtin of the " "same name" % name, level='warning') def get_option(self, name, default=NONE): try: return self.module.attributes[name] except KeyError: if default is not NONE: return default else: raise MissingOption(name) def reset_all(self): raise NotImplementedError() @property def options(self): return self.module.attributes.keys() class PythonModule(object): def __init__(self, filename, attributes=None): if not filename.endswith('.py'): filename += '.py' self.filename = filename if attributes is None: attributes = {} self.attributes = attributes @staticmethod def find_import(type_): return 'from %s import %s' % (type_.__module__, type_.__name__) def to_code(self): """ Return a string containing valid python code to be written into the resulting python module. """ imports = set() attributes = {} for attribute, value in self.attributes.iteritems(): _import = None value_string = pformat(value) typename = type(value).__name__ if value is not None and typename not in __builtin__.__dict__: _import = self.find_import(type(value)) try: res = getattr(SpecialHandlers, typename.replace('.', '_')) except AttributeError: pass else: _import, value_string = res(value_string) attributes[attribute] = value_string if _import: imports.add(_import) return '\n\n'.join(x for x in ( ('\n'.join(imports)), ('__all__ = ' + pformat(tuple((attributes.keys())))), '\n'.join('%s = %s' % (a, v) for a, v in attributes.iteritems()) ) if x) def save(self): with open(self.filename, 'w') as fobj: fobj.write(self.to_code()) @classmethod def from_module(cls, module, *kwargs): """ Create a new :class:`PythonModule` with all attributes gained from ``module``. """ module_dict = dict(((k, getattr(module, k)) for k in module.__all__ if not k.startswith('_'))) return cls(module.__file__, attributes=module_dict, *kwargs) gpyconf-0.2/gpyconf/contrib/000077500000000000000000000000001214002360300161145ustar00rootroot00000000000000gpyconf-0.2/gpyconf/contrib/__init__.py000066400000000000000000000000001214002360300202130ustar00rootroot00000000000000gpyconf-0.2/gpyconf/contrib/gtk/000077500000000000000000000000001214002360300167015ustar00rootroot00000000000000gpyconf-0.2/gpyconf/contrib/gtk/__init__.py000066400000000000000000000055351214002360300210220ustar00rootroot00000000000000import time from gi.repository import Gtk as gtk, Gdk as gdk, GObject as gobject from gpyconf.fields import CharField from gpyconf.frontends.gtk.widgets import Widget, WIDGET_MAP class _HotkeyString(unicode): def __new__(cls, *args, **kwargs): self = unicode.__new__(cls, *args, **kwargs) self.keyval, self.modifiers = gtk.accelerator_parse(self) return self class HotkeyField(CharField): def __init__(self, action=None, *args, **kwargs): CharField.__init__(self, *args, **kwargs) self.action = action def to_python(self, value): return _HotkeyString(value) def __valid__(self): return gtk.accelerator_valid(*(gtk.accelerator_parse(self.value))) class HotkeyButton(gtk.Button): __gtype_name__ = 'HotkeyButton' __gsignals__ = { 'changed': (gobject.SignalFlags.RUN_LAST, None, ()), } def __init__(self): gtk.Button.__init__(self) self.value = None self.dialog = gtk.Dialog(None, None, gtk.DialogFlags.MODAL | gtk.DialogFlags.DESTROY_WITH_PARENT, (gtk.STOCK_CANCEL, gtk.ResponseType.REJECT) ) self.dialog.set_position(gtk.WindowPosition.CENTER_ALWAYS) self.dialog_content = self.dialog.get_content_area() self.dialog_content.pack_start(gtk.Label("Please press a hotkey combination..."), True, True, 10) self.dialog.connect("key-press-event", self.dialog_key_press_cb) self.connect('clicked', self.clicked_cb) def clicked_cb(self, source): self.dialog.show_all() device = gtk.get_current_event_device() window = self.dialog.get_window() ownership = gdk.GrabOwnership.NONE t = gtk.get_current_event_time() while gdk.Device.grab(device, window, ownership, False, 0, None, t) != gdk.GrabStatus.SUCCESS: time.sleep(0.1) self.dialog.run() self.dialog.hide() def dialog_key_press_cb(self, source, event): keyval = event.keyval modifier_mask = event.state & gtk.accelerator_get_default_mod_mask() if gtk.accelerator_valid(keyval, modifier_mask): self.set_value(gtk.accelerator_name(keyval, modifier_mask)) self.emit('changed') gdk.Device.ungrab(gtk.get_current_event_device(), gtk.get_current_event_time()) self.dialog.hide() def set_value(self, value): self.value = value self.set_label(gtk.accelerator_get_label(*gtk.accelerator_parse(value))) class HotkeyWidget(Widget): gtk_widget = HotkeyButton def __init__(self, field): Widget.__init__(self, field) def set_value(self, value): self.widget.set_value(value) def get_value(self): return self.widget.value # automagically register all the widgets for the new fields WIDGET_MAP.update({ 'HotkeyField' : HotkeyWidget }) gpyconf-0.2/gpyconf/events.py000066400000000000000000000145251214002360300163410ustar00rootroot00000000000000# %FILEHEADER% from collections import defaultdict from functools import partial from types import FunctionType class InvalidEvent(Exception): """ Raised if a non-defined event should be registered at a strict-mode event register. """ def __init__(self, event): self.event = event def __str__(self): return self.event class EventRegister(object): """ Very simple event handler. Listening functions can register themselves and will be called when the signal they're listening for is emitted. There's no restriction for listening to events, you can listen to any event that might be never called if you want to. Example: >>> events = EventRegister() >>> @events.start ... def on_start_do_this(): ... print "hi there" >>> @events.end ... def on_end_do_that(a, b): ... print a, b >>> events.emit('foo') # nothing will happen 'cause nobody's listening >>> events.emit('start') hi there >>> events.emit('end', 42, 'x') 42 x To avoid typos registering your events, you can use the 'strict' mode (subclass `EventRegister` and define an :attr:`__events__` list). Using the strict mode, you have to define all allowed events in the :attr:`__events__` attribute. Then, if someone wants to register an event not defined in that :attr:`__events__`, an `InvalidEvent` exception will be raised. >>> class MyStrictEvents(EventRegister): ... __events__ = ('hi', 'there') >>> sevents = MyStrictEvents() >>> @sevents.hi ... def on_hi(): ... print "Hi!" >>> @sevents.some_undefined_event ... def never_called(): ... pass Traceback (most recent call last): ... InvalidEvent: some_undefined_event You can have lazy callbacks, too: >>> events = EventRegister() >>> @events.some_signal ... def called_first(): ... print "First callback" >>> @events.some_signal(lazy=True) ... def called_last(): ... print "Last callback" >>> @events.some_signal ... def called_second(): ... print "Second callback" >>> events.emit('some_signal') First callback Second callback Last callback """ strict = False initialized = False def __init__(self): if hasattr(self, '__events__'): self.strict = True self.__events__ = list(self.__events__) self.events = defaultdict(list) self.all_events_listener = [] def __getattr__(self, event): if event == '__events__': raise AttributeError(event) def register_event(*args, **kwargs): if args and isinstance(args[0], FunctionType): self.register_event(event, args[0]) return args[0] else: def wrapper(func): self.register_event(event, func, *args, **kwargs) return func return wrapper return register_event def register_event(self, event, callback, lazy=False): """ Register ``callback`` for ``event``. This is similar to :: @myinstance.myevent def callback(...): ... where ``myevent`` is the value of the ``event`` attribute. """ callback.__dict__['lazy'] = lazy if event == 'all': self.all_events_listener.append(callback) elif self.strict and event not in self.__events__: raise InvalidEvent(event) else: self.events[event].append(callback) def emit(self, event, *args, **kwargs): """ Emit ``event``. Calls all callbacks registered for this ``event`` and all callbacks registered for *all* events (passing ``*args`` and ``**kwargs`` as parameters). Raises :exc:`InvalidEvent` if mode is strict and ``event`` is not defined in :attr:`__events__`. """ if self.strict and event not in self.__events__: raise InvalidEvent(event) lazy_callbacks = [] if event in self.events: for func in self.events[event]: if func.__dict__['lazy']: lazy_callbacks.append(func) else: func(*args, **kwargs) for func in self.all_events_listener: if func.lazy: lazy_callbacks.append(partial(event, func)) else: func(event, *args, **kwargs) for func in lazy_callbacks: func(*args, **kwargs) class GEventRegister(EventRegister): """ Event register for the :class:`GSignals` class. Automatically converts signal names with underscores ("foo_bar") to names with hyphens ("foo-bar"). Same API as :class:`EventRegister`. """ def __init__(self, events=None): if events is not None: self.__events__ = events EventRegister.__init__(self) def __getattr__(self, event): if event == '__events__': raise AttributeError(event) # GSignals uses hyphens, not underscores return EventRegister.__getattr__(self, event.replace('_', '-')) class GSignals(object): """ GObject/GSignals-compatible class mixin. Connected callbacks always have to take a ``sender`` as first argument (this is for GSignals compatibility reasons). :attr:`events` attribute: The :class:`GEventRegister`. """ __gsignals__ = None def __init__(self): events = self.__events__ if self.__gsignals__ is None: self.__gsignals__ = {} else: events += self.__gsignals__.keys() # throw away gsignals parameter definitions (this is gobject-C-stuff) self.events = GEventRegister(events) def connect(self, signal, callback, lazy=False): """ Connect ``callback`` to ``signal`` """ self.events.register_event(signal, callback, lazy) def emit(self, signal, *args, **kwargs): """ Emit ``signal`` """ self.events.emit(signal, self, *args, **kwargs) def add_events(self, events): """ Add a list of events to the allowed events """ self.events.__events__ += list(events) def add_event(self, event): """ Add a event to the allowed events """ self.events.__events__ += [event] if __name__ == '__main__': from doctest import testmod testmod() gpyconf-0.2/gpyconf/fields/000077500000000000000000000000001214002360300157225ustar00rootroot00000000000000gpyconf-0.2/gpyconf/fields/__init__.py000066400000000000000000000000571214002360300200350ustar00rootroot00000000000000from .base import Field from .fields import * gpyconf-0.2/gpyconf/fields/base.py000066400000000000000000000233461214002360300172160ustar00rootroot00000000000000# coding: utf-8 # %FILEHEADER% # Contains the base classes for gpyconf fields. from ..mvc import MVCComponent from .._internal.exceptions import InvalidOptionError __all__= ('Field',) class Field(MVCComponent): """ Superclass for all gpyconf fields. :param label: Label/short description of the presented value (e.g. "Text color:") :param label2: Second label (not used by all frontends) :param default: Default field's value. If ``default`` is :const:`None`, the field's pre-defined default value is used for default. :param blank: :const:`True` if blank value should be allowed when saving. Defaults to ``False`` for most fields, but there are fields (e.g. the :class:`CharField `) that set the default to :const:`True` to allow empty values as well. :param section: Section this field belongs to. The frontend should take care grouping the fields in sections (e.g. using tabs). A section represents the first grouping level. :param group: Group this field belongs to. The frontend takes care of grouping the fields in sections. A group represents the second grouping level (after the section). :param editable: :const:`True` if this field should be editable. Defaults to :const:`True`. :param hidden: :const:`True` if this field should be hidden (not visible to the user). Note that the frontend has to care about hiding a hidden field. Defaults to :const:`False`. :param kwargs: Any other arguments passed to the :meth:`on_initialized` method of field subclasses. .. note:: All additional arguments passed to the field constructor have to be processed by that field. If there are remaining (unused) arguments after that call, a :exc:`TypeError` is raised. """ class __metaclass__(type): def __new__(cls, name, bases, dct): dct.setdefault('abstract', False) return type.__new__(cls, name, bases, dct) abstract = True creation_counter = 0 default = None __events__ = ( 'initialized', 'init-widget', 'value-changed', 'reset-value', 'set-editable' ) is_initialized = False def __init__(self, label=None, section=None, default=None, blank=None, editable=True, hidden=False, group=None, label2=None, **kwargs): MVCComponent.__init__(self) self.update_counter() self.label = label self.label2 = label2 self._editable = editable self.hidden = hidden self.section = section self.group = group if blank is not None: self.blank = blank self.connect('initialized', self.on_initialized) self.emit('initialized', kwargs) self._external_on_initialized(kwargs) # quick n dirty for cream if kwargs: # there are still kwargs left - either the Field subclass did not # pop all kwargs it takes or unexpected kwargs were given. # raise a TypeError. if len(kwargs) == 1: raise TypeError( "%s.__init__ got an unexpected keyword argument %r" \ % (self._class_name, kwargs.iterkeys().next()) ) else: raise TypeError( "%s.__init__ got unexpected keyword arguments %s" \ % (self._class_name, ', '.join(kwargs.iterkeys())) ) if default is not None: self._user_set_default = self.to_python(default) self._value = self.default self.is_initialized = True # we're done (yes, rlly!) def on_initialized(self, sender, kwargs): """ Called after initialization (connected to the `initialized` signal). Fields that take additional keyword arguments have to handle their stuff here. :param kwargs: Optional keyword arguments passed to the :meth:`__init__()` method. All items of this dictionary have to be deleted (recommended is to :meth:`dict.pop()` them) at the end of this function (otherwise, a :exc:`TypeError` will be raised, see comments in :meth:`Field.__init__()`). """ pass def _external_on_initialized(self, kwargs): pass def update_counter(self): # we want the fields exactly in the order we defined them, # so we'll need a creation counter because the fields are # handled by ConfigurationMeta using dicts (which aren't sorted) self.creation_counter = Field.creation_counter Field.creation_counter += 1 def __getattribute__(self, attribute): if attribute == 'default': if hasattr(self, '_user_set_default'): return self._user_set_default elif hasattr(self, 'custom_default'): return self.custom_default() return object.__getattribute__(self, attribute) def __setattr__(self, attribute, value): if attribute == 'default': self._user_set_default = value else: super(Field, self).__setattr__(attribute, value) def get_value(self): """ Returns the (pythonic) value of the widget """ return self._value def set_value(self, value): """ Validates ``value`` (using the :meth:`to_python` method) and stores it. Emits the :signal:`value-changed` signal if the field's value changed. """ if not self.editable: raise AttributeError("Can't change value of non-editable field %r" % self._class_name) value = self.to_python(value) emit = value != self.value self._value = value if emit: self.emit('value-changed', self, value) return value #: The field's current value. # Property for :meth:`get_value` and :meth:`set_value` value = property(get_value, set_value) def isvalid(self): """ Returns :const:`True` if the current value is a valid one (returns :const:`False` if the current value is an invalid one). .. note:: If you're building up a custom field and would need to overwrite this method, overwrite the :meth:`__valid__` method instead. """ return self.__valid__() def __valid__(self): """ (Only for interesting if you're developing your own field) Returns :const:`True` if the current field value is valid. You can do time-consuming checks like validating an email address or an URL here. Returns :const:`False` if the current value is invalid. """ return True def validation_error(self, faulty=None, *args, **kwargs): """ Raises a :exc:`InvalidOptionError` with ``args`` and ``kwargs`` """ allowed_types = self.allowed_types if callable(allowed_types): allowed_types = allowed_types() message = "%(name)s only allows %(allowed)s %(x)s" % { 'name' : self._class_name, 'allowed' : allowed_types, 'x' : '' if faulty is None else " (not %s)" % type(faulty).__name__ } raise InvalidOptionError(self, message, *args, **kwargs) def __blank__(self): return self.value is None def isblank(self): """ Returns :const:`True` if this value is blank (empty). .. note:: If you're building up a custom field and would need to overwrite this method, overwrite the :meth:`__blank__` method instead. """ return self.__blank__() def reset_value(self): """ Reset to the field's default value """ self.value = self.default self.emit('reset-value') def to_python(self, value): """ Returns ``value`` in a field-compatible form. (This may be, for example, a convertion from :class:`int` to :class:`float` or from :class:`str` to :class:`unicode`.) Calls :meth:`validation_error` (which raises a :exc:`InvalidOptionError`) with ``value`` as parameter if ``value`` is not compatible to this field (is invalid). .. warning:: Do *not* use this method for type checks that consum much time (like scheme analyses) because this method may be called, mainly depending on the frontend, frequently. Use the :meth:`__valid__` (see :meth:`isvalid` documentation) method for such purposes. """ return value def python_to_conf(self, value): """ Convert ``value`` to :class:`unicode`. This is only used if the backend used to store values is running in compatibility mode. Subclasses should override this method if conversion is needed. """ return value def conf_to_python(self, value): """ Convert ``value`` from :class:`unicode` to this field's native datatype. This is only used if the backend used to store values is running in compatibility mode. Subclasses should override this method if conversion is needed. """ return value def setfromconf(self, value): """ Set the field's value to ``value`` piped through :meth:`conf_to_python` """ self.value = self.conf_to_python(value) def get_editable(self): return self._editable def set_editable(self, value): if value != self.editable: self.emit('set-editable', value) self._editable = value #: :const:`True` if this field is editable editable = property(get_editable, set_editable) gpyconf-0.2/gpyconf/fields/fields.py000066400000000000000000000234311214002360300175450ustar00rootroot00000000000000# coding: utf-8 # %FILEHEADER% """ Contains gpyconf's default shipped fields. """ from .base import Field from .._internal.exceptions import InvalidOptionError from .._internal.utils import RGBTuple from .._internal.dicts import ordereddict from .mutable import * class BooleanField(Field): """ A field representing the :class:`bool` datatype """ allowed_types = 'boolean compatibles (True, False, 1, 0)' default = False def to_python(self, value): try: return bool(int(value)) except ValueError: if value == 'True': return True elif value == 'False': return False else: self.validation_error(value) def python_to_conf(self, value): return unicode(int(value)) def conf_to_python(self, value): return bool(int(value)) class MultiOptionField(Field): """ A (typically dropdown) selection field. Takes an extra argument ``options`` which is a tuple of two-tuples (or any other iterable datatype in this form) where the first item of the two-tuple holds a value and the second item the label for entry. The value item might be of any type, the label item has to be a string. .. note:: If your backend runs in compatibility mode, values may only be instances of any type convertable from string to that type using ``thattype(somestring)`` (for example, :class:`int`, :class:`float` support that type of conversion). Otherwise, reading values from the backend will fail. Example:: ... = MultiOptionField('My options', options=( ('foo', 'Select me for foo'), ('bar', 'Select me for bar'), (42, 'Select me for the answer to Life, the Universe, and Everything') )) """ # TODO: Rewrite this Field and make it use real dictionaries. def custom_default(self): return self.values[0] def allowed_types(self): return 'unicode-strings in %s' % self.values def on_initialized(self, sender, kwargs): self.options = ordereddict() self.values = [] options = kwargs.pop('options') for value, text in options: self.options[text] = value # 'This is a foo option' : 'foo' self.values.append(value) def to_python(self, value): if value not in self.values: self.validation_error(value) return value def conf_to_python(self, value): def _find_value(): for _value in self.values: if str(_value) == value: return _value _value = _find_value() if _value is None: self.validation_error(value) return type(_value)(value) def python_to_conf(self, value): if type(value)(str(value)) != value: raise InvalidOptionError(self, "%r has an incompatible type (%r)" % (value, type(value))) return str(value) class NumberField(Field): """ A field representing the :class:`int` datatype. Takes three extra arguments: :param min: The minimal value allowed, defaults to 0. :param max: The maxmimal value allowed, defaults to 100. """ abstract = True min = 0 max = 100 def custom_default(self): return self.min def on_initialized(self, sender, kwargs): for key in ('min', 'max'): if key in kwargs: setattr(self, key, kwargs.pop(key)) def python_to_conf(self, value): return unicode(value) def conf_to_python(self, value): return self.num_type(value) def to_python(self, value): try: return self.num_type(value) except (TypeError, ValueError): self.validation_error(value) def __valid__(self): return not (self.min > self.value or self.value > self.max) class IntegerField(NumberField): num_type = int class FloatField(IntegerField): num_type = float class CharField(Field): """ A simple on-line-input field """ allowed_types = 'unicode-strings' default = '' blank = True def __blank__(self): return self.value == '' def to_python(self, value): return unicode(value) class PasswordField(CharField): """ A simple password field. Saves values as a base64 encoded unicode-string. """ def python_to_conf(self, value): # we want at least some basic password covering return value.encode('base64') def conf_to_python(self, value): from binascii import Error as BinError try: return (value+'\n').decode('base64') except BinError: self.validation_error(value) # which will be catched by get_value class IPAddressField(CharField): def __valid__(self): import socket try: # try ipv4 socket.inet_pton(socket.AF_INET, self.value) except socket.error: try: # try ipv6 socket.inet_pton(socket.AF_INET6, self.value) except socket.error: # both failed return False return True class URIField(CharField): """ Field for any type :abbr:`URIs (Uniform Resource Identifier)`. An URI follows the following scheme:: scheme://scheme specific part """ _scheme = '[a-z][a-z\.\-:\d]*://.*' allowed_types = "unicode strings following the URI scheme (%r)" % _scheme def __valid__(self): from re import match return match(self._scheme, self.value) class URLField(CharField): """ Field for any type of :abbr:`URLs (Uniform Resource Locator)` and base class for :class:`FileField`. The :attr:`value` of this field is an instance of :class:`urlparse.ParseResult` and follows common URL syntax. :class:`urlparse.ParseResult` or :class:`unicode` may be used to update this field's value. """ allowed_types = 'unicode-strings and urlparse.ParseResults' def custom_default(self): from urlparse import urlparse return urlparse('') def to_python(self, value): from urlparse import urlparse, urlunparse, ParseResult if isinstance(value, ParseResult): return value if isinstance(value, tuple): # unparse pure tuples so they can be parsed into a ParseResult tuple value = urlunparse(value) return urlparse(value) def python_to_conf(self, value): from urlparse import urlunparse return urlunparse(value) class FileField(URLField): """ Field for file selection. Usage is similar to that of the :class:`URLField`. When updating the value, strings without a scheme (something like ``http://`` or ``file://``) are handled as if they had the ``file://`` scheme. """ def custom_default(self): from urlparse import urlparse return urlparse('file:///') def to_python(self, value): url = URLField.to_python(self, value) if not url.scheme: url = URLField.to_python(self, 'file://'+value) return url class EmailAddressField(CharField): """ A field for email addresses """ allowed_types = 'unicode-strings following the email address scheme' class TextField(CharField): """ A field for (multi-line) text input """ pass class DateTimeField(Field): """ A field for date/time input """ allowed_types = 'datetime.datetime instances' def custom_default(self): from datetime import datetime return datetime.utcnow().replace(microsecond=0) def python_to_conf(self, value): # convert to timestamp import time return unicode(time.mktime(value.timetuple())) def conf_to_python(self, value): from datetime import datetime return datetime.fromtimestamp(float(value)) def to_python(self, value): return value.replace(microsecond=0) # we throw away the microsecond thing - it's irrelevant # and causes problems with conversion using `time.mktime` def __valid__(self): from datetime import datetime return isinstance(self.value, datetime) class ColorField(Field): """ A field for color selections """ _changed_signal = 'color-set' allowed_types = 'hexadecimal color strings (#RRGGBB) or ' \ 'a tuple of integers (r, g, b)' default = RGBTuple((0, 0, 0)) def to_python(self, value): if isinstance(value, basestring): return RGBTuple.from_hexstring(value) else: return RGBTuple(value) def python_to_conf(self, value): return value.to_string() class FontField(DictField): """ A field for font selections. The field's value is a :class:``dict`` following this layout:: { 'name' : 'The font name (e.g. Sans)', 'size' : 'The font size in pixels (e.g. 10)', 'bold' : True or False, 'italic' : True or False, 'underlined' : True or False 'color' : 'The font color (e.g. #CC00FF, case insensitive)', } Every field of this dict except for the `name` and `size` keys may be empty, the `color` key then defaults to `#000000` (black) and the `bold` and `italic` and `underlined` default to :const:`False` """ def custom_default(self): return {'name' : 'Sans', 'size' : 10, 'color' : '#000000', 'italic' : False, 'bold' : False, 'underlined' : False} def on_initialized(self, sender, kwargs): kwargs.update({ 'merge_default' : True, 'keys' : 'fromdefault' }) DictField.on_initialized(self, sender, kwargs) __all__ = tuple(name for name, object in locals().iteritems() if isinstance(object, type) and issubclass(object, Field)) gpyconf-0.2/gpyconf/fields/mutable.py000066400000000000000000000064461214002360300177370ustar00rootroot00000000000000# %FILEHEADER% from .base import Field __all__ = ('ListField', 'DictField') class ListField(Field): # TODO: Docs def custom_default(self): return list() def on_initialized(self, sender, kwargs): self.length = kwargs.pop('length', None) self.item_type = kwargs.pop('item_type', None) def allowed_types(self): s = 'lists/tuples or any other iterable' if self.length is not None: s += ' of length %d' % self.length if self.item_type is not None: s += " that items are a '%s'" % self.item_type return s def to_python(self, iterable): return list(iterable) def python_to_conf(self, value): from .._internal.serializers import serialize_list return serialize_list(value) def conf_to_python (self, value): from .._internal.serializers import unserialize_list return unserialize_list(value, self.item_type) def __valid__(self): if self.length is not None and self.length != len(self.value): return False if self.item_type is not None: return all(isinstance(item, self.item_type) for item in self.value) return True class DictField(Field): # TODO: Docs def custom_default(self): return dict() def on_initialized(self, sender, kwargs): self.keys = kwargs.pop('keys', None) self.merge_default = kwargs.pop('merge_default', True) if self.keys is not None: if self.keys == 'fromdefault': self._keys_setfromdefault() self.statically_typed = isinstance(self.keys, dict) def _keys_setfromdefault(self): self.keys = dict((k, type(v)) for k, v in self.default.iteritems()) def type_of(self, key): if not self.statically_typed: raise TypeError("Can't get type of '%s' key of non-statically typed %s" \ % (key, self._class_name)) return self.keys[key] def allowed_types(self): if self.keys is None: return 'dicts/dict-like objects' else: return 'dicts/dict-like objects with keys %s' % self.keys def to_python(self, value): to_dict = lambda x:x if isinstance(x, dict) else dict(x) try: if not self.merge_default: return to_dict(value) else: return dict(self.default, **to_dict(value)) except TypeError: self.validation_error(value) def conf_to_python(self, value): from .._internal.serializers import unserialize_dict if not self.statically_typed: self.emit('log', "No static key types given, unserialized values " "will all be of type 'unicode'", level='warning') return unserialize_dict(value, self.keys) def python_to_conf(self, value): from .._internal.serializers import serialize_dict return serialize_dict(value) def __valid__(self): if not self.keys: return True # no validation, so always True if set(self.keys) != set(self.value): return False # different keys if not self.statically_typed: return True for k, v in self.value.iteritems(): if not isinstance(v, self.keys[k]): return False return True gpyconf-0.2/gpyconf/frontends/000077500000000000000000000000001214002360300164565ustar00rootroot00000000000000gpyconf-0.2/gpyconf/frontends/__init__.py000066400000000000000000000065771214002360300206060ustar00rootroot00000000000000# coding: utf-8 # %FILEHEADER% from ..fields import Field from ..mvc import MVCComponent, ComponentFactory class Frontend(MVCComponent): """ Abstract base class for all frontends. If you want to define your own frontend, inherit from this class and implement all methods and members. The frontend is called with two parameters: :param backref: A :class:`weakref.ref` to the :class:`gypconf.Configuration` instance :param fields: The Model fields The frontend class now has to care about mapping those abstract fields to widgets. After adding a field to the display area (e.g. a window or a HTML page), it should emit the :signal:`add-field` signal. Furthermore, the frontend has to notify the Controller class about *every* value-change of *any* widget; whenever a widget value was changed, the :signal:`field-value-changed` signal should be emitted (passing the field as first and the new value as second parameter) -- otherwise, the Controller class doesn't know about this value-change and thus, the changes will not be saved. """ __events__ = ( 'add-field', 'close', 'closed', 'save', 'field-value-changed', 'log' ) def __init__(self): MVCComponent.__init__(self) def run(self): """ Run the frontend ;-) """ raise NotImplementedError() def close(self): """ Manages the close event of the frontend. If all inputs are valid, stops the frontend (e.g. closes the window). (If not, it adds an error message using :meth:`add_value_error`.) This method emits the following singals: +-----------------------+--------------------------------------------------------------+ | Signal | Description | +=======================+==============================================================+ | :signal:`save` | | | | | | | When this signal is emitted, the | | | Controller class saves the current field | | | values. If one of that value is not valid, an | | | :exc:`InvalidOptionError ` | | | is raised. This error should be catched and | | | the user should then be notified about that | | | incorrect input to make him correct that fault. | +-----------------------+--------------------------------------------------------------+ | :signal:`close` | Emitted before the frontend is closed | +-----------------------+--------------------------------------------------------------+ | :signal:`closed` | Emitted after the frontend is closed | +-----------------------+--------------------------------------------------------------+ """ self.emit('close') raise NotImplementedError() self.emit('closed') gpyconf-0.2/gpyconf/frontends/gtk/000077500000000000000000000000001214002360300172435ustar00rootroot00000000000000gpyconf-0.2/gpyconf/frontends/gtk/__init__.py000066400000000000000000000161311214002360300213560ustar00rootroot00000000000000# %FILEHEADER% """ gpyconf GTK+ frontend --------------------- Every :class:`Field ` is mapped to a :class:`Widget`, which handles interaction between GTK widgets and gpyconf fields. That mapping is defined in :attr:`widget.WIDGET_MAP`. Sections are separated using tabs. Custom widgets to be added to the mapping have to be inherited from the :class:`Widget` base class and have to implement all documented methods. """ from gi.repository import Gtk as gtk from gpyconf.frontends import Frontend from gpyconf._internal.exceptions import InvalidOptionError from widgets import get_widget_for_field from .utils import * class ConfigurationOption(object): def __init__(self, widget, label=None, post_label=None): self.widget = widget self.label = label self.post_label = post_label self.interface = gtk.Builder() self.interface.add_from_file(joindir(__file__, 'interface/option.ui')) self.label_container = self.interface.get_object('label_container') self.post_label_container = self.interface.get_object('post_label_container') self.widget_container = self.interface.get_object('widget_container') self.widget_container.add(self.widget) if self.label: label = gtk.Label(self.label) label.set_alignment(0, .5) self.label_container.add(label) if self.post_label: post_label = gtk.Label(self.post_label) post_label.set_alignment(0, .5) self.post_label_container.add(post_label) class ConfigurationGroup(object): def __init__(self, title=None): self.options = {} self.title = title self.interface = gtk.Builder() self.interface.add_from_file(joindir(__file__, 'interface/group.ui')) self.group = self.interface.get_object('group') self.table = self.interface.get_object('table') if self.title: self.label = gtk.Label() self.label.set_alignment(0, .5) self.label.set_markup('{0}'.format(self.title)) self.group.pack_start(self.label, True, False, 5) self.group.reorder_child(self.label, 0) def append_option(self, option): row = len(self.options) self.table.resize(len(self.options), 2) if option.label and not option.post_label: self.table.attach(option.label_container, 0, 1, row, row+1) self.table.attach(option.widget_container, 1, 2, row, row+1, xoptions=gtk.Align.FILL) elif option.label and option.post_label: box = gtk.HBox() box.set_spacing(10) box.pack_start(option.label_container, expand=False) box.pack_start(option.widget_container, expand=False) box.pack_start(option.post_label_container, expand=False) self.table.attach(box, 0, 2, row, row+1, xoptions=gtk.Align.FILL) else: self.table.attach(option.widget_container, 0, 2, row, row+1, xoptions=gtk.Align.FILL) def add_field(self, field): opt = ConfigurationOption(field.widget.widget, field.widget.label, field.widget.label2) self.options[field.field_var] = opt self.append_option(opt) class ConfigurationSection(object): def __init__(self, title=None): self.groups = {} self.title = title self.interface = gtk.Builder() self.interface.add_from_file(joindir(__file__, 'interface/section.ui')) self.section = self.interface.get_object('section') self.layout = self.interface.get_object('layout') def append_group(self, group): self.section.pack_start(option.group, True, False, 0) def add_field(self, field): group = self.groups.get(field.group) if group is None: self.groups[field.group] = group = ConfigurationGroup(title=field.group) self.layout.pack_start(group.group, False, False, 5) group.add_field(field) class ConfigurationDialog(Frontend): def __init__(self, backref, fields, title=None, ignore_missing_widgets=False): Frontend.__init__(self) self.sections = {} self.widgets = {} self.interface = gtk.Builder() self.interface.add_from_file(joindir(__file__, 'interface/dialog.ui')) self.dialog = self.interface.get_object('dialog') self.layout = self.interface.get_object('layout') self.content = self.interface.get_object('content') if title: self.dialog.set_title(title) for name, field in fields.iteritems(): if field.hidden: continue self.add_field(field, ignore_missing_widgets) def add_field(self, field, ignore_missing_widgets): try: field.widget = get_widget_for_field(field) except NotImplementedError: # TODO: Eliminate. if ignore_missing_widgets: return else: raise section = self.sections.get(field.section) if section is None: self.sections[field.section] = section = ConfigurationSection() if field.section: self.content.append_page(section.section, gtk.Label(field.section)) else: self.content.insert_page(section.section, gtk.Label("General"), position=0) self.content.set_current_page(0) if self.content.get_n_pages() > 1: self.content.set_show_tabs(True) self.content.set_show_border(True) section.add_field(field) field.connect('value-changed', self.on_field_value_changed) field.widget.connect('log', self.on_widget_log) field.widget.connect('value-changed', self.on_widget_value_changed) if not field.editable: field.widget.widget.set_sensitive(False) self.widgets[field.field_var] = field.widget def on_field_value_changed(self, sender, field_instance, new_value): self.widgets[field_instance.field_var].value = new_value def on_widget_value_changed(self, sender, new_value): self.emit('field-value-changed', sender.field_var, new_value) def on_widget_log(self, sender, msg, level='info'): self.emit('log', msg, level=level) def run(self): self.dialog.show_all() response = self.dialog.run() self.close(save=response==gtk.ResponseType.CLOSE) def close(self, save=False): self.emit('close') if save: try: self.emit('save') except InvalidOptionError: # TODO: Some error displaying here. self.run() self.emit('closed') if __name__ == '__main__': from gpyconf.fields import ColorField f1 = ColorField( label = "Background Color", section = "Foo Section", group = "Bar Group", ) f1.field_var = 'foo' f2 = ColorField( group = "Bar Group", ) f2.field_var = 'bar' dialog = ConfigurationDialog() dialog.add_field(f1) dialog.add_field(f2) dialog.run() gpyconf-0.2/gpyconf/frontends/gtk/interface/000077500000000000000000000000001214002360300212035ustar00rootroot00000000000000gpyconf-0.2/gpyconf/frontends/gtk/interface/dialog.ui000066400000000000000000000043311214002360300230020ustar00rootroot00000000000000 5 center dialog True vertical 2 True True False False False 1 True end gtk-close True True True True False False 0 False end 0 button_close gpyconf-0.2/gpyconf/frontends/gtk/interface/group.ui000066400000000000000000000014551214002360300227030ustar00rootroot00000000000000 True vertical True 2 20 5 False 0 gpyconf-0.2/gpyconf/frontends/gtk/interface/option.ui000066400000000000000000000017711214002360300230600ustar00rootroot00000000000000 True 1 5 5 True 0 5 5 True 0 5 5 gpyconf-0.2/gpyconf/frontends/gtk/interface/section.ui000066400000000000000000000012431214002360300232060ustar00rootroot00000000000000 True 5 5 5 5 True vertical gpyconf-0.2/gpyconf/frontends/gtk/utils.py000066400000000000000000000017471214002360300207660ustar00rootroot00000000000000import os def dict_to_font_description(_dict): from gi.repository.Pango import FontDescription, Style, Weight desc = FontDescription() desc.set_family(_dict['name']) desc.set_size(_dict['size']*1024) # *1024? aha. if _dict['italic']: desc.set_style(Style.ITALIC) if _dict['bold']: desc.set_weight(Weight.BOLD) return desc def font_description_to_dict(desc): from gi.repository.Pango import FontDescription, Style, Weight desc = FontDescription(desc) return { 'name' : desc.get_family(), 'size' : int(desc.get_size()/1024.0), 'bold' : desc.get_weight() == Weight.BOLD, 'italic' : desc.get_style() == Style.ITALIC, 'underlined' : False # TODO } def to_rgb(srgb_tuple): """ Converts a three-tuple of SRGB values to RGB values """ return map(lambda x:int(round(x/257.0)), srgb_tuple) def joindir(file, *parts): return os.path.join(os.path.abspath(os.path.dirname(file)), *parts) gpyconf-0.2/gpyconf/frontends/gtk/widgets.py000066400000000000000000000153321214002360300212670ustar00rootroot00000000000000# %FILEHEADER% from gi.repository import Gtk as gtk from gpyconf.mvc import MVCComponent from .utils import * def get_widget_for_field(field, mapping=None): """ Looks up for a corresponding :class:`Widget` in ``mapping`` (Uses the module-default :attr:`WIDGET_MAP` if ``mapping`` is :const:`None`). Raises :exc:`KeyError` if no such widget is defined. """ if mapping is None: mapping = WIDGET_MAP if field._class_name in mapping: return mapping[field._class_name](field) else: raise NotImplementedError("No widget defined for '%s' field. " "You could extend the `gpyconf.frontends._gtk.WIDGET_MAP` dict " "with your own implementation of a widget for this field " "(inherit from `gpyconf.frontends._gtk.Widget`)." % field) class Widget(MVCComponent): """ Abstract base class for all widgets. A widget is used to "display" a field within the window. """ __events__ = ('value-changed',) _changed_signal = 'changed' def __init__(self, field): MVCComponent.__init__(self) self.widget = self.gtk_widget() self.field_var = field.field_var self.label = field.label self.label2 = field.label2 self.widget.connect(self._changed_signal, self.on_value_changed) self.initialize() self.value = field.value def initialize(self): """ Called after initialition. Widgets can to stuff like additional method calls here. """ pass def on_value_changed(self, sender): """ Value of the :class:`gtk.Widget` changed (mostly through the end user). Emits :signal:`value-changed` with :attr:`value` as parameter. """ self.emit('value-changed', self.value) log_msg = "Value of %s '%s' changed to '%s'" % (self._class_name, self.field_var, self.value) self.emit('log', log_msg, level='info') def get_value(self): """ Returns the widget's value. If the :attr:`prop` attribute is defined, tries to return the corresponding :attr:`gtk.Widget.props` attribute. If not, :exc:`NotImplementedError` is raised (so this method has to be overwritten in that case). """ if hasattr(self, 'prop'): return getattr(self.widget.props, self.prop) else: raise NotImplementedError() def set_value(self, value): """ Set the widget's value to ``value``. If the :attr:`prop` attribute is defined, tries to set the corresponding :attr:`gtk.Widget.props` attribute. If not, :exc:`NotImplementedError` is raised (so this method has to be overwritten in that case). """ if hasattr(self, 'prop'): setattr(self.widget.props, self.prop, value) else: raise NotImplementedError() def to_gtk(self, value): """ Converts ``value`` to gtk compatible value """ return value def to_python(self, value): """ Converts ``value`` to a standard python value """ return value def _set_value(self, value): self.set_value(self.to_gtk(value)) def _get_value(self): return self.to_python(self.get_value()) #: The current value value = property(_get_value, _set_value) # DEFAULT WIDGETS AND CONTAINERS class BooleanWidget(Widget): gtk_widget = gtk.CheckButton prop = 'active' _changed_signal = 'toggled' def initialize(self): self.widget.set_label(self.label) self.label = None self.label2 = None class NumberWidget(Widget): gtk_widget = gtk.SpinButton _changed_signal = 'value-changed' prop = 'value' def __init__(self, field, digits, step): Widget.__init__(self, field) self.widget.set_range(float(field.min), float(field.max)) self.widget.set_digits(digits) self.widget.set_increments(step, self.widget.get_increments()[0]) self.value = field.value # setting the value doesn't work before the range/digits setup class FloatingPointNumberWidget(NumberWidget): def __init__(self, field, decimals=None): NumberWidget.__init__(self, field, digits=2, step=0.01) class IntegerWidget(NumberWidget): def __init__(self, field): NumberWidget.__init__(self, field, digits=0, step=1) class CharWidget(Widget): gtk_widget = gtk.Entry prop = 'text' def to_python(self, value): return unicode(value) class PasswordWidget(CharWidget): def initialize(self): self.widget.set_visibility(False) class IPAddressWidget(CharWidget): pass class URIWidget(CharWidget): pass class URLWidget(URIWidget): def to_gtk(self, value): from urlparse import urlunparse return urlunparse(value) class FileWidget(URLWidget): pass class EmailAddressWidget(CharWidget): pass class MultiOptionWidget(Widget): gtk_widget = gtk.ComboBoxText def __init__(self, field): self.field = field Widget.__init__(self, field) def initialize(self): for option in self.field.options.keys(): self.append_entry(option) def append_entry(self, label): self.widget.append_text(label) def get_value(self): return self.field.values[self.widget.get_active()] def set_value(self, value): self.widget.set_active(self.field.values.index(value)) class ColorWidget(Widget): gtk_widget = gtk.ColorButton prop = 'color' _changed_signal = 'color-set' def to_python(self, value): return to_rgb((value.red, value.green, value.blue)) def to_gtk(self, value): return gtk.gdk.color_parse(value.to_string()) class FontWidget(Widget): gtk_widget = gtk.FontButton _changed_signal = 'font-set' prop = 'font-name' def to_gtk(self, value): return dict_to_font_description(value) def to_python(self, value): return font_description_to_dict(value) class DateTimeWidget(CharWidget): pass class TextWidget(CharWidget): pass WIDGET_MAP = { 'BooleanField' : BooleanWidget, 'CharField' : CharWidget, 'PasswordField' : PasswordWidget, 'IPAddressField' : IPAddressWidget, 'URIField' : URIWidget, 'URLField' : URLWidget, 'FileField' : FileWidget, 'EmailAddressField' : EmailAddressWidget, 'IntegerField' : IntegerWidget, 'FloatField' : FloatingPointNumberWidget, 'MultiOptionField' : MultiOptionWidget, 'ColorField' : ColorWidget, 'DateTimeField' : DateTimeWidget, 'FontField' : FontWidget, 'TextField' : TextWidget } gpyconf-0.2/gpyconf/gpyconf.py000066400000000000000000000254341214002360300165030ustar00rootroot00000000000000# coding: utf-8 # %FILEHEADER% """ The :mod:`gpyconf` module ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The :mod:`gpyconf` module contains gpyconf's default Controller, the :class:`Configuration` class. It takes care of communication between frontend and backend and offers an interface to "the developer". The API is very minimalistic and easy to use. To get information on how to define configuration models and access fields and fields' values, refer to the :doc:`/usage` section. API documentation ----------------- """ import weakref from . import fields, backends, frontends from .mvc import MVCComponent from ._internal import logging, dicts from ._internal import exceptions from ._internal.exceptions import InvalidOptionError __all__ = ('fields', 'backends', 'frontends', 'exceptions', 'Configuration') class Proxy(object): def __setattr__(self, attribute, value): if attribute == '_proxy_obj': object.__setattr__(self, attribute, value) else: self._proxy_obj.__setattr__(attribute, value) def __getattr__(self, attribute): return self._proxy_obj.__getattribute__(attribute) class DefaultBackend(Proxy): def __init__(self, *args, **kwargs): from .backends import configparser self._proxy_obj = configparser.ConfigParserBackend(*args, **kwargs) class DefaultFrontend(Proxy): def __init__(self, *args, **kwargs): from .frontends import gtk self._proxy_obj = gtk.ConfigurationDialog(*args, **kwargs) class ConfigurationMeta(type): """ Metaclass for the :class:`Configuration` class """ def __new__(cls, cls_name, cls_bases, cls_dict): super_new = super(ConfigurationMeta, cls).__new__ parents = tuple(base for base in cls_bases if isinstance(base, ConfigurationMeta)) if not parents: # This isn't a subclass of ConfigurationMeta, don't do anything special return super_new(cls, cls_name, cls_bases, cls_dict) class_fields = cls_dict['fields'] = dicts.FieldsDict() for superclass in parents: for name, field in superclass.fields.iteritems(): class_fields[name] = field field.field_var = name new_fields = list() for name, obj in cls_dict.items(): if isinstance(obj, fields.Field): new_fields.append((name, cls_dict.pop(name))) new_fields.sort(key=lambda item:item[1].creation_counter) for name, field in new_fields: class_fields[name] = field field.field_var = name return super_new(cls, cls_name, cls_bases, cls_dict) class Configuration(MVCComponent): """ gpyconf's controller class. The :class:`Configuration` class acts between the backend and the frontend; it makes the backend load its stored values and passes them to the frontend and the other way round. All keyword arguments passed will result in attributes, so calling the call like this :: conf_instance = MyConfiguration(foo=42, bar='bazz') would set the :attr:`foo` attribute to 42 and the :attr:`bar` to 'bazz'. With this, you can also change the used frontend or backend *at runtime*:: conf_instance = MyConfiguration(frontend=MyGreatWebInterface, backend=MyGreatSQLiteBackend) The signals :signal:`field-value-changed`, :signal:`frontend-initialized`, :signal:`pre-read`, :signal:`pre-save`, :signal:`pre-reset` and :signal:`initialized` should be self-speaking. The signature for a callback connecting to :signal:`field-value-changed` is the following:: def callback(sender_instance, field_instance, new_field_value): ... """ __metaclass__ = ConfigurationMeta fields = dict() frontend_instance = None initially_read = False logger = None logging_level = 'warning' #: The :doc:`backend ` to use backend = DefaultBackend #: The :doc:`frontend ` to use frontend = DefaultFrontend __events__ = ( 'field-value-changed', 'frontend-initialized', 'pre-read', 'pre-save', 'pre-reset', 'initialized' ) def __init__(self, read=True, **kwargs): MVCComponent.__init__(self) for key, value in kwargs.iteritems(): setattr(self, key, value) if self.logger is None: self.logger = logging.Logger(self._class_name, self.logging_level) self.logger.info("Logger initialized (%s)" % self.logger) if not hasattr(self, 'backend_instance'): self.backend_instance = self.backend(weakref.ref(self)) self.backend_instance.connect('log', self.backend_log) self.logger.info("Backend initialized (%s)" % self.backend) for name, instance in self.fields.iteritems(): instance.connect('value-changed', self.on_field_value_changed) self.emit('initialized') if read: self.read() # read the config andd set it to the fields. def on_field_value_changed(self, sender, field, new_value): self.emit('field-value-changed', field.field_var, new_value) # MAGIC/API: def __setattr__(self, attr, value): # if ``attr`` is a field, don't overwrite the field but its value if attr in self.fields: return self.fields[attr].set_value(value) else: super(Configuration, self).__setattr__(attr, value) def __getattr__(self, name): try: return self.fields[name].value except KeyError: raise AttributeError("No such attribute '%s'" % name) # BACKEND: def save(self, save=True): """ Checks for every field wether it's value is valid and not emtpy; if the value is invalid or empty and the field was not marked to allow blank values, an :exc:`InvalidOptionError ` will be raised. Otherwise, passes the fields' values to the backend. If the ``save`` argument is set :const:`True`, makes the backend store the values permanently. """ self.logger.debug("Saving option values...") if self.backend_instance.compatibility_mode: self.logger.info("Backend runs in compatibility mode") for name, field in self.fields.iteritems(): if not field.editable: # not editable, ignore continue if field.isblank(): self.logger.info('Is blank', field=field) if not field.blank: # blank, but blank values are not allowed, raise error raise InvalidOptionError("The option '%s' wasn't set yet" \ % name + " (is None). Use blank=True to safe anyway.") value = None else: # not blank, validate if not field.isvalid(): self.logger.error("Invalid option '%s'" % field.value, field=field) field.validation_error(field.value) value = field.value # if backend runs in compatibility mode, convert to str type: if self.backend_instance.compatibility_mode: if value is None: value = u'' else: value = field.python_to_conf(value) if not isinstance(value, unicode): self.logger.warning("Wrong datatype conversion: " "Got %s, not unicode" % type(value), field=field) self.backend_instance.set_option(name, value) if save: self._save() def _save(self): self.emit('pre-save') self.backend_instance.save() def read(self): """ Reads the configuration options from the backend and updates the fields' values """ self.logger.debug("Reading option values...") self.emit('pre-read') if not self.initially_read: self.backend_instance.read() self.initially_read = True for field, value in self.backend_instance.tree.iteritems(): try: if self.backend_instance.compatibility_mode: self.logger.info("Datatype conversion of '%s'" % field) self.fields[field].setfromconf(value) else: self.fields[field].value = value except KeyError: self.logger.warning("Got an unexpected option name '%s' " "(No field according to configuration option '%s')" % \ (field, field)) def reset(self): """ Resets all configuration options """ self.logger.debug("Resetting option values...") self.emit('pre-reset') self.backend_instance.reset_all() map(lambda field:field.reset_value(), self.fields.values()) self.read() # FRONTEND: def get_frontend(self): """ Returns a (new) instance of the specified :attr:`frontend` """ if self.frontend_instance is None: self.logger.info("Using '%s' as frontend" % (self.frontend.__name__)) # initialize the frontend: self._init_frontend(self.fields) self.frontend_instance.connect('save', self.frontend_save) self.frontend_instance.connect('log', self.frontend_log) self.logger.info("Initialized frontend (%s)" % self.frontend) self.frontend_instance.connect('field-value-changed', self.frontend_field_value_changed) self.emit('frontend-initialized') return self.frontend_instance def _init_frontend(self, fields): """ Instantiates the :attr:`frontend` passing all ``fields`` as parameter and stores it the instance in the :attr:`frontend_instance` attribute. Only interesting for developers who want to overwrite the default :class:`Configuration` class. """ self.frontend_instance = self.frontend(weakref.ref(self), fields) def run_frontend(self): """ Runs (shows) the frontend, waits for the frontend to quit, reads values and saves them if the frontend tells to. """ self.logger.debug("Running frontend...") self.get_frontend().run() def frontend_field_value_changed(self, sender, field_name, new_value): setattr(self, field_name, new_value) def frontend_log(self, sender, msg, level): getattr(self.logger, level)("Frontend: %s" % msg) def frontend_save(self, sender): self.save() # BACKEND: def backend_log(self, sender, msg, level): getattr(self.logger, level)("Backend: %s" % msg) gpyconf-0.2/gpyconf/mvc.py000066400000000000000000000032511214002360300156140ustar00rootroot00000000000000# %FILEHEADER% from . import events class MVCComponent(events.GSignals): """ Base class for all MVC components (frontend, backend, controller). Implements the :class:`gpyconf.events.GSignals` class, so signal emitting and connecting can be used for inheriting classes. """ def __init__(self): events.GSignals.__init__(self) self.add_event('log') def log(self, message, level='debug'): self.emit('log', message, level=level) def warn(self, message): self.log(message, 'warning') @classmethod def with_arguments(cls, *args, **kwargs): """ Returns a :class:`ComponentFactory` instance. That factory is a wrapper to this class that returns an instance of this class when calling it passing ``args`` and ``kwargs`` as additional arguments. For example, :: wrapper = MyMVCComponentSubclass.with_arguments(42, foo='bar') wrapper(a, b) is equal to :: MyMVCComponentSubclass(a, b, 42, foo='bar') """ return ComponentFactory(cls, *args, **kwargs) @property def _class_name(self): """ Wrapper to :attr:`self.__class__.__name__` """ return self.__class__.__name__ class ComponentFactory(object): def __init__(self, cls, *args, **kwargs): self.cls = cls self.args = args self.kwargs = kwargs self.__name__ = cls.__name__ self.__module__ = cls.__module__ self.__doc__ = cls.__doc__ def __call__(self, *args): return self.cls(*args+self.args, **self.kwargs) def __repr__(self): return "" % self.cls.__name__ gpyconf-0.2/setup.py000066400000000000000000000015071214002360300145240ustar00rootroot00000000000000from distutils.core import setup setup( name = 'gpyconf', version = '0.2', description = 'a modular Python configuration framework with support for multiple frontends and backends', author = 'Kristoffer Kleine', author_email= 'kris.kleine@yahoo.de', url = 'http://github.com/kkris/gpyconf', license = '2-clause BSD | LGPL 2.1', packages = ['gpyconf', 'gpyconf.frontends', 'gpyconf.frontends.gtk', 'gpyconf._internal', 'gpyconf.contrib', 'gpyconf.contrib.gtk', 'gpyconf.backends', 'gpyconf.backends._xml', 'gpyconf.fields' ], package_data= {'gpyconf.frontends.gtk' : ['interface/*']} ) gpyconf-0.2/tools/000077500000000000000000000000001214002360300141475ustar00rootroot00000000000000gpyconf-0.2/tools/update_fileheaders.py000066400000000000000000000011201214002360300203300ustar00rootroot00000000000000FILE_HEADER = """ Copyright (c) 2008-2009 Jonas Haag. This file is part of gpyconf. For conditions of distribution and use, see the accompanying LICENSE file. """ FILE_HEADER = '# ' + '\n# '.join(FILE_HEADER.strip('\n').split('\n')) if __name__ == '__main__': import os for _, _, files in os.walk('../src'): for file in files: with open(file) as fobj: content = fobj.read() with open(file, 'w') as fobj: fobj.write(content.replace('# %FILEHEADER%', FILE_HEADER)) print "Updated license header in %s" % file