├── .gitignore ├── DOCUMENTATION.md ├── LICENSE ├── README.md ├── install.sh ├── sample.png ├── shell completion ├── README ├── _codedict └── codedict.sh ├── source ├── __init__.py ├── checksums.md5 ├── codedict ├── database.py ├── lib │ ├── __init__.py │ ├── docopt.py │ └── prettytable.py └── processor.py └── update-md5.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.DB 3 | *.txt 4 | -------------------------------------------------------------------------------- /DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | ## codedict Metadata 4 | 5 | 6 | **Name:** codedict 7 | **Version:** 0.7 8 | **Description:** CLI dictionary for the developer who likes it organized. 9 | **Author:** Sebastian Gabriel Pältz 10 | **Email:** bastipaeltz@googlemail.com 11 | 12 | **Development Status : 4 - Beta**, 13 | **Intended Audience : Developers**, 14 | **Topic : Software Development : Assistance tool**, 15 | **License : MIT License**, 16 | **Programming Language : Python : 2.7** 17 | 18 | 19 | ## codedict uses the following components 20 | 21 | 22 | ### prettytable 23 | 24 | Copyright (c) 2009-2013 Luke Maurits 25 | All rights reserved. 26 | With contributions from: 27 | * Chris Clark 28 | * Christoph Robbert 29 | * Klein Stephane 30 | * "maartendb" 31 | 32 | Redistribution and use in source and binary forms, with or without 33 | modification, are permitted provided that the following conditions are met: 34 | 35 | * Redistributions of source code must retain the above copyright notice, 36 | this list of conditions and the following disclaimer. 37 | * Redistributions in binary form must reproduce the above copyright notice, 38 | this list of conditions and the following disclaimer in the documentation 39 | and/or other materials provided with the distribution. 40 | * The name of the author may not be used to endorse or promote products 41 | derived from this software without specific prior written permission. 42 | 43 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 44 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 45 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 46 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 47 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 48 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 49 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 50 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 51 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 52 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 53 | POSSIBILITY OF SUCH DAMAGE. 54 | 55 | 56 | 57 | ### docopt 58 | 59 | Copyright (c) 2012 Vladimir Keleshev, 60 | 61 | Permission is hereby granted, free of charge, to any person 62 | obtaining a copy of this software and associated 63 | documentation files (the "Software"), to deal in the Software 64 | without restriction, including without limitation the rights 65 | to use, copy, modify, merge, publish, distribute, sublicense, 66 | and/or sell copies of the Software, and to permit persons to 67 | whom the Software is furnished to do so, subject to the 68 | following conditions: 69 | 70 | The above copyright notice and this permission notice shall 71 | be included in all copies or substantial portions of the 72 | Software. 73 | 74 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 75 | KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 76 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 77 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 78 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 79 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 80 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 81 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 82 | 83 | 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sebastian Pältz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # codedict 2 | 3 | #### A command-line dictionary for the developer who likes it organized. 4 | 5 | #### Thanks to Adam for his awesome pull request which brings export and import functionality to codedict. 6 | 7 | ## What is it? 8 | 9 | **codedict** is a little command line tool designed to be your personal dictionary for programming / developing. It is entirely up to you how to organize and arrange it. 10 | **Lightweight and locally stored**, you can create your own *reference*, *documentation* or *dictionary* for development with codedict. 11 | 12 | codedict uses the **classic Cookbook** approach and adds additonal tag features. A typical **codedict entry** consists of 4 values: 13 | 14 | * (programming) **language** - e.g. *'python'* 15 | 16 | * **tags**, seperated by semicolon - e.g. *'list methods;lists'* 17 | 18 | * **problem** - *What do you want to do?* - *'adding element to a list'* for instance 19 | 20 | * The **solution** - *How do you accomplish that?* - *'list.append(element)'* for instance. 21 | solution can be anything, from complicated algorithms or code examples to simple one-liners like our add-to-list example since you can **edit inside your favorite editor, where it is most comfortable.** 22 | 23 | ## How to use 24 | 25 | Here are the elementary commands: 26 | 27 | * `codedict add` 28 | * Basic, interactive, self-explaining way to **add content** to your codedict. 29 | 30 | * `codedict tags` 31 | * Lists **all tags** for the given language and offers to display **all entries associated** with a certain tag. 32 | 33 | * `codedict edit` 34 | * A shortcut for adding or **editing content**. You need to provide a language as well as a problem. If this combination already exists, you can edit the already exisiting solution. If not, a **new entry will be created**. 35 | 36 | * `codedict file` 37 | * You can add a basically **unlimited amount of content** to your codedict **at a time** by reading from a file. Just follow the pattern of beginning every new element(vocabulary) with a '%' and following that up with 3 (tags, problem, solution in that order) sections, each enclosed by '|'. See the **sample.png for an example**. 38 | Content gets **overwritten** when adding new entries. So in case you messed something up - *codedict rollback im sure* will bring you to the point right before your last adding from file. 39 | 40 | * `codedict display` 41 | * Displays content from your codedict. Either for an **entire language** or only for certain problems, which match the **search pattern**. When doing the latter, all problems *starting* with your input get matched (e.g. *'python foo'* matches the problem *foo* as well as *foobar* for the language python). 42 | The output gets printed to **console** (if it isn't longer than 25 lines), to your **pager** or editor in **table form**. Afterwards you can do **further operations**, like updating the solution for example. See the section below for more information. 43 | 44 | * `codedict link` 45 | * You can add links to your codedict. Provide an **URL**, give it a name (optionally, but recommended) and assign it to a certain language (optionally). 46 | 47 | * `codedict export file language` and `codedict import file` 48 | * codedict allows you to export and import entries stored outside of your database for easy backup and storage. 49 | 50 | **When in doubt - `codedict -h` brings you to the help page.** 51 | 52 | 53 | ## How to install 54 | Clone the current revision of the repository with 55 | `git clone --depth=1 https://github.com/BastiPaeltz/codedict.git` 56 | 57 | Run the **install.sh** inside the install directory, it's **usage** is: 58 | `install.sh [INSTALL_DIR] [EXE_DIR]` 59 | 60 | *INSTALL_DIR* and *EXE_DIR*: 61 | You can specify a directory where the actual executable respectively the required libraries / source files **will be placed**. You **won't require** `sudo` rights to install if neither of those directories is in root land. 62 | 63 | **Requires python 2.7 interpreter** 64 | 65 | ### codedict on Windows 66 | 67 | I tested codedict on Windows and the **core program works** on Windows shells (PowerShell etc.) and unix shells (git bash etc.). Just clone codedict and `cd` into the `source` folder. On Windows shells, you would run `python.exe codedict`, on unix shells just run `./codedict`. 68 | The **problem** is, I'm not a Windows guy and unfortunately have no clue how to make codedict runnable from anywhere. 69 | **It would be supercool, if someone of you, who knows how to work with Windows could help me out with this.** 70 | 71 | ## Troubleshooting / remaining options explained 72 | 73 | * After displaying my table, I get prompted with: 74 | `'Do you want to do more? Usage: INDEX [ATTRIBUTE] - Press ENTER to abort:'` 75 | 76 | *Based on the table you can edit your codedict this way.* **Choose the entry you want to change by index.** *If you omit the attribute, you will be brought to your editor (for normal tables), where you can edit the solution - or for link tables, your browser will be opened on the entrie's URL*. 77 | ATTRIBUTE can be `problem` or `tags` for normal tables and 78 | `name` or `language` for link tables. 79 | **You can also type `del`, if you want to delete an entry entirely.** 80 | 81 | * codedict doesn't work with my editor. **I immediately see "Nothing changed"**. 82 | *This has something to do with editors behaving differently in terms of how their executable gets invoked and how they deal with files they're currently working on.* 83 | **Set `--wait` to 'on' to solve this.** 84 | 85 | * `codedict display` has a `-l` and a `-t` option. 86 | 87 | *-l displays* **links** *in the same way like normal display does with dictionary entries. -t works a bit differently. It won't display tags (thats what `codedict tags` is for), but display all (dict) entries that match the pattern.* 88 | 89 | * `codedict display "" "my_search_pattern"` 90 | 91 | *One extra trick you can do - if you omit the language like above, entries get matched across languages. The command above would display all 'my_search_pattern' entries, no matter what language.* 92 | 93 | * What happens when I set `problem` additionally when adding from a file ? 94 | 95 | *By default the content of the file will be parsed like described in the related section above. When 'problem' is set, the file's content will not be parsed but instead set as the* `solution` field *(of that specified 'problem').* 96 | 97 | * How can I see my current configurations (editor etc.) ? 98 | 99 | *Just invoke the command you like without a value - e.g.* `codedict --editor` 100 | 101 | ## Shell auto completion 102 | There are completion files provided for **zsh and bash** inside the shell completion folder. 103 | 104 | 105 | ## License 106 | 107 | *MIT* 108 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | Install () { 4 | 5 | 6 | LOCATION="source/" 7 | 8 | echo "Beginning installation..." 9 | 10 | mkdir -p "${1}" 11 | chown $(logname): "${1}" 12 | 13 | if [ -e "${1}/res" ]; then 14 | cp -pvi ${LOCATION}* ${1} 15 | else 16 | cp -rpvi ${LOCATION}* ${1} 17 | fi 18 | 19 | case "$1" in 20 | /*) ABSOLUTE_PATH=""$1"codedict";; 21 | *) ABSOLUTE_PATH="$(pwd)/"$1"codedict";; 22 | esac 23 | 24 | 25 | EXECTEXT='#!/bin/sh \n \n'${ABSOLUTE_PATH}' $@' 26 | 27 | case "$3" in 28 | */) ABSOLUTE_PATH=""$1"codedict";; 29 | *) ABSOLUTE_PATH="$(pwd)/"$1"codedict";; 30 | esac 31 | 32 | echo "$EXECTEXT" > ${2}"codedict" 33 | chmod +x ${2}"codedict" 34 | 35 | } 36 | 37 | 38 | if [ -e source/ ]; then 39 | 40 | INSTALLDIR=$1 41 | EXECUTABLEDIR=$2 42 | 43 | 44 | if [ ! $INSTALLDIR ]; then 45 | INSTALLDIR="./" 46 | fi 47 | 48 | if [ ! $EXECUTABLEDIR ]; then 49 | EXECUTABLEDIR="/usr/local/bin/" 50 | fi 51 | 52 | case "$INSTALLDIR" in 53 | */) INSTALLDIR=$INSTALLDIR;; 54 | *) INSTALLDIR=$INSTALLDIR"/";; 55 | esac 56 | 57 | case "$EXECUTABLEDIR" in 58 | */) EXECUTABLEDIR=$EXECUTABLEDIR;; 59 | *) EXECUTABLEDIR=$EXECUTABLEDIR"/";; 60 | esac 61 | 62 | 63 | echo "\ 64 | Installing from source.\n\ 65 | All files will be placed into "\'$INSTALLDIR\'" .\n\ 66 | The actual executable will be placed into "\'$EXECUTABLEDIR\'" ." 67 | 68 | while true; do 69 | read -p "Do you wish to install this program? (y/n) " yn 70 | case $yn in 71 | [Yy]* ) Install $INSTALLDIR $EXECUTABLEDIR; break;; 72 | [Nn]* ) exit;; 73 | * ) echo "Please answer yes or no. (y/n) ";; 74 | esac 75 | done 76 | 77 | else 78 | echo "Installation error - missing files." 79 | exit 80 | fi 81 | -------------------------------------------------------------------------------- /sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BastiPaeltz/codedict/5830cc277c5d0dbcd62d5d86217383fad4d0f207/sample.png -------------------------------------------------------------------------------- /shell completion/README: -------------------------------------------------------------------------------- 1 | _codedict contains zsh completion. 2 | codedict.sh contains bash completion. 3 | -------------------------------------------------------------------------------- /shell completion/_codedict: -------------------------------------------------------------------------------- 1 | #compdef codedict 2 | 3 | _message_next_arg() 4 | { 5 | argcount=0 6 | for word in "${words[@][2,-1]}" 7 | do 8 | if [[ $word != -* ]] ; then 9 | ((argcount++)) 10 | fi 11 | done 12 | if [[ $argcount -le ${#myargs[@]} ]] ; then 13 | _message -r $myargs[$argcount] 14 | if [[ $myargs[$argcount] =~ ".*file.*" || $myargs[$argcount] =~ ".*path.*" ]] ; then 15 | _files 16 | fi 17 | fi 18 | } 19 | 20 | _codedict () 21 | { 22 | local context state state_descr line 23 | typeset -A opt_args 24 | 25 | if [[ $words[$CURRENT] == -* ]] ; then 26 | _arguments -C \ 27 | ':command:->command' \ 28 | '(--suffix)--suffix[Sets the suffix for the specified language to the given value.]' \ 29 | '(--editor)--editor[Sets your editor to the specified value.]' \ 30 | '(--wait)--wait[This is needed on certain editors.]' \ 31 | '(--line)--line[The output table gets formated based on this value.]' \ 32 | 33 | else 34 | myargs=('LANGUAGE' 'SUFFIX' 'EDITOR' 'INTEGER') 35 | _message_next_arg 36 | fi 37 | } 38 | 39 | 40 | _codedict "$@" -------------------------------------------------------------------------------- /shell completion/codedict.sh: -------------------------------------------------------------------------------- 1 | 2 | _codedict() 3 | { 4 | local cur 5 | cur="${COMP_WORDS[COMP_CWORD]}" 6 | 7 | if [ $COMP_CWORD -eq 1 ]; then 8 | COMPREPLY=( $( compgen -fW '--suffix --editor --wait --line rollback on off tags edit add link file import export display' -- $cur) ) 9 | else 10 | case ${COMP_WORDS[1]} in 11 | rollback) 12 | _codedict_rollback 13 | ;; 14 | on) 15 | _codedict_on 16 | ;; 17 | off) 18 | _codedict_off 19 | ;; 20 | tags) 21 | _codedict_tags 22 | ;; 23 | edit) 24 | _codedict_edit 25 | ;; 26 | add) 27 | _codedict_add 28 | ;; 29 | link) 30 | _codedict_link 31 | ;; 32 | file) 33 | _codedict_file 34 | ;; 35 | import) 36 | _codedict_import 37 | ;; 38 | export) 39 | _codedict_export 40 | ;; 41 | display) 42 | _codedict_display 43 | ;; 44 | esac 45 | 46 | fi 47 | } 48 | 49 | _codedict_rollback() 50 | { 51 | local cur 52 | cur="${COMP_WORDS[COMP_CWORD]}" 53 | 54 | if [ $COMP_CWORD -eq 2 ]; then 55 | COMPREPLY=( $( compgen -W ' im' -- $cur) ) 56 | else 57 | case ${COMP_WORDS[2]} in 58 | im) 59 | _codedict_rollback_im 60 | ;; 61 | esac 62 | 63 | fi 64 | } 65 | 66 | _codedict_rollback_im() 67 | { 68 | local cur 69 | cur="${COMP_WORDS[COMP_CWORD]}" 70 | 71 | if [ $COMP_CWORD -eq 3 ]; then 72 | COMPREPLY=( $( compgen -W ' sure' -- $cur) ) 73 | else 74 | case ${COMP_WORDS[3]} in 75 | sure) 76 | _codedict_rollback_im_sure 77 | ;; 78 | esac 79 | 80 | fi 81 | } 82 | 83 | _codedict_rollback_im_sure() 84 | { 85 | local cur 86 | cur="${COMP_WORDS[COMP_CWORD]}" 87 | 88 | if [ $COMP_CWORD -ge 4 ]; then 89 | COMPREPLY=( $( compgen -W ' ' -- $cur) ) 90 | fi 91 | } 92 | 93 | _codedict_on() 94 | { 95 | local cur 96 | cur="${COMP_WORDS[COMP_CWORD]}" 97 | 98 | if [ $COMP_CWORD -ge 2 ]; then 99 | COMPREPLY=( $( compgen -W ' ' -- $cur) ) 100 | fi 101 | } 102 | 103 | _codedict_off() 104 | { 105 | local cur 106 | cur="${COMP_WORDS[COMP_CWORD]}" 107 | 108 | if [ $COMP_CWORD -ge 2 ]; then 109 | COMPREPLY=( $( compgen -W ' ' -- $cur) ) 110 | fi 111 | } 112 | 113 | _codedict_tags() 114 | { 115 | local cur 116 | cur="${COMP_WORDS[COMP_CWORD]}" 117 | 118 | if [ $COMP_CWORD -ge 2 ]; then 119 | COMPREPLY=( $( compgen -fW ' ' -- $cur) ) 120 | fi 121 | } 122 | 123 | _codedict_edit() 124 | { 125 | local cur 126 | cur="${COMP_WORDS[COMP_CWORD]}" 127 | 128 | if [ $COMP_CWORD -ge 2 ]; then 129 | COMPREPLY=( $( compgen -fW ' ' -- $cur) ) 130 | fi 131 | } 132 | 133 | _codedict_add() 134 | { 135 | local cur 136 | cur="${COMP_WORDS[COMP_CWORD]}" 137 | 138 | if [ $COMP_CWORD -ge 2 ]; then 139 | COMPREPLY=( $( compgen -W ' ' -- $cur) ) 140 | fi 141 | } 142 | 143 | _codedict_link() 144 | { 145 | local cur 146 | cur="${COMP_WORDS[COMP_CWORD]}" 147 | 148 | if [ $COMP_CWORD -ge 2 ]; then 149 | COMPREPLY=( $( compgen -fW ' ' -- $cur) ) 150 | fi 151 | } 152 | 153 | _codedict_file() 154 | { 155 | local cur 156 | cur="${COMP_WORDS[COMP_CWORD]}" 157 | 158 | if [ $COMP_CWORD -ge 2 ]; then 159 | COMPREPLY=( $( compgen -fW ' ' -- $cur) ) 160 | fi 161 | } 162 | 163 | _codedict_import() 164 | { 165 | local cur 166 | cur="${COMP_WORDS[COMP_CWORD]}" 167 | 168 | if [ $COMP_CWORD -ge 2 ]; then 169 | COMPREPLY=( $( compgen -fW ' ' -- $cur) ) 170 | fi 171 | } 172 | 173 | _codedict_export() 174 | { 175 | local cur 176 | cur="${COMP_WORDS[COMP_CWORD]}" 177 | 178 | if [ $COMP_CWORD -ge 2 ]; then 179 | COMPREPLY=( $( compgen -fW ' ' -- $cur) ) 180 | fi 181 | } 182 | 183 | _codedict_display() 184 | { 185 | local cur 186 | cur="${COMP_WORDS[COMP_CWORD]}" 187 | 188 | if [ $COMP_CWORD -ge 2 ]; then 189 | COMPREPLY=( $( compgen -fW '-t -l --hline ' -- $cur) ) 190 | fi 191 | } 192 | 193 | complete -F _codedict codedict -------------------------------------------------------------------------------- /source/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BastiPaeltz/codedict/5830cc277c5d0dbcd62d5d86217383fad4d0f207/source/__init__.py -------------------------------------------------------------------------------- /source/checksums.md5: -------------------------------------------------------------------------------- 1 | 4818170c2ace0ad4181e40439fe7d8c7 codedict 2 | 67e8a83baee200e02c1c13f5aa0229a3 database.py 3 | d41d8cd98f00b204e9800998ecf8427e __init__.py 4 | 036b662f7a1ca41d92c910f597448127 processor.py 5 | -------------------------------------------------------------------------------- /source/codedict: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | 4 | Let's you compile and access your own personal dictionary 5 | for programming via the command line with ease. 6 | 7 | Usage: 8 | codedict display LANGUAGE [SEARCHPATTERN] [(-t | -l) --hline] 9 | codedict file PATH-TO-FILE LANGUAGE [PROBLEM] 10 | codedict edit LANGUAGE PROBLEM 11 | codedict add 12 | codedict tags LANGUAGE 13 | codedict link URL [LINK_NAME] [LANGUAGE] 14 | codedict export PATH-TO-FILE LANGUAGE 15 | codedict import PATH-TO-FILE 16 | codedict --suffix LANGUAGE [SUFFIX] 17 | codedict --editor [EDITOR] 18 | codedict --wait (off | on) 19 | codedict --line [INTEGER] 20 | codedict rollback im sure 21 | 22 | Options: 23 | --editor Sets your editor to the specified value. 24 | This has to be an executable. 25 | --line The output table gets formated based on this value. 26 | Should match your console's line length. Default value: 80 27 | --suffix Sets the suffix for the specified language to the given value. 28 | This is convenient for syntax highlighting inside editors. 29 | --hline Doesn't print horizontal line between each row of output table. 30 | --wait This is needed on certain editors. 31 | See the troubleshooting section on GitHub. 32 | --rollback Rolls database back to moment right before your last file adding 33 | --help Show this screen. 34 | --version Show version. 35 | 36 | """ 37 | 38 | # relative import 39 | from lib.docopt import docopt 40 | import processor 41 | 42 | if __name__ == '__main__': 43 | 44 | COMMAND_LINE_ARGS = docopt(__doc__, version="codedict v0.7") 45 | 46 | try: 47 | processor.start_process(COMMAND_LINE_ARGS) 48 | except (KeyboardInterrupt, EOFError): 49 | print "\nAborted!" 50 | -------------------------------------------------------------------------------- /source/database.py: -------------------------------------------------------------------------------- 1 | """ 2 | Defines the data processing / handling using local sqlite3 database. 3 | """ 4 | 5 | # import from standard library 6 | from collections import namedtuple 7 | import sqlite3 8 | import sys 9 | import os 10 | import shutil 11 | 12 | # This structures return values form 'Database.get_full_dump' 13 | DumpEntry = namedtuple('DumpEntry', ['language', 'tags', 'problem', 'solution']) 14 | 15 | class Database(object): 16 | """ 17 | DB class, handles all connections with the database. 18 | """ 19 | 20 | def __init__(self): 21 | self.db_path = determine_db_path() 22 | if not os.path.isdir(self.db_path): 23 | print "Building database." 24 | os.makedirs(self.db_path) 25 | self._db_instance = establish_db_connection(self.db_path + '/codedict_db.DB') 26 | self._setup_database() 27 | 28 | def _setup_database(self): 29 | """ 30 | Sets up the database for usage. Exits if connecting to DB or setting up 31 | tables fails. 32 | """ 33 | 34 | if not self._db_instance: 35 | print "Error while reaching DB." 36 | sys.exit(1) 37 | 38 | if not self._create_tables(): 39 | print "Error while creating DB tables." 40 | sys.exit(1) 41 | 42 | def _create_tables(self): 43 | """ 44 | Creates tables 'dictionary', 'languages', 'links' and 'config' if they not exist. 45 | """ 46 | 47 | try: 48 | with self._db_instance: 49 | # create tables 50 | self._db_instance.execute(''' 51 | CREATE table IF NOT EXISTS Languages (id INTEGER PRIMARY KEY, 52 | language TEXT UNIQUE, suffix TEXT) 53 | ''') 54 | 55 | self._db_instance.execute(''' 56 | CREATE table IF NOT EXISTS Tags (id INTEGER PRIMARY KEY, 57 | name TEXT, language TEXT) 58 | ''') 59 | 60 | self._db_instance.execute(''' 61 | CREATE table IF NOT EXISTS ItemsToTags (id INTEGER PRIMARY KEY, 62 | tagID INTEGER, dictID INTEGER) 63 | ''') 64 | 65 | self._db_instance.execute(''' 66 | CREATE table IF NOT EXISTS Dictionary 67 | (id INTEGER PRIMARY KEY, language TEXT, 68 | problem TEXT, solution TEXT) 69 | ''') 70 | 71 | self._db_instance.execute(''' 72 | CREATE table IF NOT EXISTS Config (configItem TEXT PRIMARY KEY, value TEXT) 73 | ''') 74 | 75 | self._db_instance.execute(''' 76 | CREATE table IF NOT EXISTS Links (id INTEGER PRIMARY KEY, name TEXT, 77 | URL text, language TEXT) 78 | ''') 79 | 80 | return True 81 | 82 | except sqlite3.Error as error: 83 | print "A database error has ocurred: ", error 84 | return False 85 | 86 | def rollback(self): 87 | """ 88 | Rolls back the database to last 89 | """ 90 | 91 | try: 92 | shutil.copy2(self.db_path + "/BACKUP_codedict_db.DB", self.db_path + "/codedict_db.DB") 93 | except (shutil.Error, IOError, OSError) as error: 94 | print "Error while rolling back database.\n", error 95 | sys.exit(1) 96 | 97 | def get_config_item(self, config_item): 98 | """ 99 | Gets a config item (editor or line-length) from the Config table. 100 | """ 101 | 102 | try: 103 | with self._db_instance: 104 | value = self._db_instance.execute(''' 105 | SELECT value from Config where configItem = ? 106 | ''', (config_item, )) 107 | 108 | return value.fetchone() 109 | except sqlite3.Error as error: 110 | print "A database error has ocurred: ", error 111 | sys.exit(1) 112 | 113 | def set_config_item(self, config_item, value): 114 | """ 115 | Sets the editor row in the Config table of the DB. 116 | """ 117 | 118 | try: 119 | with self._db_instance: 120 | 121 | self._db_instance.execute(''' 122 | INSERT or IGNORE INTO Config (configItem, value) VALUES (?, ?) 123 | ''', (config_item, value, )) 124 | 125 | self._db_instance.execute(''' 126 | UPDATE Config SET value = ? WHERE configItem = ? 127 | ''', (value, config_item, )) 128 | 129 | except sqlite3.Error as error: 130 | print "A database error has ocurred: ", error 131 | sys.exit(1) 132 | 133 | def retrieve_suffix(self, lang_name): 134 | """ 135 | Retrieves suffix for 1 language from Language table of the DB. 136 | """ 137 | 138 | try: 139 | with self._db_instance: 140 | 141 | suffix = self._db_instance.execute(''' 142 | SELECT suffix from Languages where language = ? 143 | ''', (lang_name, )) 144 | 145 | return suffix.fetchone() 146 | except sqlite3.Error as error: 147 | print "A database error has ocurred: ", error 148 | sys.exit(1) 149 | 150 | def set_suffix(self, lang_name, suffix): 151 | """ 152 | Inserts suffix for 1 language into the DB. 153 | """ 154 | 155 | try: 156 | with self._db_instance: 157 | 158 | self._db_instance.execute(''' 159 | INSERT or IGNORE into Languages (language, suffix) VALUES(?,?) 160 | ''', (lang_name, suffix)) 161 | 162 | self._db_instance.execute(''' 163 | UPDATE Languages SET suffix = ? WHERE language = ? 164 | ''', (suffix, lang_name, )) 165 | 166 | except sqlite3.Error as error: 167 | print "A database error has ocurred: ", error 168 | sys.exit(1) 169 | 170 | def delete_content(self, values): 171 | """ 172 | Deletes content from the Dictionary table, language and problem field 173 | have to match the rows values. 174 | """ 175 | 176 | try: 177 | with self._db_instance: 178 | 179 | self._db_instance.execute(''' 180 | DELETE from Dictionary WHERE problem = ? AND language = 181 | (SELECT language from Languages where language = ?) 182 | ''', (values['problem'], values['language'])) 183 | 184 | except sqlite3.Error as error: 185 | print "A database error has ocurred: ", error 186 | sys.exit(1) 187 | 188 | def update_content(self, values): 189 | """ 190 | Updates content of the DB, not insert! Only for values whcih already exist. 191 | """ 192 | 193 | try: 194 | with self._db_instance: 195 | # update database 196 | 197 | if values['attribute'] != 'link': 198 | self._db_instance.execute(''' 199 | UPDATE Dictionary SET {0} = ? WHERE problem = ? AND language = 200 | (SELECT language from Languages where language = ?) 201 | '''.format(values['attribute']), 202 | (values['data'], 203 | values['problem'], 204 | values['language'])) 205 | else: 206 | self._db_instance.execute(''' 207 | UPDATE Links SET {0} = ? WHERE problem = ? AND language = 208 | (SELECT id from Languages where language = ?) 209 | '''.format(values['attribute']), 210 | (values['data'], 211 | values['problem'], 212 | values['language'])) 213 | 214 | except sqlite3.Error as error: 215 | print "A database error has ocurred: ", error 216 | sys.exit(1) 217 | 218 | def get_full_dump(self, language, required_tags): 219 | """ 220 | Gets a full dump of all entries which have the given language, and 221 | all of the given tags. The return value is a list of DumpEntry 222 | objects. 223 | """ 224 | required_tags = set(required_tags) 225 | try: 226 | with self._db_instance: 227 | # FIXME: This can probably be done better in pure-SQL, but 30 228 | # minutes of web searching hasn't revealed how 229 | all_problems = self._db_instance.execute( 230 | ''' 231 | SELECT id, language, problem, solution 232 | FROM Dictionary 233 | ''') 234 | 235 | results = [] 236 | for dict_id, language, problem, solution in all_problems.fetchall(): 237 | tag_results = self._db_instance.execute( 238 | ''' 239 | SELECT name FROM Tags 240 | INNER JOIN ItemsToTags 241 | ON ItemsToTags.tagID = Tags.id 242 | WHERE ItemsToTags.dictID = ? 243 | ''', (dict_id,)) 244 | tags = set(tag for (tag,) in tag_results) 245 | 246 | # This is the part of the query difficult to recreate in 247 | # SQL - ensuring that the tags on the entry are a superset 248 | # of the tags given by the user 249 | if required_tags <= tags: 250 | results.append(DumpEntry(language, tags, problem, solution)) 251 | 252 | return results 253 | except sqlite3.Error as error: 254 | print "A database error has ocurred: ", error 255 | sys.exit(1) 256 | 257 | ### Tags 258 | 259 | def get_tags(self, values): 260 | """ 261 | Retrieves / gets tags from DB. 262 | """ 263 | 264 | try: 265 | with self._db_instance: 266 | 267 | if 'problem' in values: 268 | results = self._db_instance.execute( 269 | ''' 270 | SELECT name FROM Tags 271 | INNER JOIN ItemsToTags ON Tags.id = ItemsToTags.tagID 272 | WHERE dictID = (SELECT id from Dictionary where language = ? 273 | and problem = ?) 274 | and language = ? 275 | ''', (values['language'], values['problem'], values['language'])) 276 | 277 | else: 278 | results = self._db_instance.execute( 279 | ''' 280 | SELECT name FROM Tags where language = ? 281 | ''', (values['language'], )) 282 | 283 | return results.fetchall() 284 | 285 | except sqlite3.Error as error: 286 | print " A database error has ocurred.", error 287 | sys.exit(1) 288 | 289 | def update_tags(self, values, update_type='add'): 290 | """ 291 | Updates the tag field of item (link or dict) 292 | """ 293 | 294 | try: 295 | with self._db_instance: 296 | if update_type == 'add': 297 | self._db_instance.execute(''' 298 | INSERT or IGNORE into Tags (name, language) 299 | VALUES (?, ?) 300 | ''', (values['tag_name'], values['language'])) 301 | 302 | self._db_instance.execute(''' 303 | INSERT or REPLACE into ItemsToTags (tagID, dictID) 304 | VALUES ( 305 | (SELECT id from Tags WHERE name = ? AND language = ?), 306 | (SELECT id from Dictionary WHERE problem = ? and language = ?) 307 | )''', (values['tag_name'], values['language'], 308 | values['problem'], values['language'])) 309 | 310 | #update_type = delete 311 | else: 312 | self._db_instance.execute(''' 313 | DELETE from ItemsToTags WHERE dictID = 314 | (SELECT id from Dictionary WHERE problem = ? and language = ?) 315 | AND tagID = (SELECT id from Tags WHERE name = ? AND language = ?) 316 | ''', (values['problem'], values['language'], 317 | values['tag_name'], values['language'])) 318 | 319 | except sqlite3.Error as error: 320 | print "A database error has ocurred: ", error 321 | sys.exit(1) 322 | 323 | def delete_tag(self, values): 324 | """ 325 | Deletes the tag and all associated items (link or dict). 326 | """ 327 | 328 | try: 329 | with self._db_instance: 330 | self._db_instance.execute( 331 | ''' 332 | DELETE from Tags WHERE name = ? AND language = 333 | (SELECT language from Languages where language = ?) 334 | ''', (values['tag_name'], values['language'])) 335 | 336 | except sqlite3.Error as error: 337 | print "A database error has ocurred ", error 338 | sys.exit(1) 339 | 340 | def retrieve_dict_per_tags(self, values): 341 | """ 342 | Retrieves dict content based on tags. 343 | """ 344 | 345 | try: 346 | with self._db_instance: 347 | 348 | results = self._db_instance.execute( 349 | ''' 350 | SELECT DISTINCT problem, solution FROM Dictionary 351 | INNER JOIN ItemsToTags On Dictionary.id = ItemsToTags.dictID 352 | INNER JOIN Tags On ItemsToTags.tagID = Tags.id 353 | WHERE Tags.language = ? and Tags.name LIKE ? 354 | ''', (values['language'], values['searchpattern'] + '%')) 355 | 356 | return selected_rows_to_list(results) 357 | 358 | except sqlite3.Error as error: 359 | print "A database error has ocurred ", error 360 | sys.exit(1) 361 | 362 | ### LINKS 363 | 364 | def upsert_links(self, values, operation_type='add'): 365 | """ 366 | Upserts (insert or update if exists) links into Link table. 367 | """ 368 | 369 | try: 370 | with self._db_instance: 371 | 372 | # add link to Links db if not exists 373 | self._db_instance.execute(''' 374 | INSERT OR IGNORE INTO Links (id, name, url, language) VALUES 375 | ((SELECT id from Links WHERE name = ? AND language = ?), ?, ?, ?) 376 | ''', (values['link_name'], values['original-lang'], values['link_name'], 377 | values['url'], values['language'])) 378 | 379 | if operation_type == 'upsert': 380 | self._db_instance.execute(''' 381 | UPDATE Links SET {0} = ? WHERE name = ? AND url = ? AND language = ? 382 | '''.format(values['attribute']), (values['data'], values['link_name'], 383 | values['url'], values['original-lang'])) 384 | 385 | except sqlite3.Error as error: 386 | print "A database error has ocurred: ", error 387 | sys.exit(1) 388 | 389 | def delete_links(self, values): 390 | """ 391 | Deletes links from Link table. 392 | """ 393 | 394 | try: 395 | with self._db_instance: 396 | self._db_instance.execute(''' 397 | DELETE from Links WHERE url = ? 398 | ''', (values['url'], )) 399 | 400 | except sqlite3.Error as error: 401 | print "A database error has ocurred: ", error 402 | sys.exit(1) 403 | 404 | def retrieve_links(self, values, selection_type): 405 | """ 406 | Retrieves links into Link table. 407 | """ 408 | try: 409 | with self._db_instance: 410 | if selection_type == 'open': 411 | 412 | selection = self._db_instance.execute(''' 413 | SELECT url from Links WHERE name = ? AND language = ? 414 | ''', (values['searchpattern'], values['language'])) 415 | return selection.fetchone() 416 | 417 | else: # display 418 | 419 | if selection_type == 'display': 420 | selection = self._db_instance.execute(''' 421 | SELECT name, url, language from Links WHERE name LIKE ? 422 | ''', (values['searchpattern'] + '%', )) 423 | 424 | elif selection_type == 'lang-display': # lang display 425 | selection = self._db_instance.execute(''' 426 | SELECT name, url from Links WHERE name LIKE ? 427 | AND language = ? 428 | ''', (values['searchpattern'] + '%', values['language'])) 429 | 430 | selection_list = selected_rows_to_list(selection) 431 | return selection_list 432 | 433 | except sqlite3.Error as error: 434 | print "A database error has ocurred: ", error 435 | sys.exit(1) 436 | 437 | def upsert_solution(self, values): 438 | """ 439 | Upserts (insert or update if exists) code into the DB. 440 | """ 441 | 442 | try: 443 | with self._db_instance: 444 | 445 | #add language to lang db if not exists 446 | self._db_instance.execute(''' 447 | INSERT OR IGNORE INTO Languages (language, suffix) VALUES (?, "") 448 | ''', (values['language'], )) 449 | 450 | self._db_instance.execute(''' 451 | UPDATE Dictionary SET solution = ? WHERE problem = ? AND language = 452 | (SELECT language from Languages where language = ?) 453 | ''', (values['data'], 454 | values['problem'], 455 | values['language'])) 456 | 457 | self._db_instance.execute(''' 458 | INSERT or IGNORE into Dictionary (id, language, problem, solution) 459 | VALUES((SELECT id from Dictionary where problem = ? AND language = 460 | (SELECT language from Languages where language = ?)) 461 | ,(SELECT language from Languages where language = ?), ?, ?) 462 | ''', (values['problem'], 463 | values['language'], 464 | values['language'], 465 | values['problem'], 466 | values['data'])) 467 | 468 | except sqlite3.Error as error: 469 | print "A database error has ocurred: ", error 470 | sys.exit(1) 471 | 472 | def add_content(self, values, lang_name, insert_type="normal"): 473 | """ 474 | Adds content to the database. Tries to insert and updates if 475 | row already exists. 476 | """ 477 | 478 | # backup database file 479 | if insert_type == "from_file": 480 | try: 481 | shutil.copy2(self.db_path + "/codedict_db.DB", 482 | self.db_path + "/BACKUP_codedict_db.DB") 483 | except (shutil.Error, IOError, OSError) as error: 484 | print "Error while backing up database.", error 485 | print "Continuing ..." 486 | 487 | try: 488 | with self._db_instance: 489 | dict_cursor = self._db_instance.cursor() 490 | tags_cursor = self._db_instance.cursor() 491 | #add language to lang db if not exists 492 | self._db_instance.execute(''' 493 | INSERT OR IGNORE INTO Languages (language, suffix) VALUES (?, "") 494 | ''', (lang_name, )) 495 | 496 | for new_row in values: 497 | 498 | dict_cursor.execute(''' 499 | INSERT or REPLACE into Dictionary 500 | (id, language, problem, solution) 501 | VALUES((SELECT id from Dictionary where problem = ? AND language = 502 | (SELECT language from Languages where language = ?)), 503 | (SELECT language from Languages where language = ?), ?, ?) 504 | ''', (new_row[1], lang_name, lang_name, new_row[1], new_row[2])) 505 | 506 | tags_list = process_input_tags(new_row[0]) 507 | dict_id = dict_cursor.lastrowid 508 | 509 | self._db_instance.execute(''' 510 | DELETE from ItemsToTags where dictID = ? 511 | ''', (dict_id,)) 512 | 513 | for tag in tags_list: 514 | tags_cursor.execute(''' 515 | INSERT OR REPLACE INTO Tags (id, name, language) VALUES ( 516 | (SELECT id from Tags WHERE name = ? and language = ?), 517 | ?, (SELECT language from Languages where language = ?)) 518 | ''', (tag.strip(), lang_name, tag.strip(), lang_name)) 519 | 520 | tag_id = tags_cursor.lastrowid 521 | 522 | self._db_instance.execute(''' 523 | INSERT OR IGNORE into ItemsToTags (tagID, dictID) VALUES (?, ?) 524 | ''', (tag_id, dict_id)) 525 | 526 | except sqlite3.Error as error: 527 | print "A database error has ocurred: ", error 528 | sys.exit(1) 529 | 530 | def retrieve_content(self, location, selection_type): 531 | """ 532 | Retrieves content, packs them in indexed tuples if needed 533 | and sends results back to calling function. 534 | """ 535 | 536 | db_selection = self._select_from_db(location, selection_type) 537 | if not selection_type == "code": 538 | selection_result = selected_rows_to_list(db_selection) 539 | else: 540 | selection_result = db_selection.fetchone() 541 | return selection_result # returns False if no rows were selected 542 | 543 | def _select_from_db(self, location, selection_type): 544 | """ 545 | Selects from DB. Runs correct query. 546 | """ 547 | 548 | if selection_type not in ('language', 'basic', 'code'): 549 | print "DB received no valid selection type." 550 | sys.exit(1) 551 | 552 | try: 553 | with self._db_instance: 554 | 555 | if selection_type == "basic": 556 | 557 | selection = self._db_instance.execute(''' 558 | SELECT language, problem, solution FROM Dictionary WHERE problem LIKE ? 559 | ''', (location['searchpattern'] + '%', )) 560 | 561 | elif selection_type == "language": 562 | 563 | selection = self._db_instance.execute(''' 564 | SELECT problem, solution FROM Dictionary WHERE language = 565 | (SELECT language from Languages where language = ?) 566 | ''', (location['language'], )) 567 | 568 | elif selection_type == "code": 569 | 570 | selection = self._db_instance.execute(''' 571 | SELECT solution FROM Dictionary WHERE problem = ? and language = 572 | (SELECT language from Languages where language = ?) 573 | ''', (location['searchpattern'], location['language'])) 574 | 575 | return selection 576 | 577 | except sqlite3.Error as error: 578 | print "A database error has ocurred: ", error 579 | sys.exit(1) 580 | 581 | 582 | def selected_rows_to_list(all_rows): 583 | """ 584 | Packs all results from a SELECT statement into a list of tuples 585 | with index attached (at first position). 586 | """ 587 | 588 | result_list = [] 589 | for count, row in enumerate(all_rows): 590 | result_list.append((count + 1, ) + row) 591 | if result_list: 592 | return result_list 593 | else: 594 | return False 595 | 596 | 597 | def determine_db_path(): 598 | """ 599 | Determines where the DB file is located. If executable is frozen the location 600 | differentiates. 601 | """ 602 | 603 | if getattr(sys, 'frozen', False): 604 | # The application is frozen 605 | datadir = os.path.dirname(sys.executable) 606 | else: 607 | # The application is not frozen 608 | datadir = os.path.dirname(__file__) 609 | 610 | return datadir + '/res' 611 | 612 | 613 | def establish_db_connection(db_path): 614 | """ 615 | Establishes the connection to the DB. 616 | """ 617 | 618 | try: 619 | return sqlite3.connect(db_path) 620 | 621 | except sqlite3.Error as error: 622 | print "A database error has ocurred: ", error 623 | return False 624 | 625 | 626 | def process_input_tags(all_tags): 627 | """ 628 | Gets input for the tags field, validates them and returns all. 629 | """ 630 | 631 | if ";" in all_tags: 632 | tag_list = all_tags.split(";") 633 | else: 634 | tag_list = [all_tags] 635 | return tag_list 636 | 637 | -------------------------------------------------------------------------------- /source/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BastiPaeltz/codedict/5830cc277c5d0dbcd62d5d86217383fad4d0f207/source/lib/__init__.py -------------------------------------------------------------------------------- /source/lib/docopt.py: -------------------------------------------------------------------------------- 1 | """Pythonic command-line interface parser that will make you smile. 2 | 3 | * http://docopt.org 4 | * Repository and issue-tracker: https://github.com/docopt/docopt 5 | * Licensed under terms of MIT license (see LICENSE-MIT) 6 | * Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com 7 | 8 | """ 9 | import sys 10 | import re 11 | 12 | 13 | __all__ = ['docopt'] 14 | __version__ = '0.6.1' 15 | 16 | 17 | class DocoptLanguageError(Exception): 18 | 19 | """Error in construction of usage-message by developer.""" 20 | 21 | 22 | class DocoptExit(SystemExit): 23 | 24 | """Exit in case user invoked program with incorrect arguments.""" 25 | 26 | usage = '' 27 | 28 | def __init__(self, message=''): 29 | SystemExit.__init__(self, (message + '\n' + self.usage).strip()) 30 | 31 | 32 | class Pattern(object): 33 | 34 | def __eq__(self, other): 35 | return repr(self) == repr(other) 36 | 37 | def __hash__(self): 38 | return hash(repr(self)) 39 | 40 | def fix(self): 41 | self.fix_identities() 42 | self.fix_repeating_arguments() 43 | return self 44 | 45 | def fix_identities(self, uniq=None): 46 | """Make pattern-tree tips point to same object if they are equal.""" 47 | if not hasattr(self, 'children'): 48 | return self 49 | uniq = list(set(self.flat())) if uniq is None else uniq 50 | for i, child in enumerate(self.children): 51 | if not hasattr(child, 'children'): 52 | assert child in uniq 53 | self.children[i] = uniq[uniq.index(child)] 54 | else: 55 | child.fix_identities(uniq) 56 | 57 | def fix_repeating_arguments(self): 58 | """Fix elements that should accumulate/increment values.""" 59 | either = [list(child.children) for child in transform(self).children] 60 | for case in either: 61 | for e in [child for child in case if case.count(child) > 1]: 62 | if type(e) is Argument or type(e) is Option and e.argcount: 63 | if e.value is None: 64 | e.value = [] 65 | elif type(e.value) is not list: 66 | e.value = e.value.split() 67 | if type(e) is Command or type(e) is Option and e.argcount == 0: 68 | e.value = 0 69 | return self 70 | 71 | 72 | def transform(pattern): 73 | """Expand pattern into an (almost) equivalent one, but with single Either. 74 | 75 | Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d) 76 | Quirks: [-a] => (-a), (-a...) => (-a -a) 77 | 78 | """ 79 | result = [] 80 | groups = [[pattern]] 81 | while groups: 82 | children = groups.pop(0) 83 | parents = [Required, Optional, OptionsShortcut, Either, OneOrMore] 84 | if any(t in map(type, children) for t in parents): 85 | child = [c for c in children if type(c) in parents][0] 86 | children.remove(child) 87 | if type(child) is Either: 88 | for c in child.children: 89 | groups.append([c] + children) 90 | elif type(child) is OneOrMore: 91 | groups.append(child.children * 2 + children) 92 | else: 93 | groups.append(child.children + children) 94 | else: 95 | result.append(children) 96 | return Either(*[Required(*e) for e in result]) 97 | 98 | 99 | class LeafPattern(Pattern): 100 | 101 | """Leaf/terminal node of a pattern tree.""" 102 | 103 | def __init__(self, name, value=None): 104 | self.name, self.value = name, value 105 | 106 | def __repr__(self): 107 | return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value) 108 | 109 | def flat(self, *types): 110 | return [self] if not types or type(self) in types else [] 111 | 112 | def match(self, left, collected=None): 113 | collected = [] if collected is None else collected 114 | pos, match = self.single_match(left) 115 | if match is None: 116 | return False, left, collected 117 | left_ = left[:pos] + left[pos + 1:] 118 | same_name = [a for a in collected if a.name == self.name] 119 | if type(self.value) in (int, list): 120 | if type(self.value) is int: 121 | increment = 1 122 | else: 123 | increment = ([match.value] if type(match.value) is str 124 | else match.value) 125 | if not same_name: 126 | match.value = increment 127 | return True, left_, collected + [match] 128 | same_name[0].value += increment 129 | return True, left_, collected 130 | return True, left_, collected + [match] 131 | 132 | 133 | class BranchPattern(Pattern): 134 | 135 | """Branch/inner node of a pattern tree.""" 136 | 137 | def __init__(self, *children): 138 | self.children = list(children) 139 | 140 | def __repr__(self): 141 | return '%s(%s)' % (self.__class__.__name__, 142 | ', '.join(repr(a) for a in self.children)) 143 | 144 | def flat(self, *types): 145 | if type(self) in types: 146 | return [self] 147 | return sum([child.flat(*types) for child in self.children], []) 148 | 149 | 150 | class Argument(LeafPattern): 151 | 152 | def single_match(self, left): 153 | for n, pattern in enumerate(left): 154 | if type(pattern) is Argument: 155 | return n, Argument(self.name, pattern.value) 156 | return None, None 157 | 158 | @classmethod 159 | def parse(class_, source): 160 | name = re.findall('(<\S*?>)', source)[0] 161 | value = re.findall('\[default: (.*)\]', source, flags=re.I) 162 | return class_(name, value[0] if value else None) 163 | 164 | 165 | class Command(Argument): 166 | 167 | def __init__(self, name, value=False): 168 | self.name, self.value = name, value 169 | 170 | def single_match(self, left): 171 | for n, pattern in enumerate(left): 172 | if type(pattern) is Argument: 173 | if pattern.value == self.name: 174 | return n, Command(self.name, True) 175 | else: 176 | break 177 | return None, None 178 | 179 | 180 | class Option(LeafPattern): 181 | 182 | def __init__(self, short=None, long=None, argcount=0, value=False): 183 | assert argcount in (0, 1) 184 | self.short, self.long, self.argcount = short, long, argcount 185 | self.value = None if value is False and argcount else value 186 | 187 | @classmethod 188 | def parse(class_, option_description): 189 | short, long, argcount, value = None, None, 0, False 190 | options, _, description = option_description.strip().partition(' ') 191 | options = options.replace(',', ' ').replace('=', ' ') 192 | for s in options.split(): 193 | if s.startswith('--'): 194 | long = s 195 | elif s.startswith('-'): 196 | short = s 197 | else: 198 | argcount = 1 199 | if argcount: 200 | matched = re.findall('\[default: (.*)\]', description, flags=re.I) 201 | value = matched[0] if matched else None 202 | return class_(short, long, argcount, value) 203 | 204 | def single_match(self, left): 205 | for n, pattern in enumerate(left): 206 | if self.name == pattern.name: 207 | return n, pattern 208 | return None, None 209 | 210 | @property 211 | def name(self): 212 | return self.long or self.short 213 | 214 | def __repr__(self): 215 | return 'Option(%r, %r, %r, %r)' % (self.short, self.long, 216 | self.argcount, self.value) 217 | 218 | 219 | class Required(BranchPattern): 220 | 221 | def match(self, left, collected=None): 222 | collected = [] if collected is None else collected 223 | l = left 224 | c = collected 225 | for pattern in self.children: 226 | matched, l, c = pattern.match(l, c) 227 | if not matched: 228 | return False, left, collected 229 | return True, l, c 230 | 231 | 232 | class Optional(BranchPattern): 233 | 234 | def match(self, left, collected=None): 235 | collected = [] if collected is None else collected 236 | for pattern in self.children: 237 | m, left, collected = pattern.match(left, collected) 238 | return True, left, collected 239 | 240 | 241 | class OptionsShortcut(Optional): 242 | 243 | """Marker/placeholder for [options] shortcut.""" 244 | 245 | 246 | class OneOrMore(BranchPattern): 247 | 248 | def match(self, left, collected=None): 249 | assert len(self.children) == 1 250 | collected = [] if collected is None else collected 251 | l = left 252 | c = collected 253 | l_ = None 254 | matched = True 255 | times = 0 256 | while matched: 257 | # could it be that something didn't match but changed l or c? 258 | matched, l, c = self.children[0].match(l, c) 259 | times += 1 if matched else 0 260 | if l_ == l: 261 | break 262 | l_ = l 263 | if times >= 1: 264 | return True, l, c 265 | return False, left, collected 266 | 267 | 268 | class Either(BranchPattern): 269 | 270 | def match(self, left, collected=None): 271 | collected = [] if collected is None else collected 272 | outcomes = [] 273 | for pattern in self.children: 274 | matched, _, _ = outcome = pattern.match(left, collected) 275 | if matched: 276 | outcomes.append(outcome) 277 | if outcomes: 278 | return min(outcomes, key=lambda outcome: len(outcome[1])) 279 | return False, left, collected 280 | 281 | 282 | class Tokens(list): 283 | 284 | def __init__(self, source, error=DocoptExit): 285 | self += source.split() if hasattr(source, 'split') else source 286 | self.error = error 287 | 288 | @staticmethod 289 | def from_pattern(source): 290 | source = re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source) 291 | source = [s for s in re.split('\s+|(\S*<.*?>)', source) if s] 292 | return Tokens(source, error=DocoptLanguageError) 293 | 294 | def move(self): 295 | return self.pop(0) if len(self) else None 296 | 297 | def current(self): 298 | return self[0] if len(self) else None 299 | 300 | 301 | def parse_long(tokens, options): 302 | """long ::= '--' chars [ ( ' ' | '=' ) chars ] ;""" 303 | long, eq, value = tokens.move().partition('=') 304 | assert long.startswith('--') 305 | value = None if eq == value == '' else value 306 | similar = [o for o in options if o.long == long] 307 | if tokens.error is DocoptExit and similar == []: # if no exact match 308 | similar = [o for o in options if o.long and o.long.startswith(long)] 309 | if len(similar) > 1: # might be simply specified ambiguously 2+ times? 310 | raise tokens.error('%s is not a unique prefix: %s?' % 311 | (long, ', '.join(o.long for o in similar))) 312 | elif len(similar) < 1: 313 | argcount = 1 if eq == '=' else 0 314 | o = Option(None, long, argcount) 315 | options.append(o) 316 | if tokens.error is DocoptExit: 317 | o = Option(None, long, argcount, value if argcount else True) 318 | else: 319 | o = Option(similar[0].short, similar[0].long, 320 | similar[0].argcount, similar[0].value) 321 | if o.argcount == 0: 322 | if value is not None: 323 | raise tokens.error('%s must not have an argument' % o.long) 324 | else: 325 | if value is None: 326 | if tokens.current() in [None, '--']: 327 | raise tokens.error('%s requires argument' % o.long) 328 | value = tokens.move() 329 | if tokens.error is DocoptExit: 330 | o.value = value if value is not None else True 331 | return [o] 332 | 333 | 334 | def parse_shorts(tokens, options): 335 | """shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;""" 336 | token = tokens.move() 337 | assert token.startswith('-') and not token.startswith('--') 338 | left = token.lstrip('-') 339 | parsed = [] 340 | while left != '': 341 | short, left = '-' + left[0], left[1:] 342 | similar = [o for o in options if o.short == short] 343 | if len(similar) > 1: 344 | raise tokens.error('%s is specified ambiguously %d times' % 345 | (short, len(similar))) 346 | elif len(similar) < 1: 347 | o = Option(short, None, 0) 348 | options.append(o) 349 | if tokens.error is DocoptExit: 350 | o = Option(short, None, 0, True) 351 | else: # why copying is necessary here? 352 | o = Option(short, similar[0].long, 353 | similar[0].argcount, similar[0].value) 354 | value = None 355 | if o.argcount != 0: 356 | if left == '': 357 | if tokens.current() in [None, '--']: 358 | raise tokens.error('%s requires argument' % short) 359 | value = tokens.move() 360 | else: 361 | value = left 362 | left = '' 363 | if tokens.error is DocoptExit: 364 | o.value = value if value is not None else True 365 | parsed.append(o) 366 | return parsed 367 | 368 | 369 | def parse_pattern(source, options): 370 | tokens = Tokens.from_pattern(source) 371 | result = parse_expr(tokens, options) 372 | if tokens.current() is not None: 373 | raise tokens.error('unexpected ending: %r' % ' '.join(tokens)) 374 | return Required(*result) 375 | 376 | 377 | def parse_expr(tokens, options): 378 | """expr ::= seq ( '|' seq )* ;""" 379 | seq = parse_seq(tokens, options) 380 | if tokens.current() != '|': 381 | return seq 382 | result = [Required(*seq)] if len(seq) > 1 else seq 383 | while tokens.current() == '|': 384 | tokens.move() 385 | seq = parse_seq(tokens, options) 386 | result += [Required(*seq)] if len(seq) > 1 else seq 387 | return [Either(*result)] if len(result) > 1 else result 388 | 389 | 390 | def parse_seq(tokens, options): 391 | """seq ::= ( atom [ '...' ] )* ;""" 392 | result = [] 393 | while tokens.current() not in [None, ']', ')', '|']: 394 | atom = parse_atom(tokens, options) 395 | if tokens.current() == '...': 396 | atom = [OneOrMore(*atom)] 397 | tokens.move() 398 | result += atom 399 | return result 400 | 401 | 402 | def parse_atom(tokens, options): 403 | """atom ::= '(' expr ')' | '[' expr ']' | 'options' 404 | | long | shorts | argument | command ; 405 | """ 406 | token = tokens.current() 407 | result = [] 408 | if token in '([': 409 | tokens.move() 410 | matching, pattern = {'(': [')', Required], '[': [']', Optional]}[token] 411 | result = pattern(*parse_expr(tokens, options)) 412 | if tokens.move() != matching: 413 | raise tokens.error("unmatched '%s'" % token) 414 | return [result] 415 | elif token == 'options': 416 | tokens.move() 417 | return [OptionsShortcut()] 418 | elif token.startswith('--') and token != '--': 419 | return parse_long(tokens, options) 420 | elif token.startswith('-') and token not in ('-', '--'): 421 | return parse_shorts(tokens, options) 422 | elif token.startswith('<') and token.endswith('>') or token.isupper(): 423 | return [Argument(tokens.move())] 424 | else: 425 | return [Command(tokens.move())] 426 | 427 | 428 | def parse_argv(tokens, options, options_first=False): 429 | """Parse command-line argument vector. 430 | 431 | If options_first: 432 | argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ; 433 | else: 434 | argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ; 435 | 436 | """ 437 | parsed = [] 438 | while tokens.current() is not None: 439 | if tokens.current() == '--': 440 | return parsed + [Argument(None, v) for v in tokens] 441 | elif tokens.current().startswith('--'): 442 | parsed += parse_long(tokens, options) 443 | elif tokens.current().startswith('-') and tokens.current() != '-': 444 | parsed += parse_shorts(tokens, options) 445 | elif options_first: 446 | return parsed + [Argument(None, v) for v in tokens] 447 | else: 448 | parsed.append(Argument(None, tokens.move())) 449 | return parsed 450 | 451 | 452 | def parse_defaults(doc): 453 | defaults = [] 454 | for s in parse_section('options:', doc): 455 | # FIXME corner case "bla: options: --foo" 456 | _, _, s = s.partition(':') # get rid of "options:" 457 | split = re.split('\n[ \t]*(-\S+?)', '\n' + s)[1:] 458 | split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])] 459 | options = [Option.parse(s) for s in split if s.startswith('-')] 460 | defaults += options 461 | return defaults 462 | 463 | 464 | def parse_section(name, source): 465 | pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)', 466 | re.IGNORECASE | re.MULTILINE) 467 | return [s.strip() for s in pattern.findall(source)] 468 | 469 | 470 | def formal_usage(section): 471 | _, _, section = section.partition(':') # drop "usage:" 472 | pu = section.split() 473 | return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )' 474 | 475 | 476 | def extras(help, version, options, doc): 477 | if help and any((o.name in ('-h', '--help')) and o.value for o in options): 478 | print(doc.strip("\n")) 479 | sys.exit() 480 | if version and any(o.name == '--version' and o.value for o in options): 481 | print(version) 482 | sys.exit() 483 | 484 | 485 | class Dict(dict): 486 | def __repr__(self): 487 | return '{%s}' % ',\n '.join('%r: %r' % i for i in sorted(self.items())) 488 | 489 | 490 | def docopt(doc, argv=None, help=True, version=None, options_first=False): 491 | """Parse `argv` based on command-line interface described in `doc`. 492 | 493 | `docopt` creates your command-line interface based on its 494 | description that you pass as `doc`. Such description can contain 495 | --options, , commands, which could be 496 | [optional], (required), (mutually | exclusive) or repeated... 497 | 498 | Parameters 499 | ---------- 500 | doc : str 501 | Description of your command-line interface. 502 | argv : list of str, optional 503 | Argument vector to be parsed. sys.argv[1:] is used if not 504 | provided. 505 | help : bool (default: True) 506 | Set to False to disable automatic help on -h or --help 507 | options. 508 | version : any object 509 | If passed, the object will be printed if --version is in 510 | `argv`. 511 | options_first : bool (default: False) 512 | Set to True to require options precede positional arguments, 513 | i.e. to forbid options and positional arguments intermix. 514 | 515 | Returns 516 | ------- 517 | args : dict 518 | A dictionary, where keys are names of command-line elements 519 | such as e.g. "--verbose" and "", and values are the 520 | parsed values of those elements. 521 | 522 | Example 523 | ------- 524 | >>> from docopt import docopt 525 | >>> doc = ''' 526 | ... Usage: 527 | ... my_program tcp [--timeout=] 528 | ... my_program serial [--baud=] [--timeout=] 529 | ... my_program (-h | --help | --version) 530 | ... 531 | ... Options: 532 | ... -h, --help Show this screen and exit. 533 | ... --baud= Baudrate [default: 9600] 534 | ... ''' 535 | >>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30'] 536 | >>> docopt(doc, argv) 537 | {'--baud': '9600', 538 | '--help': False, 539 | '--timeout': '30', 540 | '--version': False, 541 | '': '127.0.0.1', 542 | '': '80', 543 | 'serial': False, 544 | 'tcp': True} 545 | 546 | See also 547 | -------- 548 | * For video introduction see http://docopt.org 549 | * Full documentation is available in README.rst as well as online 550 | at https://github.com/docopt/docopt#readme 551 | 552 | """ 553 | argv = sys.argv[1:] if argv is None else argv 554 | 555 | usage_sections = parse_section('usage:', doc) 556 | if len(usage_sections) == 0: 557 | raise DocoptLanguageError('"usage:" (case-insensitive) not found.') 558 | if len(usage_sections) > 1: 559 | raise DocoptLanguageError('More than one "usage:" (case-insensitive).') 560 | DocoptExit.usage = usage_sections[0] 561 | 562 | options = parse_defaults(doc) 563 | pattern = parse_pattern(formal_usage(DocoptExit.usage), options) 564 | # [default] syntax for argument is disabled 565 | #for a in pattern.flat(Argument): 566 | # same_name = [d for d in arguments if d.name == a.name] 567 | # if same_name: 568 | # a.value = same_name[0].value 569 | argv = parse_argv(Tokens(argv), list(options), options_first) 570 | pattern_options = set(pattern.flat(Option)) 571 | for options_shortcut in pattern.flat(OptionsShortcut): 572 | doc_options = parse_defaults(doc) 573 | options_shortcut.children = list(set(doc_options) - pattern_options) 574 | #if any_options: 575 | # options_shortcut.children += [Option(o.short, o.long, o.argcount) 576 | # for o in argv if type(o) is Option] 577 | extras(help, version, argv, doc) 578 | matched, left, collected = pattern.fix().match(argv) 579 | if matched and left == []: # better error message if left? 580 | return Dict((a.name, a.value) for a in (pattern.flat() + collected)) 581 | raise DocoptExit() -------------------------------------------------------------------------------- /source/lib/prettytable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2009-2013, Luke Maurits 4 | # All rights reserved. 5 | # With contributions from: 6 | # * Chris Clark 7 | # * Klein Stephane 8 | # 9 | # Redistribution and use in source and binary forms, with or without 10 | # modification, are permitted provided that the following conditions are met: 11 | # 12 | # * Redistributions of source code must retain the above copyright notice, 13 | # this list of conditions and the following disclaimer. 14 | # * Redistributions in binary form must reproduce the above copyright notice, 15 | # this list of conditions and the following disclaimer in the documentation 16 | # and/or other materials provided with the distribution. 17 | # * The name of the author may not be used to endorse or promote products 18 | # derived from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 24 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | # POSSIBILITY OF SUCH DAMAGE. 31 | 32 | __version__ = "0.7.2" 33 | 34 | import copy 35 | import csv 36 | import random 37 | import re 38 | import sys 39 | import textwrap 40 | import itertools 41 | import unicodedata 42 | 43 | py3k = sys.version_info[0] >= 3 44 | if py3k: 45 | unicode = str 46 | basestring = str 47 | itermap = map 48 | iterzip = zip 49 | uni_chr = chr 50 | from html.parser import HTMLParser 51 | else: 52 | itermap = itertools.imap 53 | iterzip = itertools.izip 54 | uni_chr = unichr 55 | from HTMLParser import HTMLParser 56 | 57 | if py3k and sys.version_info[1] >= 2: 58 | from html import escape 59 | else: 60 | from cgi import escape 61 | 62 | # hrule styles 63 | FRAME = 0 64 | ALL = 1 65 | NONE = 2 66 | HEADER = 3 67 | 68 | # Table styles 69 | DEFAULT = 10 70 | MSWORD_FRIENDLY = 11 71 | PLAIN_COLUMNS = 12 72 | RANDOM = 20 73 | 74 | _re = re.compile("\033\[[0-9;]*m") 75 | 76 | def _get_size(text): 77 | lines = text.split("\n") 78 | height = len(lines) 79 | width = max([_str_block_width(line) for line in lines]) 80 | return (width, height) 81 | 82 | class PrettyTable(object): 83 | 84 | def __init__(self, field_names=None, **kwargs): 85 | 86 | """Return a new PrettyTable instance 87 | 88 | Arguments: 89 | 90 | encoding - Unicode encoding scheme used to decode any encoded input 91 | field_names - list or tuple of field names 92 | fields - list or tuple of field names to include in displays 93 | start - index of first data row to include in output 94 | end - index of last data row to include in output PLUS ONE (list slice style) 95 | header - print a header showing field names (True or False) 96 | header_style - stylisation to apply to field names in header ("cap", "title", "upper", "lower" or None) 97 | border - print a border around the table (True or False) 98 | hrules - controls printing of horizontal rules after rows. Allowed values: FRAME, HEADER, ALL, NONE 99 | vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE 100 | int_format - controls formatting of integer data 101 | float_format - controls formatting of floating point data 102 | padding_width - number of spaces on either side of column data (only used if left and right paddings are None) 103 | left_padding_width - number of spaces on left hand side of column data 104 | right_padding_width - number of spaces on right hand side of column data 105 | vertical_char - single character string used to draw vertical lines 106 | horizontal_char - single character string used to draw horizontal lines 107 | junction_char - single character string used to draw line junctions 108 | sortby - name of field to sort rows by 109 | sort_key - sorting key function, applied to data points before sorting 110 | valign - default valign for each row (None, "t", "m" or "b") 111 | reversesort - True or False to sort in descending or ascending order""" 112 | 113 | self.encoding = kwargs.get("encoding", "UTF-8") 114 | 115 | # Data 116 | self._field_names = [] 117 | self._align = {} 118 | self._valign = {} 119 | self._max_width = {} 120 | self._rows = [] 121 | if field_names: 122 | self.field_names = field_names 123 | else: 124 | self._widths = [] 125 | 126 | # Options 127 | self._options = "start end fields header border sortby reversesort sort_key attributes format hrules vrules".split() 128 | self._options.extend("int_format float_format padding_width left_padding_width right_padding_width".split()) 129 | self._options.extend("vertical_char horizontal_char junction_char header_style valign xhtml print_empty".split()) 130 | for option in self._options: 131 | if option in kwargs: 132 | self._validate_option(option, kwargs[option]) 133 | else: 134 | kwargs[option] = None 135 | 136 | self._start = kwargs["start"] or 0 137 | self._end = kwargs["end"] or None 138 | self._fields = kwargs["fields"] or None 139 | 140 | if kwargs["header"] in (True, False): 141 | self._header = kwargs["header"] 142 | else: 143 | self._header = True 144 | self._header_style = kwargs["header_style"] or None 145 | if kwargs["border"] in (True, False): 146 | self._border = kwargs["border"] 147 | else: 148 | self._border = True 149 | self._hrules = kwargs["hrules"] or FRAME 150 | self._vrules = kwargs["vrules"] or ALL 151 | 152 | self._sortby = kwargs["sortby"] or None 153 | if kwargs["reversesort"] in (True, False): 154 | self._reversesort = kwargs["reversesort"] 155 | else: 156 | self._reversesort = False 157 | self._sort_key = kwargs["sort_key"] or (lambda x: x) 158 | 159 | self._int_format = kwargs["int_format"] or {} 160 | self._float_format = kwargs["float_format"] or {} 161 | self._padding_width = kwargs["padding_width"] or 1 162 | self._left_padding_width = kwargs["left_padding_width"] or None 163 | self._right_padding_width = kwargs["right_padding_width"] or None 164 | 165 | self._vertical_char = kwargs["vertical_char"] or self._unicode("|") 166 | self._horizontal_char = kwargs["horizontal_char"] or self._unicode("-") 167 | self._junction_char = kwargs["junction_char"] or self._unicode("+") 168 | 169 | if kwargs["print_empty"] in (True, False): 170 | self._print_empty = kwargs["print_empty"] 171 | else: 172 | self._print_empty = True 173 | self._format = kwargs["format"] or False 174 | self._xhtml = kwargs["xhtml"] or False 175 | self._attributes = kwargs["attributes"] or {} 176 | 177 | def _unicode(self, value): 178 | if not isinstance(value, basestring): 179 | value = str(value) 180 | if not isinstance(value, unicode): 181 | value = unicode(value, self.encoding, "strict") 182 | return value 183 | 184 | def _justify(self, text, width, align): 185 | excess = width - _str_block_width(text) 186 | if align == "l": 187 | return text + excess * " " 188 | elif align == "r": 189 | return excess * " " + text 190 | else: 191 | if excess % 2: 192 | # Uneven padding 193 | # Put more space on right if text is of odd length... 194 | if _str_block_width(text) % 2: 195 | return (excess//2)*" " + text + (excess//2 + 1)*" " 196 | # and more space on left if text is of even length 197 | else: 198 | return (excess//2 + 1)*" " + text + (excess//2)*" " 199 | # Why distribute extra space this way? To match the behaviour of 200 | # the inbuilt str.center() method. 201 | else: 202 | # Equal padding on either side 203 | return (excess//2)*" " + text + (excess//2)*" " 204 | 205 | def __getattr__(self, name): 206 | 207 | if name == "rowcount": 208 | return len(self._rows) 209 | elif name == "colcount": 210 | if self._field_names: 211 | return len(self._field_names) 212 | elif self._rows: 213 | return len(self._rows[0]) 214 | else: 215 | return 0 216 | else: 217 | raise AttributeError(name) 218 | 219 | def __getitem__(self, index): 220 | 221 | new = PrettyTable() 222 | new.field_names = self.field_names 223 | for attr in self._options: 224 | setattr(new, "_"+attr, getattr(self, "_"+attr)) 225 | setattr(new, "_align", getattr(self, "_align")) 226 | if isinstance(index, slice): 227 | for row in self._rows[index]: 228 | new.add_row(row) 229 | elif isinstance(index, int): 230 | new.add_row(self._rows[index]) 231 | else: 232 | raise Exception("Index %s is invalid, must be an integer or slice" % str(index)) 233 | return new 234 | 235 | if py3k: 236 | def __str__(self): 237 | return self.__unicode__() 238 | else: 239 | def __str__(self): 240 | return self.__unicode__().encode(self.encoding) 241 | 242 | def __unicode__(self): 243 | return self.get_string() 244 | 245 | ############################## 246 | # ATTRIBUTE VALIDATORS # 247 | ############################## 248 | 249 | # The method _validate_option is all that should be used elsewhere in the code base to validate options. 250 | # It will call the appropriate validation method for that option. The individual validation methods should 251 | # never need to be called directly (although nothing bad will happen if they *are*). 252 | # Validation happens in TWO places. 253 | # Firstly, in the property setters defined in the ATTRIBUTE MANAGMENT section. 254 | # Secondly, in the _get_options method, where keyword arguments are mixed with persistent settings 255 | 256 | def _validate_option(self, option, val): 257 | if option in ("field_names"): 258 | self._validate_field_names(val) 259 | elif option in ("start", "end", "max_width", "padding_width", "left_padding_width", "right_padding_width", "format"): 260 | self._validate_nonnegative_int(option, val) 261 | elif option in ("sortby"): 262 | self._validate_field_name(option, val) 263 | elif option in ("sort_key"): 264 | self._validate_function(option, val) 265 | elif option in ("hrules"): 266 | self._validate_hrules(option, val) 267 | elif option in ("vrules"): 268 | self._validate_vrules(option, val) 269 | elif option in ("fields"): 270 | self._validate_all_field_names(option, val) 271 | elif option in ("header", "border", "reversesort", "xhtml", "print_empty"): 272 | self._validate_true_or_false(option, val) 273 | elif option in ("header_style"): 274 | self._validate_header_style(val) 275 | elif option in ("int_format"): 276 | self._validate_int_format(option, val) 277 | elif option in ("float_format"): 278 | self._validate_float_format(option, val) 279 | elif option in ("vertical_char", "horizontal_char", "junction_char"): 280 | self._validate_single_char(option, val) 281 | elif option in ("attributes"): 282 | self._validate_attributes(option, val) 283 | else: 284 | raise Exception("Unrecognised option: %s!" % option) 285 | 286 | def _validate_field_names(self, val): 287 | # Check for appropriate length 288 | if self._field_names: 289 | try: 290 | assert len(val) == len(self._field_names) 291 | except AssertionError: 292 | raise Exception("Field name list has incorrect number of values, (actual) %d!=%d (expected)" % (len(val), len(self._field_names))) 293 | if self._rows: 294 | try: 295 | assert len(val) == len(self._rows[0]) 296 | except AssertionError: 297 | raise Exception("Field name list has incorrect number of values, (actual) %d!=%d (expected)" % (len(val), len(self._rows[0]))) 298 | # Check for uniqueness 299 | try: 300 | assert len(val) == len(set(val)) 301 | except AssertionError: 302 | raise Exception("Field names must be unique!") 303 | 304 | def _validate_header_style(self, val): 305 | try: 306 | assert val in ("cap", "title", "upper", "lower", None) 307 | except AssertionError: 308 | raise Exception("Invalid header style, use cap, title, upper, lower or None!") 309 | 310 | def _validate_align(self, val): 311 | try: 312 | assert val in ["l","c","r"] 313 | except AssertionError: 314 | raise Exception("Alignment %s is invalid, use l, c or r!" % val) 315 | 316 | def _validate_valign(self, val): 317 | try: 318 | assert val in ["t","m","b",None] 319 | except AssertionError: 320 | raise Exception("Alignment %s is invalid, use t, m, b or None!" % val) 321 | 322 | def _validate_nonnegative_int(self, name, val): 323 | try: 324 | assert int(val) >= 0 325 | except AssertionError: 326 | raise Exception("Invalid value for %s: %s!" % (name, self._unicode(val))) 327 | 328 | def _validate_true_or_false(self, name, val): 329 | try: 330 | assert val in (True, False) 331 | except AssertionError: 332 | raise Exception("Invalid value for %s! Must be True or False." % name) 333 | 334 | def _validate_int_format(self, name, val): 335 | if val == "": 336 | return 337 | try: 338 | assert type(val) in (str, unicode) 339 | assert val.isdigit() 340 | except AssertionError: 341 | raise Exception("Invalid value for %s! Must be an integer format string." % name) 342 | 343 | def _validate_float_format(self, name, val): 344 | if val == "": 345 | return 346 | try: 347 | assert type(val) in (str, unicode) 348 | assert "." in val 349 | bits = val.split(".") 350 | assert len(bits) <= 2 351 | assert bits[0] == "" or bits[0].isdigit() 352 | assert bits[1] == "" or bits[1].isdigit() 353 | except AssertionError: 354 | raise Exception("Invalid value for %s! Must be a float format string." % name) 355 | 356 | def _validate_function(self, name, val): 357 | try: 358 | assert hasattr(val, "__call__") 359 | except AssertionError: 360 | raise Exception("Invalid value for %s! Must be a function." % name) 361 | 362 | def _validate_hrules(self, name, val): 363 | try: 364 | assert val in (ALL, FRAME, HEADER, NONE) 365 | except AssertionError: 366 | raise Exception("Invalid value for %s! Must be ALL, FRAME, HEADER or NONE." % name) 367 | 368 | def _validate_vrules(self, name, val): 369 | try: 370 | assert val in (ALL, FRAME, NONE) 371 | except AssertionError: 372 | raise Exception("Invalid value for %s! Must be ALL, FRAME, or NONE." % name) 373 | 374 | def _validate_field_name(self, name, val): 375 | try: 376 | assert (val in self._field_names) or (val is None) 377 | except AssertionError: 378 | raise Exception("Invalid field name: %s!" % val) 379 | 380 | def _validate_all_field_names(self, name, val): 381 | try: 382 | for x in val: 383 | self._validate_field_name(name, x) 384 | except AssertionError: 385 | raise Exception("fields must be a sequence of field names!") 386 | 387 | def _validate_single_char(self, name, val): 388 | try: 389 | assert _str_block_width(val) == 1 390 | except AssertionError: 391 | raise Exception("Invalid value for %s! Must be a string of length 1." % name) 392 | 393 | def _validate_attributes(self, name, val): 394 | try: 395 | assert isinstance(val, dict) 396 | except AssertionError: 397 | raise Exception("attributes must be a dictionary of name/value pairs!") 398 | 399 | ############################## 400 | # ATTRIBUTE MANAGEMENT # 401 | ############################## 402 | 403 | def _get_field_names(self): 404 | return self._field_names 405 | """The names of the fields 406 | 407 | Arguments: 408 | 409 | fields - list or tuple of field names""" 410 | def _set_field_names(self, val): 411 | val = [self._unicode(x) for x in val] 412 | self._validate_option("field_names", val) 413 | if self._field_names: 414 | old_names = self._field_names[:] 415 | self._field_names = val 416 | if self._align and old_names: 417 | for old_name, new_name in zip(old_names, val): 418 | self._align[new_name] = self._align[old_name] 419 | for old_name in old_names: 420 | if old_name not in self._align: 421 | self._align.pop(old_name) 422 | else: 423 | for field in self._field_names: 424 | self._align[field] = "c" 425 | if self._valign and old_names: 426 | for old_name, new_name in zip(old_names, val): 427 | self._valign[new_name] = self._valign[old_name] 428 | for old_name in old_names: 429 | if old_name not in self._valign: 430 | self._valign.pop(old_name) 431 | else: 432 | for field in self._field_names: 433 | self._valign[field] = "t" 434 | field_names = property(_get_field_names, _set_field_names) 435 | 436 | def _get_align(self): 437 | return self._align 438 | def _set_align(self, val): 439 | self._validate_align(val) 440 | for field in self._field_names: 441 | self._align[field] = val 442 | align = property(_get_align, _set_align) 443 | 444 | def _get_valign(self): 445 | return self._valign 446 | def _set_valign(self, val): 447 | self._validate_valign(val) 448 | for field in self._field_names: 449 | self._valign[field] = val 450 | valign = property(_get_valign, _set_valign) 451 | 452 | def _get_max_width(self): 453 | return self._max_width 454 | def _set_max_width(self, val): 455 | self._validate_option("max_width", val) 456 | for field in self._field_names: 457 | self._max_width[field] = val 458 | max_width = property(_get_max_width, _set_max_width) 459 | 460 | def _get_fields(self): 461 | """List or tuple of field names to include in displays 462 | 463 | Arguments: 464 | 465 | fields - list or tuple of field names to include in displays""" 466 | return self._fields 467 | def _set_fields(self, val): 468 | self._validate_option("fields", val) 469 | self._fields = val 470 | fields = property(_get_fields, _set_fields) 471 | 472 | def _get_start(self): 473 | """Start index of the range of rows to print 474 | 475 | Arguments: 476 | 477 | start - index of first data row to include in output""" 478 | return self._start 479 | 480 | def _set_start(self, val): 481 | self._validate_option("start", val) 482 | self._start = val 483 | start = property(_get_start, _set_start) 484 | 485 | def _get_end(self): 486 | """End index of the range of rows to print 487 | 488 | Arguments: 489 | 490 | end - index of last data row to include in output PLUS ONE (list slice style)""" 491 | return self._end 492 | def _set_end(self, val): 493 | self._validate_option("end", val) 494 | self._end = val 495 | end = property(_get_end, _set_end) 496 | 497 | def _get_sortby(self): 498 | """Name of field by which to sort rows 499 | 500 | Arguments: 501 | 502 | sortby - field name to sort by""" 503 | return self._sortby 504 | def _set_sortby(self, val): 505 | self._validate_option("sortby", val) 506 | self._sortby = val 507 | sortby = property(_get_sortby, _set_sortby) 508 | 509 | def _get_reversesort(self): 510 | """Controls direction of sorting (ascending vs descending) 511 | 512 | Arguments: 513 | 514 | reveresort - set to True to sort by descending order, or False to sort by ascending order""" 515 | return self._reversesort 516 | def _set_reversesort(self, val): 517 | self._validate_option("reversesort", val) 518 | self._reversesort = val 519 | reversesort = property(_get_reversesort, _set_reversesort) 520 | 521 | def _get_sort_key(self): 522 | """Sorting key function, applied to data points before sorting 523 | 524 | Arguments: 525 | 526 | sort_key - a function which takes one argument and returns something to be sorted""" 527 | return self._sort_key 528 | def _set_sort_key(self, val): 529 | self._validate_option("sort_key", val) 530 | self._sort_key = val 531 | sort_key = property(_get_sort_key, _set_sort_key) 532 | 533 | def _get_header(self): 534 | """Controls printing of table header with field names 535 | 536 | Arguments: 537 | 538 | header - print a header showing field names (True or False)""" 539 | return self._header 540 | def _set_header(self, val): 541 | self._validate_option("header", val) 542 | self._header = val 543 | header = property(_get_header, _set_header) 544 | 545 | def _get_header_style(self): 546 | """Controls stylisation applied to field names in header 547 | 548 | Arguments: 549 | 550 | header_style - stylisation to apply to field names in header ("cap", "title", "upper", "lower" or None)""" 551 | return self._header_style 552 | def _set_header_style(self, val): 553 | self._validate_header_style(val) 554 | self._header_style = val 555 | header_style = property(_get_header_style, _set_header_style) 556 | 557 | def _get_border(self): 558 | """Controls printing of border around table 559 | 560 | Arguments: 561 | 562 | border - print a border around the table (True or False)""" 563 | return self._border 564 | def _set_border(self, val): 565 | self._validate_option("border", val) 566 | self._border = val 567 | border = property(_get_border, _set_border) 568 | 569 | def _get_hrules(self): 570 | """Controls printing of horizontal rules after rows 571 | 572 | Arguments: 573 | 574 | hrules - horizontal rules style. Allowed values: FRAME, ALL, HEADER, NONE""" 575 | return self._hrules 576 | def _set_hrules(self, val): 577 | self._validate_option("hrules", val) 578 | self._hrules = val 579 | hrules = property(_get_hrules, _set_hrules) 580 | 581 | def _get_vrules(self): 582 | """Controls printing of vertical rules between columns 583 | 584 | Arguments: 585 | 586 | vrules - vertical rules style. Allowed values: FRAME, ALL, NONE""" 587 | return self._vrules 588 | def _set_vrules(self, val): 589 | self._validate_option("vrules", val) 590 | self._vrules = val 591 | vrules = property(_get_vrules, _set_vrules) 592 | 593 | def _get_int_format(self): 594 | """Controls formatting of integer data 595 | Arguments: 596 | 597 | int_format - integer format string""" 598 | return self._int_format 599 | def _set_int_format(self, val): 600 | # self._validate_option("int_format", val) 601 | for field in self._field_names: 602 | self._int_format[field] = val 603 | int_format = property(_get_int_format, _set_int_format) 604 | 605 | def _get_float_format(self): 606 | """Controls formatting of floating point data 607 | Arguments: 608 | 609 | float_format - floating point format string""" 610 | return self._float_format 611 | def _set_float_format(self, val): 612 | # self._validate_option("float_format", val) 613 | for field in self._field_names: 614 | self._float_format[field] = val 615 | float_format = property(_get_float_format, _set_float_format) 616 | 617 | def _get_padding_width(self): 618 | """The number of empty spaces between a column's edge and its content 619 | 620 | Arguments: 621 | 622 | padding_width - number of spaces, must be a positive integer""" 623 | return self._padding_width 624 | def _set_padding_width(self, val): 625 | self._validate_option("padding_width", val) 626 | self._padding_width = val 627 | padding_width = property(_get_padding_width, _set_padding_width) 628 | 629 | def _get_left_padding_width(self): 630 | """The number of empty spaces between a column's left edge and its content 631 | 632 | Arguments: 633 | 634 | left_padding - number of spaces, must be a positive integer""" 635 | return self._left_padding_width 636 | def _set_left_padding_width(self, val): 637 | self._validate_option("left_padding_width", val) 638 | self._left_padding_width = val 639 | left_padding_width = property(_get_left_padding_width, _set_left_padding_width) 640 | 641 | def _get_right_padding_width(self): 642 | """The number of empty spaces between a column's right edge and its content 643 | 644 | Arguments: 645 | 646 | right_padding - number of spaces, must be a positive integer""" 647 | return self._right_padding_width 648 | def _set_right_padding_width(self, val): 649 | self._validate_option("right_padding_width", val) 650 | self._right_padding_width = val 651 | right_padding_width = property(_get_right_padding_width, _set_right_padding_width) 652 | 653 | def _get_vertical_char(self): 654 | """The charcter used when printing table borders to draw vertical lines 655 | 656 | Arguments: 657 | 658 | vertical_char - single character string used to draw vertical lines""" 659 | return self._vertical_char 660 | def _set_vertical_char(self, val): 661 | val = self._unicode(val) 662 | self._validate_option("vertical_char", val) 663 | self._vertical_char = val 664 | vertical_char = property(_get_vertical_char, _set_vertical_char) 665 | 666 | def _get_horizontal_char(self): 667 | """The charcter used when printing table borders to draw horizontal lines 668 | 669 | Arguments: 670 | 671 | horizontal_char - single character string used to draw horizontal lines""" 672 | return self._horizontal_char 673 | def _set_horizontal_char(self, val): 674 | val = self._unicode(val) 675 | self._validate_option("horizontal_char", val) 676 | self._horizontal_char = val 677 | horizontal_char = property(_get_horizontal_char, _set_horizontal_char) 678 | 679 | def _get_junction_char(self): 680 | """The charcter used when printing table borders to draw line junctions 681 | 682 | Arguments: 683 | 684 | junction_char - single character string used to draw line junctions""" 685 | return self._junction_char 686 | def _set_junction_char(self, val): 687 | val = self._unicode(val) 688 | self._validate_option("vertical_char", val) 689 | self._junction_char = val 690 | junction_char = property(_get_junction_char, _set_junction_char) 691 | 692 | def _get_format(self): 693 | """Controls whether or not HTML tables are formatted to match styling options 694 | 695 | Arguments: 696 | 697 | format - True or False""" 698 | return self._format 699 | def _set_format(self, val): 700 | self._validate_option("format", val) 701 | self._format = val 702 | format = property(_get_format, _set_format) 703 | 704 | def _get_print_empty(self): 705 | """Controls whether or not empty tables produce a header and frame or just an empty string 706 | 707 | Arguments: 708 | 709 | print_empty - True or False""" 710 | return self._print_empty 711 | def _set_print_empty(self, val): 712 | self._validate_option("print_empty", val) 713 | self._print_empty = val 714 | print_empty = property(_get_print_empty, _set_print_empty) 715 | 716 | def _get_attributes(self): 717 | """A dictionary of HTML attribute name/value pairs to be included in the tag when printing HTML 718 | 719 | Arguments: 720 | 721 | attributes - dictionary of attributes""" 722 | return self._attributes 723 | def _set_attributes(self, val): 724 | self._validate_option("attributes", val) 725 | self._attributes = val 726 | attributes = property(_get_attributes, _set_attributes) 727 | 728 | ############################## 729 | # OPTION MIXER # 730 | ############################## 731 | 732 | def _get_options(self, kwargs): 733 | 734 | options = {} 735 | for option in self._options: 736 | if option in kwargs: 737 | self._validate_option(option, kwargs[option]) 738 | options[option] = kwargs[option] 739 | else: 740 | options[option] = getattr(self, "_"+option) 741 | return options 742 | 743 | ############################## 744 | # PRESET STYLE LOGIC # 745 | ############################## 746 | 747 | def set_style(self, style): 748 | 749 | if style == DEFAULT: 750 | self._set_default_style() 751 | elif style == MSWORD_FRIENDLY: 752 | self._set_msword_style() 753 | elif style == PLAIN_COLUMNS: 754 | self._set_columns_style() 755 | elif style == RANDOM: 756 | self._set_random_style() 757 | else: 758 | raise Exception("Invalid pre-set style!") 759 | 760 | def _set_default_style(self): 761 | 762 | self.header = True 763 | self.border = True 764 | self._hrules = FRAME 765 | self._vrules = ALL 766 | self.padding_width = 1 767 | self.left_padding_width = 1 768 | self.right_padding_width = 1 769 | self.vertical_char = "|" 770 | self.horizontal_char = "-" 771 | self.junction_char = "+" 772 | 773 | def _set_msword_style(self): 774 | 775 | self.header = True 776 | self.border = True 777 | self._hrules = NONE 778 | self.padding_width = 1 779 | self.left_padding_width = 1 780 | self.right_padding_width = 1 781 | self.vertical_char = "|" 782 | 783 | def _set_columns_style(self): 784 | 785 | self.header = True 786 | self.border = False 787 | self.padding_width = 1 788 | self.left_padding_width = 0 789 | self.right_padding_width = 8 790 | 791 | def _set_random_style(self): 792 | 793 | # Just for fun! 794 | self.header = random.choice((True, False)) 795 | self.border = random.choice((True, False)) 796 | self._hrules = random.choice((ALL, FRAME, HEADER, NONE)) 797 | self._vrules = random.choice((ALL, FRAME, NONE)) 798 | self.left_padding_width = random.randint(0,5) 799 | self.right_padding_width = random.randint(0,5) 800 | self.vertical_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") 801 | self.horizontal_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") 802 | self.junction_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") 803 | 804 | ############################## 805 | # DATA INPUT METHODS # 806 | ############################## 807 | 808 | def add_row(self, row): 809 | 810 | """Add a row to the table 811 | 812 | Arguments: 813 | 814 | row - row of data, should be a list with as many elements as the table 815 | has fields""" 816 | 817 | if self._field_names and len(row) != len(self._field_names): 818 | raise Exception("Row has incorrect number of values, (actual) %d!=%d (expected)" %(len(row),len(self._field_names))) 819 | if not self._field_names: 820 | self.field_names = [("Field %d" % (n+1)) for n in range(0,len(row))] 821 | self._rows.append(list(row)) 822 | 823 | def del_row(self, row_index): 824 | 825 | """Delete a row to the table 826 | 827 | Arguments: 828 | 829 | row_index - The index of the row you want to delete. Indexing starts at 0.""" 830 | 831 | if row_index > len(self._rows)-1: 832 | raise Exception("Cant delete row at index %d, table only has %d rows!" % (row_index, len(self._rows))) 833 | del self._rows[row_index] 834 | 835 | def add_column(self, fieldname, column, align="c", valign="t"): 836 | 837 | """Add a column to the table. 838 | 839 | Arguments: 840 | 841 | fieldname - name of the field to contain the new column of data 842 | column - column of data, should be a list with as many elements as the 843 | table has rows 844 | align - desired alignment for this column - "l" for left, "c" for centre and "r" for right 845 | valign - desired vertical alignment for new columns - "t" for top, "m" for middle and "b" for bottom""" 846 | 847 | if len(self._rows) in (0, len(column)): 848 | self._validate_align(align) 849 | self._validate_valign(valign) 850 | self._field_names.append(fieldname) 851 | self._align[fieldname] = align 852 | self._valign[fieldname] = valign 853 | for i in range(0, len(column)): 854 | if len(self._rows) < i+1: 855 | self._rows.append([]) 856 | self._rows[i].append(column[i]) 857 | else: 858 | raise Exception("Column length %d does not match number of rows %d!" % (len(column), len(self._rows))) 859 | 860 | def clear_rows(self): 861 | 862 | """Delete all rows from the table but keep the current field names""" 863 | 864 | self._rows = [] 865 | 866 | def clear(self): 867 | 868 | """Delete all rows and field names from the table, maintaining nothing but styling options""" 869 | 870 | self._rows = [] 871 | self._field_names = [] 872 | self._widths = [] 873 | 874 | ############################## 875 | # MISC PUBLIC METHODS # 876 | ############################## 877 | 878 | def copy(self): 879 | return copy.deepcopy(self) 880 | 881 | ############################## 882 | # MISC PRIVATE METHODS # 883 | ############################## 884 | 885 | def _format_value(self, field, value): 886 | if isinstance(value, int) and field in self._int_format: 887 | value = self._unicode(("%%%sd" % self._int_format[field]) % value) 888 | elif isinstance(value, float) and field in self._float_format: 889 | value = self._unicode(("%%%sf" % self._float_format[field]) % value) 890 | return self._unicode(value) 891 | 892 | def _compute_widths(self, rows, options): 893 | if options["header"]: 894 | widths = [_get_size(field)[0] for field in self._field_names] 895 | else: 896 | widths = len(self.field_names) * [0] 897 | for row in rows: 898 | for index, value in enumerate(row): 899 | fieldname = self.field_names[index] 900 | if fieldname in self.max_width: 901 | widths[index] = max(widths[index], min(_get_size(value)[0], self.max_width[fieldname])) 902 | else: 903 | widths[index] = max(widths[index], _get_size(value)[0]) 904 | self._widths = widths 905 | 906 | def _get_padding_widths(self, options): 907 | 908 | if options["left_padding_width"] is not None: 909 | lpad = options["left_padding_width"] 910 | else: 911 | lpad = options["padding_width"] 912 | if options["right_padding_width"] is not None: 913 | rpad = options["right_padding_width"] 914 | else: 915 | rpad = options["padding_width"] 916 | return lpad, rpad 917 | 918 | def _get_rows(self, options): 919 | """Return only those data rows that should be printed, based on slicing and sorting. 920 | 921 | Arguments: 922 | 923 | options - dictionary of option settings.""" 924 | 925 | # Make a copy of only those rows in the slice range 926 | rows = copy.deepcopy(self._rows[options["start"]:options["end"]]) 927 | # Sort if necessary 928 | if options["sortby"]: 929 | sortindex = self._field_names.index(options["sortby"]) 930 | # Decorate 931 | rows = [[row[sortindex]]+row for row in rows] 932 | # Sort 933 | rows.sort(reverse=options["reversesort"], key=options["sort_key"]) 934 | # Undecorate 935 | rows = [row[1:] for row in rows] 936 | return rows 937 | 938 | def _format_row(self, row, options): 939 | return [self._format_value(field, value) for (field, value) in zip(self._field_names, row)] 940 | 941 | def _format_rows(self, rows, options): 942 | return [self._format_row(row, options) for row in rows] 943 | 944 | ############################## 945 | # PLAIN TEXT STRING METHODS # 946 | ############################## 947 | 948 | def get_string(self, **kwargs): 949 | 950 | """Return string representation of table in current state. 951 | 952 | Arguments: 953 | 954 | start - index of first data row to include in output 955 | end - index of last data row to include in output PLUS ONE (list slice style) 956 | fields - names of fields (columns) to include 957 | header - print a header showing field names (True or False) 958 | border - print a border around the table (True or False) 959 | hrules - controls printing of horizontal rules after rows. Allowed values: ALL, FRAME, HEADER, NONE 960 | vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE 961 | int_format - controls formatting of integer data 962 | float_format - controls formatting of floating point data 963 | padding_width - number of spaces on either side of column data (only used if left and right paddings are None) 964 | left_padding_width - number of spaces on left hand side of column data 965 | right_padding_width - number of spaces on right hand side of column data 966 | vertical_char - single character string used to draw vertical lines 967 | horizontal_char - single character string used to draw horizontal lines 968 | junction_char - single character string used to draw line junctions 969 | sortby - name of field to sort rows by 970 | sort_key - sorting key function, applied to data points before sorting 971 | reversesort - True or False to sort in descending or ascending order 972 | print empty - if True, stringify just the header for an empty table, if False return an empty string """ 973 | 974 | options = self._get_options(kwargs) 975 | 976 | lines = [] 977 | 978 | # Don't think too hard about an empty table 979 | # Is this the desired behaviour? Maybe we should still print the header? 980 | if self.rowcount == 0 and (not options["print_empty"] or not options["border"]): 981 | return "" 982 | 983 | # Get the rows we need to print, taking into account slicing, sorting, etc. 984 | rows = self._get_rows(options) 985 | 986 | # Turn all data in all rows into Unicode, formatted as desired 987 | formatted_rows = self._format_rows(rows, options) 988 | 989 | # Compute column widths 990 | self._compute_widths(formatted_rows, options) 991 | 992 | # Add header or top of border 993 | self._hrule = self._stringify_hrule(options) 994 | if options["header"]: 995 | lines.append(self._stringify_header(options)) 996 | elif options["border"] and options["hrules"] in (ALL, FRAME): 997 | lines.append(self._hrule) 998 | 999 | # Add rows 1000 | for row in formatted_rows: 1001 | lines.append(self._stringify_row(row, options)) 1002 | 1003 | # Add bottom of border 1004 | if options["border"] and options["hrules"] == FRAME: 1005 | lines.append(self._hrule) 1006 | 1007 | return self._unicode("\n").join(lines) 1008 | 1009 | def _stringify_hrule(self, options): 1010 | 1011 | if not options["border"]: 1012 | return "" 1013 | lpad, rpad = self._get_padding_widths(options) 1014 | if options['vrules'] in (ALL, FRAME): 1015 | bits = [options["junction_char"]] 1016 | else: 1017 | bits = [options["horizontal_char"]] 1018 | # For tables with no data or fieldnames 1019 | if not self._field_names: 1020 | bits.append(options["junction_char"]) 1021 | return "".join(bits) 1022 | for field, width in zip(self._field_names, self._widths): 1023 | if options["fields"] and field not in options["fields"]: 1024 | continue 1025 | bits.append((width+lpad+rpad)*options["horizontal_char"]) 1026 | if options['vrules'] == ALL: 1027 | bits.append(options["junction_char"]) 1028 | else: 1029 | bits.append(options["horizontal_char"]) 1030 | if options["vrules"] == FRAME: 1031 | bits.pop() 1032 | bits.append(options["junction_char"]) 1033 | return "".join(bits) 1034 | 1035 | def _stringify_header(self, options): 1036 | 1037 | bits = [] 1038 | lpad, rpad = self._get_padding_widths(options) 1039 | if options["border"]: 1040 | if options["hrules"] in (ALL, FRAME): 1041 | bits.append(self._hrule) 1042 | bits.append("\n") 1043 | if options["vrules"] in (ALL, FRAME): 1044 | bits.append(options["vertical_char"]) 1045 | else: 1046 | bits.append(" ") 1047 | # For tables with no data or field names 1048 | if not self._field_names: 1049 | if options["vrules"] in (ALL, FRAME): 1050 | bits.append(options["vertical_char"]) 1051 | else: 1052 | bits.append(" ") 1053 | for field, width, in zip(self._field_names, self._widths): 1054 | if options["fields"] and field not in options["fields"]: 1055 | continue 1056 | if self._header_style == "cap": 1057 | fieldname = field.capitalize() 1058 | elif self._header_style == "title": 1059 | fieldname = field.title() 1060 | elif self._header_style == "upper": 1061 | fieldname = field.upper() 1062 | elif self._header_style == "lower": 1063 | fieldname = field.lower() 1064 | else: 1065 | fieldname = field 1066 | bits.append(" " * lpad + self._justify(fieldname, width, self._align[field]) + " " * rpad) 1067 | if options["border"]: 1068 | if options["vrules"] == ALL: 1069 | bits.append(options["vertical_char"]) 1070 | else: 1071 | bits.append(" ") 1072 | # If vrules is FRAME, then we just appended a space at the end 1073 | # of the last field, when we really want a vertical character 1074 | if options["border"] and options["vrules"] == FRAME: 1075 | bits.pop() 1076 | bits.append(options["vertical_char"]) 1077 | if options["border"] and options["hrules"] != NONE: 1078 | bits.append("\n") 1079 | bits.append(self._hrule) 1080 | return "".join(bits) 1081 | 1082 | def _stringify_row(self, row, options): 1083 | 1084 | for index, field, value, width, in zip(range(0,len(row)), self._field_names, row, self._widths): 1085 | # Enforce max widths 1086 | lines = value.split("\n") 1087 | new_lines = [] 1088 | for line in lines: 1089 | if _str_block_width(line) > width: 1090 | line = textwrap.fill(line, width) 1091 | new_lines.append(line) 1092 | lines = new_lines 1093 | value = "\n".join(lines) 1094 | row[index] = value 1095 | 1096 | row_height = 0 1097 | for c in row: 1098 | h = _get_size(c)[1] 1099 | if h > row_height: 1100 | row_height = h 1101 | 1102 | bits = [] 1103 | lpad, rpad = self._get_padding_widths(options) 1104 | for y in range(0, row_height): 1105 | bits.append([]) 1106 | if options["border"]: 1107 | if options["vrules"] in (ALL, FRAME): 1108 | bits[y].append(self.vertical_char) 1109 | else: 1110 | bits[y].append(" ") 1111 | 1112 | for field, value, width, in zip(self._field_names, row, self._widths): 1113 | 1114 | valign = self._valign[field] 1115 | lines = value.split("\n") 1116 | dHeight = row_height - len(lines) 1117 | if dHeight: 1118 | if valign == "m": 1119 | lines = [""] * int(dHeight / 2) + lines + [""] * (dHeight - int(dHeight / 2)) 1120 | elif valign == "b": 1121 | lines = [""] * dHeight + lines 1122 | else: 1123 | lines = lines + [""] * dHeight 1124 | 1125 | y = 0 1126 | for l in lines: 1127 | if options["fields"] and field not in options["fields"]: 1128 | continue 1129 | 1130 | bits[y].append(" " * lpad + self._justify(l, width, self._align[field]) + " " * rpad) 1131 | if options["border"]: 1132 | if options["vrules"] == ALL: 1133 | bits[y].append(self.vertical_char) 1134 | else: 1135 | bits[y].append(" ") 1136 | y += 1 1137 | 1138 | # If vrules is FRAME, then we just appended a space at the end 1139 | # of the last field, when we really want a vertical character 1140 | for y in range(0, row_height): 1141 | if options["border"] and options["vrules"] == FRAME: 1142 | bits[y].pop() 1143 | bits[y].append(options["vertical_char"]) 1144 | 1145 | if options["border"] and options["hrules"]== ALL: 1146 | bits[row_height-1].append("\n") 1147 | bits[row_height-1].append(self._hrule) 1148 | 1149 | for y in range(0, row_height): 1150 | bits[y] = "".join(bits[y]) 1151 | 1152 | return "\n".join(bits) 1153 | 1154 | ############################## 1155 | # HTML STRING METHODS # 1156 | ############################## 1157 | 1158 | def get_html_string(self, **kwargs): 1159 | 1160 | """Return string representation of HTML formatted version of table in current state. 1161 | 1162 | Arguments: 1163 | 1164 | start - index of first data row to include in output 1165 | end - index of last data row to include in output PLUS ONE (list slice style) 1166 | fields - names of fields (columns) to include 1167 | header - print a header showing field names (True or False) 1168 | border - print a border around the table (True or False) 1169 | hrules - controls printing of horizontal rules after rows. Allowed values: ALL, FRAME, HEADER, NONE 1170 | vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE 1171 | int_format - controls formatting of integer data 1172 | float_format - controls formatting of floating point data 1173 | padding_width - number of spaces on either side of column data (only used if left and right paddings are None) 1174 | left_padding_width - number of spaces on left hand side of column data 1175 | right_padding_width - number of spaces on right hand side of column data 1176 | sortby - name of field to sort rows by 1177 | sort_key - sorting key function, applied to data points before sorting 1178 | attributes - dictionary of name/value pairs to include as HTML attributes in the
tag 1179 | xhtml - print
tags if True,
tags if false""" 1180 | 1181 | options = self._get_options(kwargs) 1182 | 1183 | if options["format"]: 1184 | string = self._get_formatted_html_string(options) 1185 | else: 1186 | string = self._get_simple_html_string(options) 1187 | 1188 | return string 1189 | 1190 | def _get_simple_html_string(self, options): 1191 | 1192 | lines = [] 1193 | if options["xhtml"]: 1194 | linebreak = "
" 1195 | else: 1196 | linebreak = "
" 1197 | 1198 | open_tag = [] 1199 | open_tag.append("") 1204 | lines.append("".join(open_tag)) 1205 | 1206 | # Headers 1207 | if options["header"]: 1208 | lines.append(" ") 1209 | for field in self._field_names: 1210 | if options["fields"] and field not in options["fields"]: 1211 | continue 1212 | lines.append(" " % escape(field).replace("\n", linebreak)) 1213 | lines.append(" ") 1214 | 1215 | # Data 1216 | rows = self._get_rows(options) 1217 | formatted_rows = self._format_rows(rows, options) 1218 | for row in formatted_rows: 1219 | lines.append(" ") 1220 | for field, datum in zip(self._field_names, row): 1221 | if options["fields"] and field not in options["fields"]: 1222 | continue 1223 | lines.append(" " % escape(datum).replace("\n", linebreak)) 1224 | lines.append(" ") 1225 | 1226 | lines.append("
%s
%s
") 1227 | 1228 | return self._unicode("\n").join(lines) 1229 | 1230 | def _get_formatted_html_string(self, options): 1231 | 1232 | lines = [] 1233 | lpad, rpad = self._get_padding_widths(options) 1234 | if options["xhtml"]: 1235 | linebreak = "
" 1236 | else: 1237 | linebreak = "
" 1238 | 1239 | open_tag = [] 1240 | open_tag.append("") 1260 | lines.append("".join(open_tag)) 1261 | 1262 | # Headers 1263 | if options["header"]: 1264 | lines.append(" ") 1265 | for field in self._field_names: 1266 | if options["fields"] and field not in options["fields"]: 1267 | continue 1268 | lines.append(" %s" % (lpad, rpad, escape(field).replace("\n", linebreak))) 1269 | lines.append(" ") 1270 | 1271 | # Data 1272 | rows = self._get_rows(options) 1273 | formatted_rows = self._format_rows(rows, options) 1274 | aligns = [] 1275 | valigns = [] 1276 | for field in self._field_names: 1277 | aligns.append({ "l" : "left", "r" : "right", "c" : "center" }[self._align[field]]) 1278 | valigns.append({"t" : "top", "m" : "middle", "b" : "bottom"}[self._valign[field]]) 1279 | for row in formatted_rows: 1280 | lines.append(" ") 1281 | for field, datum, align, valign in zip(self._field_names, row, aligns, valigns): 1282 | if options["fields"] and field not in options["fields"]: 1283 | continue 1284 | lines.append(" %s" % (lpad, rpad, align, valign, escape(datum).replace("\n", linebreak))) 1285 | lines.append(" ") 1286 | lines.append("") 1287 | 1288 | return self._unicode("\n").join(lines) 1289 | 1290 | ############################## 1291 | # UNICODE WIDTH FUNCTIONS # 1292 | ############################## 1293 | 1294 | def _char_block_width(char): 1295 | # Basic Latin, which is probably the most common case 1296 | #if char in xrange(0x0021, 0x007e): 1297 | #if char >= 0x0021 and char <= 0x007e: 1298 | if 0x0021 <= char <= 0x007e: 1299 | return 1 1300 | # Chinese, Japanese, Korean (common) 1301 | if 0x4e00 <= char <= 0x9fff: 1302 | return 2 1303 | # Hangul 1304 | if 0xac00 <= char <= 0xd7af: 1305 | return 2 1306 | # Combining? 1307 | if unicodedata.combining(uni_chr(char)): 1308 | return 0 1309 | # Hiragana and Katakana 1310 | if 0x3040 <= char <= 0x309f or 0x30a0 <= char <= 0x30ff: 1311 | return 2 1312 | # Full-width Latin characters 1313 | if 0xff01 <= char <= 0xff60: 1314 | return 2 1315 | # CJK punctuation 1316 | if 0x3000 <= char <= 0x303e: 1317 | return 2 1318 | # Backspace and delete 1319 | if char in (0x0008, 0x007f): 1320 | return -1 1321 | # Other control characters 1322 | elif char in (0x0000, 0x001f): 1323 | return 0 1324 | # Take a guess 1325 | return 1 1326 | 1327 | def _str_block_width(val): 1328 | 1329 | return sum(itermap(_char_block_width, itermap(ord, _re.sub("", val)))) 1330 | 1331 | ############################## 1332 | # TABLE FACTORIES # 1333 | ############################## 1334 | 1335 | def from_csv(fp, field_names = None, **kwargs): 1336 | 1337 | dialect = csv.Sniffer().sniff(fp.read(1024)) 1338 | fp.seek(0) 1339 | reader = csv.reader(fp, dialect) 1340 | 1341 | table = PrettyTable(**kwargs) 1342 | if field_names: 1343 | table.field_names = field_names 1344 | else: 1345 | if py3k: 1346 | table.field_names = [x.strip() for x in next(reader)] 1347 | else: 1348 | table.field_names = [x.strip() for x in reader.next()] 1349 | 1350 | for row in reader: 1351 | table.add_row([x.strip() for x in row]) 1352 | 1353 | return table 1354 | 1355 | def from_db_cursor(cursor, **kwargs): 1356 | 1357 | if cursor.description: 1358 | table = PrettyTable(**kwargs) 1359 | table.field_names = [col[0] for col in cursor.description] 1360 | for row in cursor.fetchall(): 1361 | table.add_row(row) 1362 | return table 1363 | 1364 | class TableHandler(HTMLParser): 1365 | 1366 | def __init__(self, **kwargs): 1367 | HTMLParser.__init__(self) 1368 | self.kwargs = kwargs 1369 | self.tables = [] 1370 | self.last_row = [] 1371 | self.rows = [] 1372 | self.max_row_width = 0 1373 | self.active = None 1374 | self.last_content = "" 1375 | self.is_last_row_header = False 1376 | 1377 | def handle_starttag(self,tag, attrs): 1378 | self.active = tag 1379 | if tag == "th": 1380 | self.is_last_row_header = True 1381 | 1382 | def handle_endtag(self,tag): 1383 | if tag in ["th", "td"]: 1384 | stripped_content = self.last_content.strip() 1385 | self.last_row.append(stripped_content) 1386 | if tag == "tr": 1387 | self.rows.append( 1388 | (self.last_row, self.is_last_row_header)) 1389 | self.max_row_width = max(self.max_row_width, len(self.last_row)) 1390 | self.last_row = [] 1391 | self.is_last_row_header = False 1392 | if tag == "table": 1393 | table = self.generate_table(self.rows) 1394 | self.tables.append(table) 1395 | self.rows = [] 1396 | self.last_content = " " 1397 | self.active = None 1398 | 1399 | 1400 | def handle_data(self, data): 1401 | self.last_content += data 1402 | 1403 | def generate_table(self, rows): 1404 | """ 1405 | Generates from a list of rows a PrettyTable object. 1406 | """ 1407 | table = PrettyTable(**self.kwargs) 1408 | for row in self.rows: 1409 | if len(row[0]) < self.max_row_width: 1410 | appends = self.max_row_width - len(row[0]) 1411 | for i in range(1,appends): 1412 | row[0].append("-") 1413 | 1414 | if row[1] == True: 1415 | self.make_fields_unique(row[0]) 1416 | table.field_names = row[0] 1417 | else: 1418 | table.add_row(row[0]) 1419 | return table 1420 | 1421 | def make_fields_unique(self, fields): 1422 | """ 1423 | iterates over the row and make each field unique 1424 | """ 1425 | for i in range(0, len(fields)): 1426 | for j in range(i+1, len(fields)): 1427 | if fields[i] == fields[j]: 1428 | fields[j] += "'" 1429 | 1430 | def from_html(html_code, **kwargs): 1431 | """ 1432 | Generates a list of PrettyTables from a string of HTML code. Each in 1433 | the HTML becomes one PrettyTable object. 1434 | """ 1435 | 1436 | parser = TableHandler(**kwargs) 1437 | parser.feed(html_code) 1438 | return parser.tables 1439 | 1440 | def from_html_one(html_code, **kwargs): 1441 | """ 1442 | Generates a PrettyTables from a string of HTML code which contains only a 1443 | single
1444 | """ 1445 | 1446 | tables = from_html(html_code, **kwargs) 1447 | try: 1448 | assert len(tables) == 1 1449 | except AssertionError: 1450 | raise Exception("More than one
in provided HTML code! Use from_html instead.") 1451 | return tables[0] 1452 | 1453 | ############################## 1454 | # MAIN (TEST FUNCTION) # 1455 | ############################## 1456 | 1457 | def main(): 1458 | 1459 | x = PrettyTable(["City name", "Area", "Population", "Annual Rainfall"]) 1460 | x.sortby = "Population" 1461 | x.reversesort = True 1462 | x.int_format["Area"] = "04d" 1463 | x.float_format = "6.1f" 1464 | x.align["City name"] = "l" # Left align city names 1465 | x.add_row(["Adelaide", 1295, 1158259, 600.5]) 1466 | x.add_row(["Brisbane", 5905, 1857594, 1146.4]) 1467 | x.add_row(["Darwin", 112, 120900, 1714.7]) 1468 | x.add_row(["Hobart", 1357, 205556, 619.5]) 1469 | x.add_row(["Sydney", 2058, 4336374, 1214.8]) 1470 | x.add_row(["Melbourne", 1566, 3806092, 646.9]) 1471 | x.add_row(["Perth", 5386, 1554769, 869.4]) 1472 | print(x) 1473 | 1474 | if __name__ == "__main__": 1475 | main() 1476 | -------------------------------------------------------------------------------- /source/processor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Processes the command line args. 3 | """ 4 | 5 | # emacs shortcuts in raw_input, doesn't work on Windows 6 | try: 7 | import readline 8 | except ImportError: 9 | pass 10 | 11 | # relative import 12 | import database as db 13 | import lib.prettytable as prettytable 14 | 15 | #import from standard library 16 | import tempfile 17 | import re 18 | import subprocess 19 | import textwrap 20 | import sys 21 | import os 22 | import urlparse 23 | import webbrowser 24 | import pydoc 25 | from xml.etree import ElementTree 26 | 27 | ##GENERAL 28 | 29 | def start_process(cmd_line_args): 30 | """ 31 | Starts processing the command line args. Filters out unrelevant arguments. 32 | """ 33 | 34 | relevant_args = ({key: value for key, value in cmd_line_args.iteritems() 35 | if value is not False and value is not None}) 36 | 37 | 38 | relevant_args = unicode_everything(relevant_args) 39 | 40 | if '--editor' in relevant_args: 41 | configure_editor(relevant_args) 42 | 43 | elif '--suffix' in relevant_args: 44 | configure_suffix(relevant_args) 45 | 46 | elif '--line' in relevant_args: 47 | configure_line_length(relevant_args) 48 | 49 | elif '--wait' in relevant_args: 50 | set_wait_option(relevant_args) 51 | 52 | elif 'rollback' in relevant_args: 53 | database = db.Database() 54 | database.rollback() 55 | else: 56 | body, flags = split_arguments(relevant_args) 57 | determine_proceeding(body, flags) 58 | 59 | 60 | def set_wait_option(option): 61 | """ 62 | Sets the wait option to either on or off. 63 | """ 64 | 65 | value = "" 66 | if 'on' in option: 67 | value = "on" 68 | print "Enabling 'wait' option." 69 | else: 70 | print "Disabling 'wait' option." 71 | 72 | database = db.Database() 73 | database.set_config_item('wait', value) 74 | sys.exit(0) 75 | 76 | def configure_editor(arguments): 77 | """ 78 | Configures editor value, gets called when --editor is invoked. 79 | Displays current value when no LINE argument in given. 80 | """ 81 | 82 | if 'EDITOR' in arguments: 83 | set_editor(arguments['EDITOR']) 84 | else: 85 | database = db.Database() 86 | editor = check_for_editor(database) 87 | print "Your current editor is '{0}'.".format(editor) 88 | 89 | def configure_line_length(arguments): 90 | """ 91 | Configures line length value, gets called when --line is invoked. 92 | Displays current value when no INTEGER argument in given. 93 | """ 94 | 95 | if 'INTEGER' in arguments: 96 | set_line_length(arguments['INTEGER']) 97 | else: 98 | database = db.Database() 99 | length = get_console_length(database) 100 | print "Your current console length is {0} characters.".format(length) 101 | 102 | def configure_suffix(arguments): 103 | """ 104 | Configures suffix value, gets called when --suffix is invoked. 105 | Displays current value when no SUFFIX argument in given. 106 | """ 107 | 108 | if 'SUFFIX' in arguments: 109 | set_suffix(arguments['SUFFIX'], arguments['LANGUAGE']) 110 | else: 111 | database = db.Database() 112 | suffix = check_for_suffix(arguments['LANGUAGE'], database, prompt_for_input=False) 113 | if suffix: 114 | print "Suffix for language {0} is '{1}'.".format(arguments['LANGUAGE'], suffix) 115 | else: 116 | print "Suffix is currently not set for language {0}".format(arguments['LANGUAGE']) 117 | 118 | def set_editor(editor): 119 | """ 120 | Sets the editor (in the database). 121 | """ 122 | 123 | database = db.Database() 124 | database.set_config_item('editor', editor.strip()) 125 | print "Setting editor {0} successful.".format(editor.encode('utf-8')) 126 | 127 | 128 | def set_suffix(suffix, language): 129 | """ 130 | Sets the suffix for a specific language (in the database). 131 | """ 132 | 133 | database = db.Database() 134 | database.set_suffix(language.strip(), suffix) 135 | print "Setting suffix {0} for {1} successful.".format( 136 | suffix.encode('utf-8'), language.encode('utf-8')) 137 | 138 | 139 | def set_line_length(length): 140 | """ 141 | Sets the console's line length. 142 | """ 143 | 144 | try: 145 | int(length) 146 | database = db.Database() 147 | database.set_config_item('linelength', length.encode('utf-8')) 148 | print "Setting your console line length to {0} successful.".format(length) 149 | except ValueError: 150 | print "Console line length must be an integer." 151 | 152 | 153 | def split_arguments(arguments): 154 | """ 155 | Splits the given arguments from the command line in content and flags. 156 | Returns: Tuple filled with 2 dictionaries. 157 | """ 158 | 159 | request, flags = {}, {} 160 | for key, item in arguments.iteritems(): 161 | if key in ('edit', 'link', 'add', 'display', 'file', 'tags', 162 | 'export', 'import', '-t', '-l', '--hline', '--suffix'): 163 | flags[key] = item 164 | else: 165 | request[key.lower()] = item 166 | 167 | return request, flags 168 | 169 | 170 | def determine_proceeding(body, flags): 171 | """ 172 | Checks which operation (add, display, ...) needs to be handled. 173 | """ 174 | 175 | if 'file' in flags: 176 | process_file_adding(body) 177 | elif 'display' in flags: 178 | determine_display_operation(body, flags) 179 | elif 'tags' in flags: 180 | show_tags(body, flags) 181 | elif 'add' in flags: 182 | insert_content() 183 | elif 'edit' in flags: 184 | process_code_adding(body) 185 | elif 'link' in flags: 186 | process_links(body, flags) 187 | elif 'export' in flags: 188 | process_export(body) 189 | elif 'import' in flags: 190 | process_import(body) 191 | else: 192 | print "An unexpected error has ocurred." 193 | 194 | def check_for_suffix(language, database, prompt_for_input=True): 195 | """Checks if the DB has a suffix for the requested language, if not 196 | it prompts to specify one. 197 | """ 198 | 199 | suffix = database.retrieve_suffix(language) 200 | if suffix and suffix[0]: 201 | return suffix[0] 202 | else: 203 | if prompt_for_input: 204 | input_suffix = process_and_validate_input("Enter file suffix for language " 205 | + language + " : ") 206 | database.set_suffix(language, input_suffix) 207 | return input_suffix 208 | else: 209 | return False 210 | 211 | def check_for_editor(database): 212 | """ 213 | Checks for editor in the Database. If none is specified, it prompts to enter one. 214 | """ 215 | 216 | editor = database.get_config_item('editor') 217 | 218 | if not editor: 219 | editor = process_and_validate_input("Enter your editor: ") 220 | database.set_config_item('editor', editor) 221 | editor = editor.encode('utf-8') 222 | else: 223 | editor = editor[0].encode('utf-8') 224 | 225 | return editor 226 | 227 | 228 | def editor_to_list(database): 229 | """ 230 | Gets editor and turns it into list. Returns the editor string as well for convenience 231 | when displaying an error. 232 | """ 233 | 234 | editor_value = check_for_editor(database) 235 | # subprocess works best with lists instead of strings 236 | editor_list = [argument for argument in editor_value.split(" ")] 237 | return editor_list, editor_value 238 | 239 | ###OUTPUT ### 240 | 241 | def process_printing(table): 242 | """ 243 | Processes all priting (to console or editor). 244 | """ 245 | 246 | decision = decide_where_to_print(table) 247 | 248 | if decision == 'console': 249 | print table 250 | elif decision == 'pager': 251 | pydoc.pager(table.get_string()) 252 | else: 253 | database = db.Database() 254 | print_to_editor(table, database) 255 | 256 | 257 | def print_to_editor(table, database): 258 | """ 259 | Sets up a nice input form (editor) for viewing a large amount of content. 260 | Checks for editor -> Turns result into List -> Writes data (if present) to file -> 261 | Opens the (temporary) file. 262 | Returns: tempfile so it can be removed 263 | """ 264 | 265 | editor, editor_string = editor_to_list(database) 266 | 267 | prewritten_data = table.get_string() # prettytable to string 268 | 269 | with tempfile.NamedTemporaryFile(delete=False) as tmpfile: 270 | try: 271 | tmpfile.write(prewritten_data) 272 | tmpfile.flush() 273 | try: 274 | subprocess.Popen(editor + [tmpfile.name]) 275 | except (OSError, IOError) as error: 276 | print ("Error calling your editor '{0}'. Please make sure, this is an executable." 277 | .format(editor_string)) 278 | print "Original python error message was:\n {0}".format(error.strerror) 279 | sys.exit(1) 280 | 281 | except (OSError, IOError) as error: 282 | print error 283 | sys.exit(1) 284 | 285 | 286 | def decide_where_to_print(table): 287 | """ 288 | Decides where to print to. If output is too long (<25 lines), 289 | user gets asked where to print. 290 | """ 291 | 292 | if len(table.get_string().splitlines()) < 25: # output smaller than 25 lines 293 | return 'console' 294 | else: 295 | decision = "" 296 | while decision == "": 297 | choice = process_and_validate_input("Output longer than 25 lines - " 298 | "print to pager? (y/n)") 299 | if choice in ('y', 'yes', 'Yes', 'Y'): 300 | decision = "pager" 301 | elif choice in ('n', 'no', 'No', 'N'): 302 | decision = "editor" 303 | else: 304 | continue 305 | return decision 306 | 307 | 308 | def input_from_editor(database, existing_content="", suffix=""): 309 | """ 310 | Sets up a editor for code (solution) adding and viewing. 311 | Checks for editor -> Turns into list -> Check if wait is enabled -> 312 | Opens tempfile -> When user is done with editing : Returns content of file -> 313 | Tries to delete file. 314 | """ 315 | 316 | editor, editor_string = editor_to_list(database) 317 | 318 | prewritten_data = existing_content.encode('utf-8') 319 | 320 | try: 321 | wait_enabled = database.get_config_item('wait') 322 | except IndexError: 323 | wait_enabled = False 324 | 325 | with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmpfile: 326 | if existing_content: 327 | tmpfile.write(prewritten_data) 328 | tmpfile.seek(0) 329 | 330 | file_name = tmpfile.name 331 | 332 | if sys.platform == "win32" or wait_enabled: # windows or wait enabled 333 | 334 | # tmpfile is read after user is finished with editing, (tmpfile gets closed before) 335 | try: 336 | subprocess.Popen(editor + [tmpfile.name]) 337 | except (OSError, IOError, ValueError) as error: 338 | print ("Error calling your editor '{0}'. Please make sure, this is an executable." 339 | .format(editor_string)) 340 | print "Original python error message was:\n {0}".format(error.strerror) 341 | sys.exit(1) 342 | 343 | tmpfile.close() 344 | 345 | go_ahead = False 346 | while not go_ahead: 347 | if process_and_validate_input("Are you done adding code? (y/n): ") == 'y': 348 | go_ahead = True 349 | else: 350 | continue 351 | else: # platform != windows 352 | try: 353 | subprocess.call(editor + [tmpfile.name]) 354 | except (OSError, IOError) as error: 355 | print ("Error calling your editor '{0}'. Please make sure, this is an executable." 356 | .format(editor_string)) 357 | print "Original python error message was:\n {0}".format(error.strerror) 358 | sys.exit(1) 359 | 360 | # no matter what platform from now on 361 | return read_and_delete_tmpfile(file_name) 362 | 363 | 364 | ###CODE ### 365 | 366 | def process_code_adding(body, database=False, code_of_target=False): 367 | """ 368 | Processes code (solution) adding. 369 | Gets already present content from database -> Gets language suffix -> 370 | Input from editor -> Save content to database. 371 | """ 372 | 373 | if not database: 374 | database = db.Database() 375 | 376 | # If code isn't already retrieved (code_of_target), retrieve from DB. 377 | if not code_of_target: 378 | existing_code = database.retrieve_content(body, "code") 379 | if not existing_code: 380 | existing_code = "" 381 | else: 382 | existing_code = existing_code[0] 383 | else: 384 | existing_code = code_of_target 385 | 386 | suffix = check_for_suffix(body['language'], database) 387 | body['data'] = input_from_editor(database, existing_content=existing_code, suffix=suffix) 388 | 389 | try: 390 | body['data'] = unicode(body['data'], 'utf-8') 391 | except UnicodeError as error: 392 | print error 393 | 394 | #determine if data needs to be written to the database 395 | if body['data'] == existing_code: 396 | print "Nothing changed." 397 | else: 398 | body['attribute'] = "code" 399 | database.upsert_solution(body) 400 | print "\nUpdating your codedict successful." 401 | 402 | 403 | ###FILE ### 404 | 405 | def process_file_adding(body): 406 | """ 407 | Processes adding content to DB from a file. 408 | Opens file -> Reads its content -> 409 | Either match with regex pattern or simply save in DB. 410 | """ 411 | 412 | try: 413 | with open(body['path-to-file']) as input_file: 414 | file_text = input_file.read() 415 | input_file.close() 416 | except (OSError, IOError) as error: 417 | print "File Error({0}): {1}".format(error.errno, error.strerror) 418 | sys.exit(1) 419 | 420 | database = db.Database() 421 | 422 | # if file content should be treated as code, write it to DB. Otherwise find matches with regex. 423 | if 'problem' in body: 424 | body['data'] = file_text 425 | database.upsert_solution(body) 426 | else: 427 | all_matches = (re.findall(r'%[^\|%]*?\|([^\|]*)\|[^\|%]*?\|([^\|]*)\|[^\|%]*\|([^\|]*)\|', 428 | file_text, re.UNICODE)) 429 | database.add_content(all_matches, body['language'], insert_type="from_file") 430 | print "\nUpdating your codedict from file successful." 431 | 432 | 433 | ## LINKS 434 | 435 | def process_links(body, flags): 436 | """ 437 | Processes links. Determines proceeding. 438 | """ 439 | 440 | # add links 441 | if '--open' not in flags: 442 | if 'link_name' not in body: 443 | # set name based on url scheme 444 | body['link_name'] = set_link_name(body) 445 | 446 | if 'language' not in body: 447 | body['original-lang'] = "" 448 | body['language'] = "" 449 | else: 450 | body['original-lang'] = body['language'] 451 | database = db.Database() 452 | database.upsert_links(body) 453 | print "Added link {0} to database.".format(body['link_name'].encode('utf-8')) 454 | sys.exit(0) 455 | 456 | 457 | def set_link_name(body): 458 | """ 459 | Sets the link name if not given, based on the url scheme. 460 | """ 461 | 462 | entire_url = urlparse.urlsplit(body['url']) 463 | for url_part in reversed(entire_url): 464 | if url_part: 465 | subpart = url_part.split("/") 466 | if url_part.endswith('/'): 467 | link_name = link_name = subpart[len(subpart) - 2].replace('.html', '') 468 | else: 469 | link_name = subpart[len(subpart) - 1].replace('.html', '') 470 | break 471 | else: 472 | print "Not a valid url." 473 | sys.exit(1) 474 | if not entire_url.scheme: 475 | return "http://" + link_name 476 | else: 477 | return link_name 478 | 479 | 480 | def run_webbrowser(url): 481 | """ 482 | Runs the url in the webbrowser (and surpresses output). 483 | """ 484 | 485 | print "Opening your browser." 486 | # needed for surpressing error messages in console when opening browser 487 | os.close(2) 488 | os.close(1) 489 | os.open(os.devnull, os.O_RDWR) 490 | webbrowser.get().open(url) 491 | sys.exit(0) 492 | 493 | 494 | ## ADDING 495 | 496 | def update_content(body, database=False): 497 | """ 498 | Processes how to update content and saves it to database. (delete or add) 499 | """ 500 | 501 | if not database: 502 | database = db.Database() 503 | 504 | if body['attribute'] != 'del': 505 | 506 | if body['attribute'] != 'solution': 507 | body['data'] = process_and_validate_input("Change " + body['attribute'] + " to: ") 508 | database.update_content(body) 509 | print "Changing" + body['attribute'] + "successful." 510 | else: 511 | process_code_adding(body, database=database) 512 | else: 513 | database.delete_content(body) 514 | 515 | 516 | def insert_content(): 517 | """ 518 | Processes how to insert content. 519 | Input (and validation) from user for all required fields -> Save to database 520 | """ 521 | 522 | content_to_add = {} 523 | 524 | # get valid input 525 | database = db.Database() 526 | 527 | language = process_and_validate_input("Enter language: ") 528 | content_to_add[0] = process_and_validate_input("Enter your tags - seperated with ';' : ") 529 | content_to_add[1] = process_and_validate_input("Enter problem: ") 530 | 531 | # no editor wished 532 | if content_to_add[1].startswith('!!'): 533 | content_to_add[1] = content_to_add[1].replace('!!', '', 1) 534 | content_to_add[2] = process_and_validate_input("Enter solution: ") 535 | else: 536 | suffix = check_for_suffix(language, database) 537 | content_to_add[2] = input_from_editor(database, suffix=suffix) 538 | 539 | database.add_content([content_to_add], language) # db method works best with lists 540 | print "\nUpdating your codedict successful." 541 | 542 | 543 | ### DISPLAYING ### 544 | 545 | def determine_display_operation(body, flags): 546 | """ 547 | Processes display actions, checks if a nice form has to be provided or not. 548 | Get content from database -> get console line length -> build (output) table -> 549 | Perform follow up operation. 550 | """ 551 | 552 | args_dict = build_args_dict(flags) 553 | 554 | database = db.Database() 555 | 556 | display_type = "display" 557 | 558 | results = [] 559 | 560 | if '-t' in flags: 561 | display_type = "tag" 562 | results, column_list = get_dict_results(database, body, flags) 563 | 564 | elif '-l' in flags: 565 | display_type = "link" 566 | results, column_list = get_link_results(database, body) 567 | 568 | else: 569 | results, column_list = get_dict_results(database, body, flags) 570 | 571 | if results: 572 | 573 | console_linelength = get_console_length(database) 574 | 575 | updated_results, table = build_table(column_list, results, console_linelength, args_dict) 576 | 577 | process_printing(table) 578 | state = State(database, body, flags, updated_results) 579 | state.process_follow_up_operation(display_type) 580 | else: 581 | print "No results." 582 | 583 | 584 | def get_link_results(database, body): 585 | """ 586 | Gets all results for link query from DB and returns the column list additionally. 587 | """ 588 | 589 | if 'searchpattern' not in body: 590 | body['searchpattern'] = "" 591 | results = database.retrieve_links(body, 'display') 592 | column_list = ["index", "link name", "url", "language"] 593 | 594 | else: 595 | results = database.retrieve_links(body, 'lang-display') 596 | column_list = ["index", "link name", "url"] 597 | 598 | return results, column_list 599 | 600 | 601 | def get_dict_results(database, body, flags): 602 | """ 603 | Gets all results for dict query from DB and returns the column list additionally. 604 | """ 605 | 606 | if '-t' in flags: 607 | if not 'searchpattern' in body: 608 | body['searchpattern'] = "" 609 | results = database.retrieve_dict_per_tags(body) 610 | column_list = ["index", "problem", "solution"] 611 | 612 | elif 'language' in body and body['language'] != "": 613 | results = database.retrieve_content(body, "language") 614 | column_list = ["index", "problem", "solution preview"] 615 | 616 | else: 617 | results = database.retrieve_content(body, "basic") 618 | column_list = ["index", "language", "problem", "solution preview"] 619 | 620 | return results, column_list 621 | 622 | 623 | def get_console_length(database): 624 | """ 625 | Gets console length from DB and sets it appropiately. --> convert to int 626 | """ 627 | 628 | console_linelength = database.get_config_item('linelength') 629 | if not console_linelength: 630 | console_linelength = 80 631 | else: 632 | console_linelength = int(console_linelength[0]) 633 | return console_linelength 634 | 635 | 636 | def build_table(column_list, all_rows, line_length, args_dict): 637 | """ 638 | Builds (pretty)table and prints it to console. 639 | """ 640 | 641 | cl_length = len(column_list) - 1 642 | result_table = prettytable.PrettyTable(column_list) 643 | 644 | if 'hline' not in args_dict: 645 | result_table.hrules = prettytable.ALL 646 | 647 | all_rows_as_list = [] 648 | 649 | field_length = (line_length - 10) / cl_length 650 | 651 | for row in all_rows: 652 | single_row = list(row) # row is a tuple and contains db query results. 653 | 654 | if not 'link' in args_dict: 655 | if len(single_row[cl_length]) > 50: 656 | appended_string = "..." 657 | else: 658 | appended_string = "" 659 | single_row[cl_length] = single_row[cl_length][0:50] + appended_string 660 | 661 | for index in range(1, cl_length + 1): 662 | if not single_row[index]: 663 | single_row[index] = "" 664 | 665 | dedented_item = textwrap.dedent(single_row[index]).strip() 666 | single_row[index] = textwrap.fill(dedented_item, width=field_length) 667 | 668 | #add modified row to table, add original row to return-list 669 | result_table.add_row(single_row) 670 | all_rows_as_list.append(list(row)) 671 | return all_rows_as_list, result_table 672 | 673 | 674 | ###FOLLOW UP ### 675 | 676 | class State(object): 677 | """ 678 | State (table, query results, database etc.) after the initial 'query' gets saved. 679 | Used for 'displaying' operations only. 680 | """ 681 | 682 | def __init__(self, database, body, flags, query_result): 683 | self._database = database 684 | self._results = query_result 685 | self.body_state = body 686 | self._original_flags = flags 687 | 688 | def _prompt_by_index(self, prompt, default_attribute, permitted_actions): 689 | """Prompts the user for further commands after displaying content or links.""" 690 | 691 | valid_input = False 692 | while not valid_input: 693 | 694 | user_input = process_and_validate_input(prompt).split(None, 1) 695 | # abort with 'enter' 696 | if not user_input: 697 | sys.exit(0) 698 | index = user_input[0] 699 | try: 700 | attribute = user_input[1].lower() 701 | except IndexError: 702 | attribute = default_attribute 703 | 704 | if (len(user_input) <= 2 and index.isdigit() and 705 | 1 <= int(index) <= len(self._results)): 706 | 707 | actual_index = int(index) - 1 708 | if attribute and attribute in permitted_actions: 709 | valid_input = True 710 | else: 711 | print "Wrong attribute, Please try again." 712 | valid_input = False 713 | else: 714 | print "Wrong index, Please try again." 715 | return self._results[actual_index], attribute 716 | 717 | def process_follow_up_operation(self, operation_type): 718 | """ 719 | Processes the 2nd operation of the user, e.g. code adding. 720 | Differentiates between link and dict table (and permitted actions). 721 | """ 722 | 723 | prompt = "Do you want to do more? Usage: INDEX [ACTION] - Press ENTER to abort: \n" 724 | 725 | # link table 726 | if operation_type == 'link': 727 | permitted_actions = ('del', 'name', 'link', 'lang') 728 | target, attribute = self._prompt_by_index(prompt, 'link', permitted_actions) 729 | self._link_determine_operation(target, attribute) 730 | 731 | # dict table 732 | else: 733 | permitted_actions = ('del', 'problem', 'solution', 'tags') 734 | target, attribute = self._prompt_by_index( 735 | prompt, 'solution', permitted_actions) 736 | 737 | if self.body_state['language'] == "": 738 | self.body_state['language'] = target[1] 739 | self.body_state['problem'] = target[2] 740 | else: 741 | self.body_state['problem'] = target[1] 742 | 743 | if attribute == 'tags': 744 | self.update_tag_for_dict() 745 | elif attribute != 'solution': 746 | self.body_state['attribute'] = attribute 747 | return update_content(self.body_state, database=self._database) 748 | else: 749 | code_of_target = target[len(target) - 1] 750 | if not code_of_target: 751 | code_of_target = " " 752 | return process_code_adding(self.body_state, 753 | code_of_target=code_of_target, database=self._database) 754 | 755 | def update_tag_for_dict(self): 756 | """ 757 | Retrieves set tags for a certain dict value first, displays them 758 | and processes input (add or delete tags). 759 | """ 760 | 761 | all_tags = self._database.get_tags(self.body_state) 762 | 763 | if all_tags: 764 | 765 | print "\nFollowing tags are set for problem '{0}' :".format(self.body_state['problem']) 766 | 767 | for tag in all_tags: 768 | print "'" + tag[0] + "' ", 769 | print "" 770 | else: 771 | print "\nNo tags set." 772 | 773 | tag_input = "" 774 | while not (tag_input.startswith('+') or tag_input.startswith('-')): 775 | tag_input = process_and_validate_input( 776 | "Add or remove a tag with '+'TAGNAME or '-'TAGNAME : ") 777 | 778 | self.body_state['tag_name'] = tag_input[1:] 779 | if tag_input[0] == '+': 780 | self._database.update_tags(self.body_state, 'add') 781 | print "Adding tag {0} successful.".format(self.body_state['tag_name']) 782 | else: 783 | self._database.update_tags(self.body_state, 'del') 784 | print "Deleting tag {0} successful.".format(self.body_state['tag_name']) 785 | sys.exit(0) 786 | 787 | def _link_determine_operation(self, target, attribute): 788 | """ 789 | Determines what operation to do on link table. 790 | """ 791 | 792 | # default - run link in webbrowser 793 | if attribute == 'link': 794 | requested_url = target[2] 795 | if not requested_url.startswith('http'): 796 | requested_url = "http://" + requested_url 797 | run_webbrowser(requested_url) 798 | 799 | elif attribute == 'del': 800 | self.body_state['url'] = target[2] 801 | self._database.delete_links(self.body_state) 802 | print "Deleting link {0} successful.".format(target[1].encode('utf-8')) 803 | sys.exit(0) 804 | 805 | # language to link 806 | elif attribute == 'lang': 807 | self.body_state['attribute'] = "language" 808 | self.body_state['data'] = process_and_validate_input("Change "+ 809 | self.body_state['attribute'] + " to : ") 810 | else: 811 | # attribute = name 812 | self.body_state['attribute'] = "name" 813 | self.body_state['data'] = process_and_validate_input("Change "+ 814 | self.body_state['attribute'] + " to : ") 815 | 816 | self.body_state['link_name'] = target[1] 817 | self.body_state['url'] = target[2] 818 | self.body_state['original-lang'] = target[-1] 819 | self._database.upsert_links(self.body_state, operation_type='upsert') 820 | print "Changing link attribute {0} successful.".format( 821 | self.body_state['attribute'].encode('utf-8')) 822 | sys.exit(0) 823 | 824 | 825 | ## HELPER 826 | 827 | def process_and_validate_input(prompt): 828 | """ 829 | Processes trivial input and validates it. Returns the user's input. 830 | """ 831 | 832 | valid_input = False 833 | while not valid_input: 834 | try: 835 | user_input = unicode(raw_input(prompt).strip(), 'utf-8') 836 | valid_input = True 837 | except UnicodeError as error: 838 | print error 839 | return user_input 840 | 841 | 842 | def build_args_dict(flags): 843 | """ 844 | Determines and sets hline and cutsearch as well as links based 845 | if they are present in flags / body. --> Is needed for building table. 846 | """ 847 | 848 | args_dict = {} 849 | 850 | if '--hline' in flags: 851 | args_dict['hline'] = True 852 | 853 | if '-l' in flags: 854 | args_dict['link'] = True 855 | 856 | return args_dict 857 | 858 | 859 | def unicode_everything(input_dict): 860 | """ 861 | Converts every value of the input_dict dict which contains strings into unicode. 862 | """ 863 | 864 | for key, value in input_dict.iteritems(): 865 | if type(value) is str: 866 | try: 867 | input_dict[key] = unicode(value, 'utf-8') 868 | except UnicodeError as error: 869 | print error 870 | input_dict[key] = process_and_validate_input("Enter " + key + " again: ") 871 | return input_dict 872 | 873 | 874 | def read_and_delete_tmpfile(file_name): 875 | """ 876 | Reads from tmpfile and tries to delete it afterwards. 877 | """ 878 | 879 | with open(file_name) as my_file: 880 | try: 881 | file_output = my_file.read() 882 | except (IOError, OSError) as error: 883 | print error 884 | sys.exit(1) 885 | try: 886 | os.remove(file_name) 887 | except OSError: 888 | print "Couldn't delete temporary file." 889 | 890 | return file_output 891 | 892 | 893 | def show_tags(body, flags): 894 | """ 895 | Prints all tags, which are set for a certain language to console. 896 | (Gives option to remove some of them entirely) 897 | """ 898 | 899 | database = db.Database() 900 | all_tags = database.get_tags(body) 901 | 902 | if not all_tags: 903 | print "No tags set for language {0}.".format(body['language']) 904 | else: 905 | print "Following tags are set for language '{0}' :".format(body['language']) 906 | 907 | for tag in all_tags: 908 | print "'" + tag[0] + "' ", 909 | 910 | print "\n" 911 | 912 | tag_input = process_and_validate_input( 913 | """Remove a tag entirely with '-'TAGNAME or \ntype its name to get all associated items: """) 914 | 915 | if tag_input[0] == '-': 916 | body['tag_name'] = tag_input[1:] 917 | database.delete_tag(body) 918 | print "Deleting tag {0} entirely successful.".format(body['tag_name']) 919 | sys.exit(0) 920 | else: 921 | print "" 922 | body['searchpattern'] = tag_input 923 | flags['display'] = True 924 | print body, flags 925 | determine_display_operation(body, flags) 926 | 927 | ## IMPORTING AND EXPORTING 928 | 929 | def process_export(body): 930 | """ 931 | Exports the contents of at least one entry as XML, according to the 932 | language and tags. 933 | """ 934 | database = db.Database() 935 | 936 | output_file = body['path-to-file'] 937 | language = body['language'] 938 | 939 | raw_tags = process_and_validate_input("Enter the tags to export - separated with ';' : ") 940 | tags = [tag.strip() for tag in raw_tags.split(';')] 941 | 942 | if len(tags) == 1 and not tags[0]: 943 | # This actually empty input - we don't want to search for 944 | # 'the empty tag', but rather place no restriction on what tags we export. 945 | tags = [] 946 | 947 | entries = database.get_full_dump(language, tags) 948 | 949 | root_element = ElementTree.Element('codedict', {'language': language}) 950 | for entry in entries: 951 | entry_root = ElementTree.SubElement(root_element, 'entry') 952 | 953 | entry_problem = ElementTree.SubElement(entry_root, 'problem') 954 | entry_problem.text = entry.problem 955 | 956 | entry_solution = ElementTree.SubElement(entry_root, 'solution') 957 | entry_solution.text = entry.solution 958 | 959 | for tag in entry.tags: 960 | ElementTree.SubElement(entry_root, 'tag', {'value': tag}) 961 | 962 | tree = ElementTree.ElementTree(root_element) 963 | tree.write(output_file) 964 | 965 | def construct_entry_from_node(node): 966 | """ 967 | Constructs a DumpEntry object from an ElementTree Element. 968 | 969 | Note that the language isn't actually defined, since that is given with the 970 | root of the XML hierarchy, and not each individual element. 971 | """ 972 | language = None 973 | problem = node.find('problem').text 974 | solution = node.find('solution').text 975 | 976 | # This has to be a frozenset, since we want to put the DumpEntry as a whole 977 | # in a set later 978 | tags = frozenset(tag_elem.get('value') for tag_elem in node.findall('tag')) 979 | return db.DumpEntry(language, tags, problem, solution) 980 | 981 | def get_indicies(selection, entries): 982 | """ 983 | Converts a selection (a number, range, or '*') into a list of numeric indices 984 | over a list of entries. 985 | 986 | Note that the indices the user is giving start at 1, not at 0. 987 | """ 988 | if selection == '*': 989 | return range(len(entries)) 990 | elif '-' in selection: 991 | start, end = [int(boundary) for boundary in selection.split('-')] 992 | if start > len(entries): 993 | raise ValueError('Cannot have a range which starts after the end') 994 | if end > len(entries): 995 | raise ValueError('Cannot have a range which ends after the end') 996 | 997 | return range(start - 1, end) 998 | else: 999 | return [int(selection) - 1] 1000 | 1001 | def process_import(body): 1002 | """ 1003 | Imports the contents of an XML file into the database. Note that the user 1004 | is first given a choice of what contents to import and export. 1005 | """ 1006 | database = db.Database() 1007 | 1008 | # First, slurp in the XML file and convert it into a form we can use - 1009 | # a list of DumpEntry objects as returned by Database.get_full_dump 1010 | xml_tree = ElementTree.parse(body['path-to-file']) 1011 | xml_root = xml_tree.getroot() 1012 | 1013 | language = xml_root.get('language') 1014 | 1015 | entries = [construct_entry_from_node(xml_entry) for xml_entry in xml_root] 1016 | selections = set(entries) 1017 | 1018 | while True: 1019 | # Before asking the user to decide anything, give them an updated view 1020 | # of the table 1021 | printed_entries = [ 1022 | (str(index + 1), str(entry in selections), entry.language, ';'.join(entry.tags), entry.problem, 1023 | entry.solution) 1024 | for index, entry in enumerate(entries) 1025 | ] 1026 | 1027 | linelen = get_console_length(database) 1028 | _, table = build_table(['Index', 'Importing?', 'Language', 'Tags', 'Problem', 'Solution'], 1029 | printed_entries, 1030 | linelen, 1031 | {}) 1032 | process_printing(table) 1033 | 1034 | HELP = ''' 1035 | Available actions are: 1036 | 1037 | go runs the import, and pulls all selected items into your database. 1038 | 1039 | abort stops the import, and does not modify your database. 1040 | 1041 | +{selection} adds the selected rows to the import set. 1042 | {selection} can either be a number, a range (like 1-10), or '*' 1043 | which means all rows. 1044 | 1045 | -{selection} removes the selected rows from the import set. 1046 | 1047 | help shows this text. 1048 | ''' 1049 | 1050 | action = raw_input('Enter a command [go, abort, +{selection}, -{selection}, help]: ') 1051 | action = action.strip() 1052 | 1053 | try: 1054 | if action[0] == '+': 1055 | rows = get_indicies(action[1:], entries) 1056 | action_entries = set(entries[idx] for idx in rows) 1057 | 1058 | selections |= action_entries 1059 | elif action[0] == '-': 1060 | rows = get_indicies(action[1:], entries) 1061 | action_entries = set(entries[idx] for idx in rows) 1062 | 1063 | selections -= action_entries 1064 | elif action == 'go': 1065 | break 1066 | elif action == 'abort': 1067 | return 1068 | else: 1069 | print HELP 1070 | except ValueError as err: 1071 | print('Error:', err) 1072 | 1073 | # We've got confirmation, and we can go ahead and push everything to the 1074 | # DB. We need to massage the input to get it to conform to the DB module's 1075 | # expectations before we can do that. 1076 | db_rows = [(';'.join(entry.tags), entry.problem, entry.solution) 1077 | for entry in entries] 1078 | 1079 | database.add_content(db_rows, language, insert_type='from_file') 1080 | print 'Successfully inserted', len(db_rows), 'entries' 1081 | -------------------------------------------------------------------------------- /update-md5.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd source 3 | md5sum codedict database.py __init__.py processor.py > checksums.md5 4 | --------------------------------------------------------------------------------