diff options
author | Marc Abramowitz <marc@marc-abramowitz.com> | 2016-03-07 18:52:36 -0800 |
---|---|---|
committer | Marc Abramowitz <marc@marc-abramowitz.com> | 2016-03-07 18:52:36 -0800 |
commit | cc83e06efff71b81ca5a3ac6df65775971181295 (patch) | |
tree | d52fa3f1a93730f263c2c5ac8266de8e5fb12abf /paste/util | |
download | paste-git-tox_coverage.tar.gz |
tox.ini: Measure test coveragetox_coverage
Diffstat (limited to 'paste/util')
-rw-r--r-- | paste/util/PySourceColor.py | 2102 | ||||
-rw-r--r-- | paste/util/__init__.py | 4 | ||||
-rw-r--r-- | paste/util/classinit.py | 42 | ||||
-rw-r--r-- | paste/util/classinstance.py | 38 | ||||
-rw-r--r-- | paste/util/converters.py | 30 | ||||
-rw-r--r-- | paste/util/dateinterval.py | 104 | ||||
-rw-r--r-- | paste/util/datetimeutil.py | 359 | ||||
-rw-r--r-- | paste/util/filemixin.py | 53 | ||||
-rw-r--r-- | paste/util/finddata.py | 98 | ||||
-rw-r--r-- | paste/util/findpackage.py | 26 | ||||
-rw-r--r-- | paste/util/import_string.py | 95 | ||||
-rw-r--r-- | paste/util/intset.py | 515 | ||||
-rw-r--r-- | paste/util/ip4.py | 274 | ||||
-rw-r--r-- | paste/util/killthread.py | 30 | ||||
-rw-r--r-- | paste/util/looper.py | 156 | ||||
-rw-r--r-- | paste/util/mimeparse.py | 160 | ||||
-rw-r--r-- | paste/util/multidict.py | 429 | ||||
-rw-r--r-- | paste/util/quoting.py | 85 | ||||
-rw-r--r-- | paste/util/scgiserver.py | 172 | ||||
-rw-r--r-- | paste/util/template.py | 756 | ||||
-rw-r--r-- | paste/util/threadedprint.py | 250 | ||||
-rw-r--r-- | paste/util/threadinglocal.py | 43 |
22 files changed, 5821 insertions, 0 deletions
diff --git a/paste/util/PySourceColor.py b/paste/util/PySourceColor.py new file mode 100644 index 0000000..c576ead --- /dev/null +++ b/paste/util/PySourceColor.py @@ -0,0 +1,2102 @@ +# -*- coding: Latin-1 -*- +""" +PySourceColor: color Python source code +""" + +""" + PySourceColor.py + +---------------------------------------------------------------------------- + + A python source to colorized html/css/xhtml converter. + Hacked by M.E.Farmer Jr. 2004, 2005 + Python license + +---------------------------------------------------------------------------- + + - HTML markup does not create w3c valid html, but it works on every + browser i've tried so far.(I.E.,Mozilla/Firefox,Opera,Konqueror,wxHTML). + - CSS markup is w3c validated html 4.01 strict, + but will not render correctly on all browsers. + - XHTML markup is w3c validated xhtml 1.0 strict, + like html 4.01, will not render correctly on all browsers. + +---------------------------------------------------------------------------- + +Features: + + -Three types of markup: + html (default) + css/html 4.01 strict + xhtml 1.0 strict + + -Can tokenize and colorize: + 12 types of strings + 2 comment types + numbers + operators + brackets + math operators + class / name + def / name + decorator / name + keywords + arguments class/def/decorator + linenumbers + names + text + + -Eight colorschemes built-in: + null + mono + lite (default) + dark + dark2 + idle + viewcvs + pythonwin + + -Header and footer + set to '' for builtin header / footer. + give path to a file containing the html + you want added as header or footer. + + -Arbitrary text and html + html markup converts all to raw (TEXT token) + #@# for raw -> send raw text. + #$# for span -> inline html and text. + #%# for div -> block level html and text. + + -Linenumbers + Supports all styles. New token is called LINENUMBER. + Defaults to NAME if not defined. + + Style options + + -ALL markups support these text styles: + b = bold + i = italic + u = underline + -CSS and XHTML has limited support for borders: + HTML markup functions will ignore these. + Optional: Border color in RGB hex + Defaults to the text forecolor. + #rrggbb = border color + Border size: + l = thick + m = medium + t = thin + Border type: + - = dashed + . = dotted + s = solid + d = double + g = groove + r = ridge + n = inset + o = outset + You can specify multiple sides, + they will all use the same style. + Optional: Default is full border. + v = bottom + < = left + > = right + ^ = top + NOTE: Specify the styles you want. + The markups will ignore unsupported styles + Also note not all browsers can show these options + + -All tokens default to NAME if not defined + so the only absolutely critical ones to define are: + NAME, ERRORTOKEN, PAGEBACKGROUND + +---------------------------------------------------------------------------- + +Example usage:: + + # import + import PySourceColor as psc + psc.convert('c:/Python22/PySourceColor.py', colors=psc.idle, show=1) + + # from module import * + from PySourceColor import * + convert('c:/Python22/Lib', colors=lite, markup="css", + header='#$#<b>This is a simpe heading</b><hr/>') + + # How to use a custom colorscheme, and most of the 'features' + from PySourceColor import * + new = { + ERRORTOKEN: ('bui','#FF8080',''), + DECORATOR_NAME: ('s','#AACBBC',''), + DECORATOR: ('n','#333333',''), + NAME: ('t.<v','#1133AA','#DDFF22'), + NUMBER: ('','#236676','#FF5555'), + OPERATOR: ('b','#454567','#BBBB11'), + MATH_OPERATOR: ('','#935623','#423afb'), + BRACKETS: ('b','#ac34bf','#6457a5'), + COMMENT: ('t-#0022FF','#545366','#AABBFF'), + DOUBLECOMMENT: ('<l#553455','#553455','#FF00FF'), + CLASS_NAME: ('m^v-','#000000','#FFFFFF'), + DEF_NAME: ('l=<v','#897845','#000022'), + KEYWORD: ('.b','#345345','#FFFF22'), + SINGLEQUOTE: ('mn','#223344','#AADDCC'), + SINGLEQUOTE_R: ('','#344522',''), + SINGLEQUOTE_U: ('','#234234',''), + DOUBLEQUOTE: ('m#0022FF','#334421',''), + DOUBLEQUOTE_R: ('','#345345',''), + DOUBLEQUOTE_U: ('','#678673',''), + TRIPLESINGLEQUOTE: ('tv','#FFFFFF','#000000'), + TRIPLESINGLEQUOTE_R: ('tbu','#443256','#DDFFDA'), + TRIPLESINGLEQUOTE_U: ('','#423454','#DDFFDA'), + TRIPLEDOUBLEQUOTE: ('li#236fd3b<>','#000000','#FFFFFF'), + TRIPLEDOUBLEQUOTE_R: ('tub','#000000','#FFFFFF'), + TRIPLEDOUBLEQUOTE_U: ('-', '#CCAABB','#FFFAFF'), + LINENUMBER: ('ib-','#ff66aa','#7733FF'),] + TEXT: ('','#546634',''), + PAGEBACKGROUND: '#FFFAAA', + } + if __name__ == '__main__': + import sys + convert(sys.argv[1], './xhtml.html', colors=new, markup='xhtml', show=1, + linenumbers=1) + convert(sys.argv[1], './html.html', colors=new, markup='html', show=1, + linenumbers=1) + +""" + +__all__ = ['ERRORTOKEN','DECORATOR_NAME', 'DECORATOR', 'ARGS', 'EXTRASPACE', + 'NAME', 'NUMBER', 'OPERATOR', 'COMMENT', 'MATH_OPERATOR', + 'DOUBLECOMMENT', 'CLASS_NAME', 'DEF_NAME', 'KEYWORD', 'BRACKETS', + 'SINGLEQUOTE','SINGLEQUOTE_R','SINGLEQUOTE_U','DOUBLEQUOTE', + 'DOUBLEQUOTE_R', 'DOUBLEQUOTE_U', 'TRIPLESINGLEQUOTE', 'TEXT', + 'TRIPLESINGLEQUOTE_R', 'TRIPLESINGLEQUOTE_U', 'TRIPLEDOUBLEQUOTE', + 'TRIPLEDOUBLEQUOTE_R', 'TRIPLEDOUBLEQUOTE_U', 'PAGEBACKGROUND', + 'LINENUMBER', 'CODESTART', 'CODEEND', 'PY', 'TOKEN_NAMES', 'CSSHOOK', + 'null', 'mono', 'lite', 'dark','dark2', 'pythonwin','idle', + 'viewcvs', 'Usage', 'cli', 'str2stdout', 'path2stdout', 'Parser', + 'str2file', 'str2html', 'str2css', 'str2markup', 'path2file', + 'path2html', 'convert', 'walkdir', 'defaultColors', 'showpage', + 'pageconvert','tagreplace', 'MARKUPDICT'] +__title__ = 'PySourceColor' +__version__ = "2.1a" +__date__ = '25 April 2005' +__author__ = "M.E.Farmer Jr." +__credits__ = '''This was originally based on a python recipe +submitted by Jürgen Hermann to ASPN. Now based on the voices in my head. +M.E.Farmer 2004, 2005 +Python license +''' +import os +import sys +import time +import glob +import getopt +import keyword +import token +import tokenize +import traceback +from six.moves import cStringIO as StringIO +# Do not edit +NAME = token.NAME +NUMBER = token.NUMBER +COMMENT = tokenize.COMMENT +OPERATOR = token.OP +ERRORTOKEN = token.ERRORTOKEN +ARGS = token.NT_OFFSET + 1 +DOUBLECOMMENT = token.NT_OFFSET + 2 +CLASS_NAME = token.NT_OFFSET + 3 +DEF_NAME = token.NT_OFFSET + 4 +KEYWORD = token.NT_OFFSET + 5 +SINGLEQUOTE = token.NT_OFFSET + 6 +SINGLEQUOTE_R = token.NT_OFFSET + 7 +SINGLEQUOTE_U = token.NT_OFFSET + 8 +DOUBLEQUOTE = token.NT_OFFSET + 9 +DOUBLEQUOTE_R = token.NT_OFFSET + 10 +DOUBLEQUOTE_U = token.NT_OFFSET + 11 +TRIPLESINGLEQUOTE = token.NT_OFFSET + 12 +TRIPLESINGLEQUOTE_R = token.NT_OFFSET + 13 +TRIPLESINGLEQUOTE_U = token.NT_OFFSET + 14 +TRIPLEDOUBLEQUOTE = token.NT_OFFSET + 15 +TRIPLEDOUBLEQUOTE_R = token.NT_OFFSET + 16 +TRIPLEDOUBLEQUOTE_U = token.NT_OFFSET + 17 +PAGEBACKGROUND = token.NT_OFFSET + 18 +DECORATOR = token.NT_OFFSET + 19 +DECORATOR_NAME = token.NT_OFFSET + 20 +BRACKETS = token.NT_OFFSET + 21 +MATH_OPERATOR = token.NT_OFFSET + 22 +LINENUMBER = token.NT_OFFSET + 23 +TEXT = token.NT_OFFSET + 24 +PY = token.NT_OFFSET + 25 +CODESTART = token.NT_OFFSET + 26 +CODEEND = token.NT_OFFSET + 27 +CSSHOOK = token.NT_OFFSET + 28 +EXTRASPACE = token.NT_OFFSET + 29 + +# markup classname lookup +MARKUPDICT = { + ERRORTOKEN: 'py_err', + DECORATOR_NAME: 'py_decn', + DECORATOR: 'py_dec', + ARGS: 'py_args', + NAME: 'py_name', + NUMBER: 'py_num', + OPERATOR: 'py_op', + COMMENT: 'py_com', + DOUBLECOMMENT: 'py_dcom', + CLASS_NAME: 'py_clsn', + DEF_NAME: 'py_defn', + KEYWORD: 'py_key', + SINGLEQUOTE: 'py_sq', + SINGLEQUOTE_R: 'py_sqr', + SINGLEQUOTE_U: 'py_squ', + DOUBLEQUOTE: 'py_dq', + DOUBLEQUOTE_R: 'py_dqr', + DOUBLEQUOTE_U: 'py_dqu', + TRIPLESINGLEQUOTE: 'py_tsq', + TRIPLESINGLEQUOTE_R: 'py_tsqr', + TRIPLESINGLEQUOTE_U: 'py_tsqu', + TRIPLEDOUBLEQUOTE: 'py_tdq', + TRIPLEDOUBLEQUOTE_R: 'py_tdqr', + TRIPLEDOUBLEQUOTE_U: 'py_tdqu', + BRACKETS: 'py_bra', + MATH_OPERATOR: 'py_mop', + LINENUMBER: 'py_lnum', + TEXT: 'py_text', + } +# might help users that want to create custom schemes +TOKEN_NAMES= { + ERRORTOKEN:'ERRORTOKEN', + DECORATOR_NAME:'DECORATOR_NAME', + DECORATOR:'DECORATOR', + ARGS:'ARGS', + NAME:'NAME', + NUMBER:'NUMBER', + OPERATOR:'OPERATOR', + COMMENT:'COMMENT', + DOUBLECOMMENT:'DOUBLECOMMENT', + CLASS_NAME:'CLASS_NAME', + DEF_NAME:'DEF_NAME', + KEYWORD:'KEYWORD', + SINGLEQUOTE:'SINGLEQUOTE', + SINGLEQUOTE_R:'SINGLEQUOTE_R', + SINGLEQUOTE_U:'SINGLEQUOTE_U', + DOUBLEQUOTE:'DOUBLEQUOTE', + DOUBLEQUOTE_R:'DOUBLEQUOTE_R', + DOUBLEQUOTE_U:'DOUBLEQUOTE_U', + TRIPLESINGLEQUOTE:'TRIPLESINGLEQUOTE', + TRIPLESINGLEQUOTE_R:'TRIPLESINGLEQUOTE_R', + TRIPLESINGLEQUOTE_U:'TRIPLESINGLEQUOTE_U', + TRIPLEDOUBLEQUOTE:'TRIPLEDOUBLEQUOTE', + TRIPLEDOUBLEQUOTE_R:'TRIPLEDOUBLEQUOTE_R', + TRIPLEDOUBLEQUOTE_U:'TRIPLEDOUBLEQUOTE_U', + BRACKETS:'BRACKETS', + MATH_OPERATOR:'MATH_OPERATOR', + LINENUMBER:'LINENUMBER', + TEXT:'TEXT', + PAGEBACKGROUND:'PAGEBACKGROUND', + } + +###################################################################### +# Edit colors and styles to taste +# Create your own scheme, just copy one below , rename and edit. +# Custom styles must at least define NAME, ERRORTOKEN, PAGEBACKGROUND, +# all missing elements will default to NAME. +# See module docstring for details on style attributes. +###################################################################### +# Copy null and use it as a starter colorscheme. +null = {# tokentype: ('tags border_color', 'textforecolor', 'textbackcolor') + ERRORTOKEN: ('','#000000',''),# Error token + DECORATOR_NAME: ('','#000000',''),# Decorator name + DECORATOR: ('','#000000',''),# @ symbol + ARGS: ('','#000000',''),# class,def,deco arguments + NAME: ('','#000000',''),# All other python text + NUMBER: ('','#000000',''),# 0->10 + OPERATOR: ('','#000000',''),# ':','<=',';',',','.','==', etc + MATH_OPERATOR: ('','#000000',''),# '+','-','=','','**',etc + BRACKETS: ('','#000000',''),# '[',']','(',')','{','}' + COMMENT: ('','#000000',''),# Single comment + DOUBLECOMMENT: ('','#000000',''),## Double comment + CLASS_NAME: ('','#000000',''),# Class name + DEF_NAME: ('','#000000',''),# Def name + KEYWORD: ('','#000000',''),# Python keywords + SINGLEQUOTE: ('','#000000',''),# 'SINGLEQUOTE' + SINGLEQUOTE_R: ('','#000000',''),# r'SINGLEQUOTE' + SINGLEQUOTE_U: ('','#000000',''),# u'SINGLEQUOTE' + DOUBLEQUOTE: ('','#000000',''),# "DOUBLEQUOTE" + DOUBLEQUOTE_R: ('','#000000',''),# r"DOUBLEQUOTE" + DOUBLEQUOTE_U: ('','#000000',''),# u"DOUBLEQUOTE" + TRIPLESINGLEQUOTE: ('','#000000',''),# '''TRIPLESINGLEQUOTE''' + TRIPLESINGLEQUOTE_R: ('','#000000',''),# r'''TRIPLESINGLEQUOTE''' + TRIPLESINGLEQUOTE_U: ('','#000000',''),# u'''TRIPLESINGLEQUOTE''' + TRIPLEDOUBLEQUOTE: ('','#000000',''),# """TRIPLEDOUBLEQUOTE""" + TRIPLEDOUBLEQUOTE_R: ('','#000000',''),# r"""TRIPLEDOUBLEQUOTE""" + TRIPLEDOUBLEQUOTE_U: ('','#000000',''),# u"""TRIPLEDOUBLEQUOTE""" + TEXT: ('','#000000',''),# non python text + LINENUMBER: ('>ti#555555','#000000',''),# Linenumbers + PAGEBACKGROUND: '#FFFFFF'# set the page background + } + +mono = { + ERRORTOKEN: ('s#FF0000','#FF8080',''), + DECORATOR_NAME: ('bu','#000000',''), + DECORATOR: ('b','#000000',''), + ARGS: ('b','#555555',''), + NAME: ('','#000000',''), + NUMBER: ('b','#000000',''), + OPERATOR: ('b','#000000',''), + MATH_OPERATOR: ('b','#000000',''), + BRACKETS: ('b','#000000',''), + COMMENT: ('i','#999999',''), + DOUBLECOMMENT: ('b','#999999',''), + CLASS_NAME: ('bu','#000000',''), + DEF_NAME: ('b','#000000',''), + KEYWORD: ('b','#000000',''), + SINGLEQUOTE: ('','#000000',''), + SINGLEQUOTE_R: ('','#000000',''), + SINGLEQUOTE_U: ('','#000000',''), + DOUBLEQUOTE: ('','#000000',''), + DOUBLEQUOTE_R: ('','#000000',''), + DOUBLEQUOTE_U: ('','#000000',''), + TRIPLESINGLEQUOTE: ('','#000000',''), + TRIPLESINGLEQUOTE_R: ('','#000000',''), + TRIPLESINGLEQUOTE_U: ('','#000000',''), + TRIPLEDOUBLEQUOTE: ('i','#000000',''), + TRIPLEDOUBLEQUOTE_R: ('i','#000000',''), + TRIPLEDOUBLEQUOTE_U: ('i','#000000',''), + TEXT: ('','#000000',''), + LINENUMBER: ('>ti#555555','#000000',''), + PAGEBACKGROUND: '#FFFFFF' + } + +dark = { + ERRORTOKEN: ('s#FF0000','#FF8080',''), + DECORATOR_NAME: ('b','#FFBBAA',''), + DECORATOR: ('b','#CC5511',''), + ARGS: ('b','#DDDDFF',''), + NAME: ('','#DDDDDD',''), + NUMBER: ('','#FF0000',''), + OPERATOR: ('b','#FAF785',''), + MATH_OPERATOR: ('b','#FAF785',''), + BRACKETS: ('b','#FAF785',''), + COMMENT: ('','#45FCA0',''), + DOUBLECOMMENT: ('i','#A7C7A9',''), + CLASS_NAME: ('b','#B666FD',''), + DEF_NAME: ('b','#EBAE5C',''), + KEYWORD: ('b','#8680FF',''), + SINGLEQUOTE: ('','#F8BAFE',''), + SINGLEQUOTE_R: ('','#F8BAFE',''), + SINGLEQUOTE_U: ('','#F8BAFE',''), + DOUBLEQUOTE: ('','#FF80C0',''), + DOUBLEQUOTE_R: ('','#FF80C0',''), + DOUBLEQUOTE_U: ('','#FF80C0',''), + TRIPLESINGLEQUOTE: ('','#FF9595',''), + TRIPLESINGLEQUOTE_R: ('','#FF9595',''), + TRIPLESINGLEQUOTE_U: ('','#FF9595',''), + TRIPLEDOUBLEQUOTE: ('','#B3FFFF',''), + TRIPLEDOUBLEQUOTE_R: ('','#B3FFFF',''), + TRIPLEDOUBLEQUOTE_U: ('','#B3FFFF',''), + TEXT: ('','#FFFFFF',''), + LINENUMBER: ('>mi#555555','#bbccbb','#333333'), + PAGEBACKGROUND: '#000000' + } + +dark2 = { + ERRORTOKEN: ('','#FF0000',''), + DECORATOR_NAME: ('b','#FFBBAA',''), + DECORATOR: ('b','#CC5511',''), + ARGS: ('b','#DDDDDD',''), + NAME: ('','#C0C0C0',''), + NUMBER: ('b','#00FF00',''), + OPERATOR: ('b','#FF090F',''), + MATH_OPERATOR: ('b','#EE7020',''), + BRACKETS: ('b','#FFB90F',''), + COMMENT: ('i','#D0D000','#522000'),#'#88AA88','#11111F'), + DOUBLECOMMENT: ('i','#D0D000','#522000'),#'#77BB77','#11111F'), + CLASS_NAME: ('b','#DD4080',''), + DEF_NAME: ('b','#FF8040',''), + KEYWORD: ('b','#4726d1',''), + SINGLEQUOTE: ('','#8080C0',''), + SINGLEQUOTE_R: ('','#8080C0',''), + SINGLEQUOTE_U: ('','#8080C0',''), + DOUBLEQUOTE: ('','#ADB9F1',''), + DOUBLEQUOTE_R: ('','#ADB9F1',''), + DOUBLEQUOTE_U: ('','#ADB9F1',''), + TRIPLESINGLEQUOTE: ('','#00C1C1',''),#A050C0 + TRIPLESINGLEQUOTE_R: ('','#00C1C1',''),#A050C0 + TRIPLESINGLEQUOTE_U: ('','#00C1C1',''),#A050C0 + TRIPLEDOUBLEQUOTE: ('','#33E3E3',''),#B090E0 + TRIPLEDOUBLEQUOTE_R: ('','#33E3E3',''),#B090E0 + TRIPLEDOUBLEQUOTE_U: ('','#33E3E3',''),#B090E0 + TEXT: ('','#C0C0C0',''), + LINENUMBER: ('>mi#555555','#bbccbb','#333333'), + PAGEBACKGROUND: '#000000' + } + +lite = { + ERRORTOKEN: ('s#FF0000','#FF8080',''), + DECORATOR_NAME: ('b','#BB4422',''), + DECORATOR: ('b','#3333AF',''), + ARGS: ('b','#000000',''), + NAME: ('','#333333',''), + NUMBER: ('b','#DD2200',''), + OPERATOR: ('b','#000000',''), + MATH_OPERATOR: ('b','#000000',''), + BRACKETS: ('b','#000000',''), + COMMENT: ('','#007F00',''), + DOUBLECOMMENT: ('','#608060',''), + CLASS_NAME: ('b','#0000DF',''), + DEF_NAME: ('b','#9C7A00',''),#f09030 + KEYWORD: ('b','#0000AF',''), + SINGLEQUOTE: ('','#600080',''), + SINGLEQUOTE_R: ('','#600080',''), + SINGLEQUOTE_U: ('','#600080',''), + DOUBLEQUOTE: ('','#A0008A',''), + DOUBLEQUOTE_R: ('','#A0008A',''), + DOUBLEQUOTE_U: ('','#A0008A',''), + TRIPLESINGLEQUOTE: ('','#337799',''), + TRIPLESINGLEQUOTE_R: ('','#337799',''), + TRIPLESINGLEQUOTE_U: ('','#337799',''), + TRIPLEDOUBLEQUOTE: ('','#1166AA',''), + TRIPLEDOUBLEQUOTE_R: ('','#1166AA',''), + TRIPLEDOUBLEQUOTE_U: ('','#1166AA',''), + TEXT: ('','#000000',''), + LINENUMBER: ('>ti#555555','#000000',''), + PAGEBACKGROUND: '#FFFFFF' + } + +idle = { + ERRORTOKEN: ('s#FF0000','#FF8080',''), + DECORATOR_NAME: ('','#900090',''), + DECORATOR: ('','#FF7700',''), + NAME: ('','#000000',''), + NUMBER: ('','#000000',''), + OPERATOR: ('','#000000',''), + MATH_OPERATOR: ('','#000000',''), + BRACKETS: ('','#000000',''), + COMMENT: ('','#DD0000',''), + DOUBLECOMMENT: ('','#DD0000',''), + CLASS_NAME: ('','#0000FF',''), + DEF_NAME: ('','#0000FF',''), + KEYWORD: ('','#FF7700',''), + SINGLEQUOTE: ('','#00AA00',''), + SINGLEQUOTE_R: ('','#00AA00',''), + SINGLEQUOTE_U: ('','#00AA00',''), + DOUBLEQUOTE: ('','#00AA00',''), + DOUBLEQUOTE_R: ('','#00AA00',''), + DOUBLEQUOTE_U: ('','#00AA00',''), + TRIPLESINGLEQUOTE: ('','#00AA00',''), + TRIPLESINGLEQUOTE_R: ('','#00AA00',''), + TRIPLESINGLEQUOTE_U: ('','#00AA00',''), + TRIPLEDOUBLEQUOTE: ('','#00AA00',''), + TRIPLEDOUBLEQUOTE_R: ('','#00AA00',''), + TRIPLEDOUBLEQUOTE_U: ('','#00AA00',''), + TEXT: ('','#000000',''), + LINENUMBER: ('>ti#555555','#000000',''), + PAGEBACKGROUND: '#FFFFFF' + } + +pythonwin = { + ERRORTOKEN: ('s#FF0000','#FF8080',''), + DECORATOR_NAME: ('b','#DD0080',''), + DECORATOR: ('b','#000080',''), + ARGS: ('','#000000',''), + NAME: ('','#303030',''), + NUMBER: ('','#008080',''), + OPERATOR: ('','#000000',''), + MATH_OPERATOR: ('','#000000',''), + BRACKETS: ('','#000000',''), + COMMENT: ('','#007F00',''), + DOUBLECOMMENT: ('','#7F7F7F',''), + CLASS_NAME: ('b','#0000FF',''), + DEF_NAME: ('b','#007F7F',''), + KEYWORD: ('b','#000080',''), + SINGLEQUOTE: ('','#808000',''), + SINGLEQUOTE_R: ('','#808000',''), + SINGLEQUOTE_U: ('','#808000',''), + DOUBLEQUOTE: ('','#808000',''), + DOUBLEQUOTE_R: ('','#808000',''), + DOUBLEQUOTE_U: ('','#808000',''), + TRIPLESINGLEQUOTE: ('','#808000',''), + TRIPLESINGLEQUOTE_R: ('','#808000',''), + TRIPLESINGLEQUOTE_U: ('','#808000',''), + TRIPLEDOUBLEQUOTE: ('','#808000',''), + TRIPLEDOUBLEQUOTE_R: ('','#808000',''), + TRIPLEDOUBLEQUOTE_U: ('','#808000',''), + TEXT: ('','#303030',''), + LINENUMBER: ('>ti#555555','#000000',''), + PAGEBACKGROUND: '#FFFFFF' + } + +viewcvs = { + ERRORTOKEN: ('s#FF0000','#FF8080',''), + DECORATOR_NAME: ('','#000000',''), + DECORATOR: ('','#000000',''), + ARGS: ('','#000000',''), + NAME: ('','#000000',''), + NUMBER: ('','#000000',''), + OPERATOR: ('','#000000',''), + MATH_OPERATOR: ('','#000000',''), + BRACKETS: ('','#000000',''), + COMMENT: ('i','#b22222',''), + DOUBLECOMMENT: ('i','#b22222',''), + CLASS_NAME: ('','#000000',''), + DEF_NAME: ('b','#0000ff',''), + KEYWORD: ('b','#a020f0',''), + SINGLEQUOTE: ('b','#bc8f8f',''), + SINGLEQUOTE_R: ('b','#bc8f8f',''), + SINGLEQUOTE_U: ('b','#bc8f8f',''), + DOUBLEQUOTE: ('b','#bc8f8f',''), + DOUBLEQUOTE_R: ('b','#bc8f8f',''), + DOUBLEQUOTE_U: ('b','#bc8f8f',''), + TRIPLESINGLEQUOTE: ('b','#bc8f8f',''), + TRIPLESINGLEQUOTE_R: ('b','#bc8f8f',''), + TRIPLESINGLEQUOTE_U: ('b','#bc8f8f',''), + TRIPLEDOUBLEQUOTE: ('b','#bc8f8f',''), + TRIPLEDOUBLEQUOTE_R: ('b','#bc8f8f',''), + TRIPLEDOUBLEQUOTE_U: ('b','#bc8f8f',''), + TEXT: ('','#000000',''), + LINENUMBER: ('>ti#555555','#000000',''), + PAGEBACKGROUND: '#FFFFFF' + } + +defaultColors = lite + +def Usage(): + doc = """ + ----------------------------------------------------------------------------- + PySourceColor.py ver: %s + ----------------------------------------------------------------------------- + Module summary: + This module is designed to colorize python source code. + Input--->python source + Output-->colorized (html, html4.01/css, xhtml1.0) + Standalone: + This module will work from the command line with options. + This module will work with redirected stdio. + Imported: + This module can be imported and used directly in your code. + ----------------------------------------------------------------------------- + Command line options: + -h, --help + Optional-> Display this help message. + -t, --test + Optional-> Will ignore all others flags but --profile + test all schemes and markup combinations + -p, --profile + Optional-> Works only with --test or -t + runs profile.py and makes the test work in quiet mode. + -i, --in, --input + Optional-> If you give input on stdin. + Use any of these for the current dir (.,cwd) + Input can be file or dir. + Input from stdin use one of the following (-,stdin) + If stdin is used as input stdout is output unless specified. + -o, --out, --output + Optional-> output dir for the colorized source. + default: output dir is the input dir. + To output html to stdout use one of the following (-,stdout) + Stdout can be used without stdin if you give a file as input. + -c, --color + Optional-> null, mono, dark, dark2, lite, idle, pythonwin, viewcvs + default: dark + -s, --show + Optional-> Show page after creation. + default: no show + -m, --markup + Optional-> html, css, xhtml + css, xhtml also support external stylesheets (-e,--external) + default: HTML + -e, --external + Optional-> use with css, xhtml + Writes an style sheet instead of embedding it in the page + saves it as pystyle.css in the same directory. + html markup will silently ignore this flag. + -H, --header + Opional-> add a page header to the top of the output + -H + Builtin header (name,date,hrule) + --header + You must specify a filename. + The header file must be valid html + and must handle its own font colors. + ex. --header c:/tmp/header.txt + -F, --footer + Opional-> add a page footer to the bottom of the output + -F + Builtin footer (hrule,name,date) + --footer + You must specify a filename. + The footer file must be valid html + and must handle its own font colors. + ex. --footer c:/tmp/footer.txt + -l, --linenumbers + Optional-> default is no linenumbers + Adds line numbers to the start of each line in the code. + --convertpage + Given a webpage that has code embedded in tags it will + convert embedded code to colorized html. + (see pageconvert for details) + ----------------------------------------------------------------------------- + Option usage: + # Test and show pages + python PySourceColor.py -t -s + # Test and only show profile results + python PySourceColor.py -t -p + # Colorize all .py,.pyw files in cwdir you can also use: (.,cwd) + python PySourceColor.py -i . + # Using long options w/ = + python PySourceColor.py --in=c:/myDir/my.py --color=lite --show + # Using short options w/out = + python PySourceColor.py -i c:/myDir/ -c idle -m css -e + # Using any mix + python PySourceColor.py --in . -o=c:/myDir --show + # Place a custom header on your files + python PySourceColor.py -i . -o c:/tmp -m xhtml --header c:/header.txt + ----------------------------------------------------------------------------- + Stdio usage: + # Stdio using no options + python PySourceColor.py < c:/MyFile.py > c:/tmp/MyFile.html + # Using stdin alone automatically uses stdout for output: (stdin,-) + python PySourceColor.py -i- < c:/MyFile.py > c:/tmp/myfile.html + # Stdout can also be written to directly from a file instead of stdin + python PySourceColor.py -i c:/MyFile.py -m css -o- > c:/tmp/myfile.html + # Stdin can be used as input , but output can still be specified + python PySourceColor.py -i- -o c:/pydoc.py.html -s < c:/Python22/my.py + _____________________________________________________________________________ + """ + print(doc % (__version__)) + sys.exit(1) + +###################################################### Command line interface + +def cli(): + """Handle command line args and redirections""" + try: + # try to get command line args + opts, args = getopt.getopt(sys.argv[1:], + "hseqtplHFi:o:c:m:h:f:",["help", "show", "quiet", + "test", "external", "linenumbers", "convertpage", "profile", + "input=", "output=", "color=", "markup=","header=", "footer="]) + except getopt.GetoptError: + # on error print help information and exit: + Usage() + # init some names + input = None + output = None + colorscheme = None + markup = 'html' + header = None + footer = None + linenumbers = 0 + show = 0 + quiet = 0 + test = 0 + profile = 0 + convertpage = 0 + form = None + # if we have args then process them + for o, a in opts: + if o in ["-h", "--help"]: + Usage() + sys.exit() + if o in ["-o", "--output", "--out"]: + output = a + if o in ["-i", "--input", "--in"]: + input = a + if input in [".", "cwd"]: + input = os.getcwd() + if o in ["-s", "--show"]: + show = 1 + if o in ["-q", "--quiet"]: + quiet = 1 + if o in ["-t", "--test"]: + test = 1 + if o in ["--convertpage"]: + convertpage = 1 + if o in ["-p", "--profile"]: + profile = 1 + if o in ["-e", "--external"]: + form = 'external' + if o in ["-m", "--markup"]: + markup = str(a) + if o in ["-l", "--linenumbers"]: + linenumbers = 1 + if o in ["--header"]: + header = str(a) + elif o == "-H": + header = '' + if o in ["--footer"]: + footer = str(a) + elif o == "-F": + footer = '' + if o in ["-c", "--color"]: + try: + colorscheme = globals().get(a.lower()) + except: + traceback.print_exc() + Usage() + if test: + if profile: + import profile + profile.run('_test(show=%s, quiet=%s)'%(show,quiet)) + else: + # Parse this script in every possible colorscheme and markup + _test(show,quiet) + elif input in [None, "-", "stdin"] or output in ["-", "stdout"]: + # determine if we are going to use stdio + if input not in [None, "-", "stdin"]: + if os.path.isfile(input) : + path2stdout(input, colors=colorscheme, markup=markup, + linenumbers=linenumbers, header=header, + footer=footer, form=form) + else: + raise PathError('File does not exists!') + else: + try: + if sys.stdin.isatty(): + raise InputError('Please check input!') + else: + if output in [None,"-","stdout"]: + str2stdout(sys.stdin.read(), colors=colorscheme, + markup=markup, header=header, + footer=footer, linenumbers=linenumbers, + form=form) + else: + str2file(sys.stdin.read(), outfile=output, show=show, + markup=markup, header=header, footer=footer, + linenumbers=linenumbers, form=form) + except: + traceback.print_exc() + Usage() + else: + if os.path.exists(input): + if convertpage: + # if there was at least an input given we can proceed + pageconvert(input, out=output, colors=colorscheme, + show=show, markup=markup,linenumbers=linenumbers) + else: + # if there was at least an input given we can proceed + convert(source=input, outdir=output, colors=colorscheme, + show=show, markup=markup, quiet=quiet, header=header, + footer=footer, linenumbers=linenumbers, form=form) + else: + raise PathError('File does not exists!') + Usage() + +######################################################### Simple markup tests + +def _test(show=0, quiet=0): + """Test the parser and most of the functions. + + There are 19 test total(eight colorschemes in three diffrent markups, + and a str2file test. Most functions are tested by this. + """ + fi = sys.argv[0] + if not fi.endswith('.exe'):# Do not test if frozen as an archive + # this is a collection of test, most things are covered. + path2file(fi, '/tmp/null.html', null, show=show, quiet=quiet) + path2file(fi, '/tmp/null_css.html', null, show=show, + markup='css', quiet=quiet) + path2file(fi, '/tmp/mono.html', mono, show=show, quiet=quiet) + path2file(fi, '/tmp/mono_css.html', mono, show=show, + markup='css', quiet=quiet) + path2file(fi, '/tmp/lite.html', lite, show=show, quiet=quiet) + path2file(fi, '/tmp/lite_css.html', lite, show=show, + markup='css', quiet=quiet, header='', footer='', + linenumbers=1) + path2file(fi, '/tmp/lite_xhtml.html', lite, show=show, + markup='xhtml', quiet=quiet) + path2file(fi, '/tmp/dark.html', dark, show=show, quiet=quiet) + path2file(fi, '/tmp/dark_css.html', dark, show=show, + markup='css', quiet=quiet, linenumbers=1) + path2file(fi, '/tmp/dark2.html', dark2, show=show, quiet=quiet) + path2file(fi, '/tmp/dark2_css.html', dark2, show=show, + markup='css', quiet=quiet) + path2file(fi, '/tmp/dark2_xhtml.html', dark2, show=show, + markup='xhtml', quiet=quiet, header='', footer='', + linenumbers=1, form='external') + path2file(fi, '/tmp/idle.html', idle, show=show, quiet=quiet) + path2file(fi, '/tmp/idle_css.html', idle, show=show, + markup='css', quiet=quiet) + path2file(fi, '/tmp/viewcvs.html', viewcvs, show=show, + quiet=quiet, linenumbers=1) + path2file(fi, '/tmp/viewcvs_css.html', viewcvs, show=show, + markup='css', linenumbers=1, quiet=quiet) + path2file(fi, '/tmp/pythonwin.html', pythonwin, show=show, + quiet=quiet) + path2file(fi, '/tmp/pythonwin_css.html', pythonwin, show=show, + markup='css', quiet=quiet) + teststr=r'''"""This is a test of decorators and other things""" +# This should be line 421... +@whatever(arg,arg2) +@A @B(arghh) @C +def LlamaSaysNi(arg='Ni!',arg2="RALPH"): + """This docstring is deeply disturbed by all the llama references""" + print('%s The Wonder Llama says %s'% (arg2,arg)) +# So I was like duh!, and he was like ya know?!, +# and so we were both like huh...wtf!? RTFM!! LOL!!;) +@staticmethod## Double comments are KewL. +def LlamasRLumpy(): + """This docstring is too sexy to be here. + """ + u""" +============================= +A Møøse once bit my sister... +============================= + """ + ## Relax, this won't hurt a bit, just a simple, painless procedure, + ## hold still while I get the anesthetizing hammer. + m = {'three':'1','won':'2','too':'3'} + o = r'fishy\fishy\fishy/fish\oh/where/is\my/little\..' + python = uR""" + No realli! She was Karving her initials øn the møøse with the sharpened end + of an interspace tøøthbrush given her by Svenge - her brother-in-law -an Oslo + dentist and star of many Norwegian møvies: "The Høt Hands of an Oslo + Dentist", "Fillings of Passion", "The Huge Mølars of Horst Nordfink"...""" + RU"""142 MEXICAN WHOOPING LLAMAS"""#<-Can you fit 142 llamas in a red box? + n = u' HERMSGERVØRDENBRØTBØRDA ' + """ YUTTE """ + t = """SAMALLNIATNUOMNAIRODAUCE"""+"DENIARTYLLAICEPS04" + ## We apologise for the fault in the + ## comments. Those responsible have been + ## sacked. + y = '14 NORTH CHILEAN GUANACOS \ +(CLOSELY RELATED TO THE LLAMA)' + rules = [0,1,2,3,4,5] + print y''' + htmlPath = os.path.abspath('/tmp/strtest_lines.html') + str2file(teststr, htmlPath, colors=dark, markup='xhtml', + linenumbers=420, show=show) + _printinfo(" wrote %s" % htmlPath, quiet) + htmlPath = os.path.abspath('/tmp/strtest_nolines.html') + str2file(teststr, htmlPath, colors=dark, markup='xhtml', + show=show) + _printinfo(" wrote %s" % htmlPath, quiet) + else: + Usage() + return + +# emacs wants this: ' + +####################################################### User funtctions + +def str2stdout(sourcestring, colors=None, title='', markup='html', + header=None, footer=None, + linenumbers=0, form=None): + """Converts a code(string) to colorized HTML. Writes to stdout. + + form='code',or'snip' (for "<pre>yourcode</pre>" only) + colors=null,mono,lite,dark,dark2,idle,or pythonwin + """ + Parser(sourcestring, colors=colors, title=title, markup=markup, + header=header, footer=footer, + linenumbers=linenumbers).format(form) + +def path2stdout(sourcepath, title='', colors=None, markup='html', + header=None, footer=None, + linenumbers=0, form=None): + """Converts code(file) to colorized HTML. Writes to stdout. + + form='code',or'snip' (for "<pre>yourcode</pre>" only) + colors=null,mono,lite,dark,dark2,idle,or pythonwin + """ + sourcestring = open(sourcepath).read() + Parser(sourcestring, colors=colors, title=sourcepath, + markup=markup, header=header, footer=footer, + linenumbers=linenumbers).format(form) + +def str2html(sourcestring, colors=None, title='', + markup='html', header=None, footer=None, + linenumbers=0, form=None): + """Converts a code(string) to colorized HTML. Returns an HTML string. + + form='code',or'snip' (for "<pre>yourcode</pre>" only) + colors=null,mono,lite,dark,dark2,idle,or pythonwin + """ + stringIO = StringIO.StringIO() + Parser(sourcestring, colors=colors, title=title, out=stringIO, + markup=markup, header=header, footer=footer, + linenumbers=linenumbers).format(form) + stringIO.seek(0) + return stringIO.read() + +def str2css(sourcestring, colors=None, title='', + markup='css', header=None, footer=None, + linenumbers=0, form=None): + """Converts a code string to colorized CSS/HTML. Returns CSS/HTML string + + If form != None then this will return (stylesheet_str, code_str) + colors=null,mono,lite,dark,dark2,idle,or pythonwin + """ + if markup.lower() not in ['css' ,'xhtml']: + markup = 'css' + stringIO = StringIO.StringIO() + parse = Parser(sourcestring, colors=colors, title=title, + out=stringIO, markup=markup, + header=header, footer=footer, + linenumbers=linenumbers) + parse.format(form) + stringIO.seek(0) + if form != None: + return parse._sendCSSStyle(external=1), stringIO.read() + else: + return None, stringIO.read() + +def str2markup(sourcestring, colors=None, title = '', + markup='xhtml', header=None, footer=None, + linenumbers=0, form=None): + """ Convert code strings into ([stylesheet or None], colorized string) """ + if markup.lower() == 'html': + return None, str2html(sourcestring, colors=colors, title=title, + header=header, footer=footer, markup=markup, + linenumbers=linenumbers, form=form) + else: + return str2css(sourcestring, colors=colors, title=title, + header=header, footer=footer, markup=markup, + linenumbers=linenumbers, form=form) + +def str2file(sourcestring, outfile, colors=None, title='', + markup='html', header=None, footer=None, + linenumbers=0, show=0, dosheet=1, form=None): + """Converts a code string to a file. + + makes no attempt at correcting bad pathnames + """ + css , html = str2markup(sourcestring, colors=colors, title='', + markup=markup, header=header, footer=footer, + linenumbers=linenumbers, form=form) + # write html + f = open(outfile,'wt') + f.writelines(html) + f.close() + #write css + if css != None and dosheet: + dir = os.path.dirname(outfile) + outcss = os.path.join(dir,'pystyle.css') + f = open(outcss,'wt') + f.writelines(css) + f.close() + if show: + showpage(outfile) + +def path2html(sourcepath, colors=None, markup='html', + header=None, footer=None, + linenumbers=0, form=None): + """Converts code(file) to colorized HTML. Returns an HTML string. + + form='code',or'snip' (for "<pre>yourcode</pre>" only) + colors=null,mono,lite,dark,dark2,idle,or pythonwin + """ + stringIO = StringIO.StringIO() + sourcestring = open(sourcepath).read() + Parser(sourcestring, colors, title=sourcepath, out=stringIO, + markup=markup, header=header, footer=footer, + linenumbers=linenumbers).format(form) + stringIO.seek(0) + return stringIO.read() + +def convert(source, outdir=None, colors=None, + show=0, markup='html', quiet=0, + header=None, footer=None, linenumbers=0, form=None): + """Takes a file or dir as input and places the html in the outdir. + + If outdir is none it defaults to the input dir + """ + count=0 + # If it is a filename then path2file + if not os.path.isdir(source): + if os.path.isfile(source): + count+=1 + path2file(source, outdir, colors, show, markup, + quiet, form, header, footer, linenumbers, count) + else: + raise PathError('File does not exist!') + # If we pass in a dir we need to walkdir for files. + # Then we need to colorize them with path2file + else: + fileList = walkdir(source) + if fileList != None: + # make sure outdir is a dir + if outdir != None: + if os.path.splitext(outdir)[1] != '': + outdir = os.path.split(outdir)[0] + for item in fileList: + count+=1 + path2file(item, outdir, colors, show, markup, + quiet, form, header, footer, linenumbers, count) + _printinfo('Completed colorizing %s files.'%str(count), quiet) + else: + _printinfo("No files to convert in dir.", quiet) + +def path2file(sourcePath, out=None, colors=None, show=0, + markup='html', quiet=0, form=None, + header=None, footer=None, linenumbers=0, count=1): + """ Converts python source to html file""" + # If no outdir is given we use the sourcePath + if out == None:#this is a guess + htmlPath = sourcePath + '.html' + else: + # If we do give an out_dir, and it does + # not exist , it will be created. + if os.path.splitext(out)[1] == '': + if not os.path.isdir(out): + os.makedirs(out) + sourceName = os.path.basename(sourcePath) + htmlPath = os.path.join(out,sourceName)+'.html' + # If we do give an out_name, and its dir does + # not exist , it will be created. + else: + outdir = os.path.split(out)[0] + if not os.path.isdir(outdir): + os.makedirs(outdir) + htmlPath = out + htmlPath = os.path.abspath(htmlPath) + # Open the text and do the parsing. + source = open(sourcePath).read() + parse = Parser(source, colors, sourcePath, open(htmlPath, 'wt'), + markup, header, footer, linenumbers) + parse.format(form) + _printinfo(" wrote %s" % htmlPath, quiet) + # html markup will ignore the external flag, but + # we need to stop the blank file from being written. + if form == 'external' and count == 1 and markup != 'html': + cssSheet = parse._sendCSSStyle(external=1) + cssPath = os.path.join(os.path.dirname(htmlPath),'pystyle.css') + css = open(cssPath, 'wt') + css.write(cssSheet) + css.close() + _printinfo(" wrote %s" % cssPath, quiet) + if show: + # load HTML page into the default web browser. + showpage(htmlPath) + return htmlPath + +def tagreplace(sourcestr, colors=lite, markup='xhtml', + linenumbers=0, dosheet=1, tagstart='<PY>'.lower(), + tagend='</PY>'.lower(), stylesheet='pystyle.css'): + """This is a helper function for pageconvert. Returns css, page. + """ + if markup.lower() != 'html': + link = '<link rel="stylesheet" href="%s" type="text/css"/></head>' + css = link%stylesheet + if sourcestr.find(css) == -1: + sourcestr = sourcestr.replace('</head>', css, 1) + starttags = sourcestr.count(tagstart) + endtags = sourcestr.count(tagend) + if starttags: + if starttags == endtags: + for _ in range(starttags): + datastart = sourcestr.find(tagstart) + dataend = sourcestr.find(tagend) + data = sourcestr[datastart+len(tagstart):dataend] + data = unescape(data) + css , data = str2markup(data, colors=colors, + linenumbers=linenumbers, markup=markup, form='embed') + start = sourcestr[:datastart] + end = sourcestr[dataend+len(tagend):] + sourcestr = ''.join([start,data,end]) + else: + raise InputError('Tag mismatch!\nCheck %s,%s tags'%tagstart,tagend) + if not dosheet: + css = None + return css, sourcestr + +def pageconvert(path, out=None, colors=lite, markup='xhtml', linenumbers=0, + dosheet=1, tagstart='<PY>'.lower(), tagend='</PY>'.lower(), + stylesheet='pystyle', show=1, returnstr=0): + """This function can colorize Python source + + that is written in a webpage enclosed in tags. + """ + if out == None: + out = os.path.dirname(path) + infile = open(path, 'r').read() + css,page = tagreplace(sourcestr=infile,colors=colors, + markup=markup, linenumbers=linenumbers, dosheet=dosheet, + tagstart=tagstart, tagend=tagend, stylesheet=stylesheet) + if not returnstr: + newpath = os.path.abspath(os.path.join( + out,'tmp', os.path.basename(path))) + if not os.path.exists(newpath): + try: + os.makedirs(os.path.dirname(newpath)) + except: + pass#traceback.print_exc() + #Usage() + y = open(newpath, 'w') + y.write(page) + y.close() + if css: + csspath = os.path.abspath(os.path.join( + out,'tmp','%s.css'%stylesheet)) + x = open(csspath,'w') + x.write(css) + x.close() + if show: + try: + os.startfile(newpath) + except: + traceback.print_exc() + return newpath + else: + return css, page + +##################################################################### helpers + +def walkdir(dir): + """Return a list of .py and .pyw files from a given directory. + + This function can be written as a generator Python 2.3, or a genexp + in Python 2.4. But 2.2 and 2.1 would be left out.... + """ + # Get a list of files that match *.py* + GLOB_PATTERN = os.path.join(dir, "*.[p][y]*") + pathlist = glob.glob(GLOB_PATTERN) + # Now filter out all but py and pyw + filterlist = [x for x in pathlist + if x.endswith('.py') + or x.endswith('.pyw')] + if filterlist != []: + # if we have a list send it + return filterlist + else: + return None + +def showpage(path): + """Helper function to open webpages""" + try: + import webbrowser + webbrowser.open_new(os.path.abspath(path)) + except: + traceback.print_exc() + +def _printinfo(message, quiet): + """Helper to print messages""" + if not quiet: + print(message) + +def escape(text): + """escape text for html. similar to cgi.escape""" + text = text.replace("&", "&") + text = text.replace("<", "<") + text = text.replace(">", ">") + return text + +def unescape(text): + """unsecape escaped text""" + text = text.replace(""", '"') + text = text.replace(">", ">") + text = text.replace("<", "<") + text = text.replace("&", "&") + return text + +########################################################### Custom Exceptions + +class PySourceColorError(Exception): + # Base for custom errors + def __init__(self, msg=''): + self._msg = msg + Exception.__init__(self, msg) + def __repr__(self): + return self._msg + __str__ = __repr__ + +class PathError(PySourceColorError): + def __init__(self, msg): + PySourceColorError.__init__(self, + 'Path error! : %s'% msg) + +class InputError(PySourceColorError): + def __init__(self, msg): + PySourceColorError.__init__(self, + 'Input error! : %s'% msg) + +########################################################## Python code parser + +class Parser(object): + + """MoinMoin python parser heavily chopped :)""" + + def __init__(self, raw, colors=None, title='', out=sys.stdout, + markup='html', header=None, footer=None, linenumbers=0): + """Store the source text & set some flags""" + if colors == None: + colors = defaultColors + self.raw = raw.expandtabs().rstrip() + self.title = os.path.basename(title) + self.out = out + self.line = '' + self.lasttext = '' + self.argFlag = 0 + self.classFlag = 0 + self.defFlag = 0 + self.decoratorFlag = 0 + self.external = 0 + self.markup = markup.upper() + self.colors = colors + self.header = header + self.footer = footer + self.doArgs = 1 # overrides the new tokens + self.doNames = 1 # overrides the new tokens + self.doMathOps = 1 # overrides the new tokens + self.doBrackets = 1 # overrides the new tokens + self.doURL = 1 # override url conversion + self.LINENUMHOLDER = "___line___".upper() + self.LINESTART = "___start___".upper() + self.skip = 0 + # add space left side of code for padding.Override in color dict. + self.extraspace = self.colors.get(EXTRASPACE, '') + # Linenumbers less then zero also have numberlinks + self.dolinenums = self.linenum = abs(linenumbers) + if linenumbers < 0: + self.numberlinks = 1 + else: + self.numberlinks = 0 + + def format(self, form=None): + """Parse and send the colorized source""" + if form in ('snip','code'): + self.addEnds = 0 + elif form == 'embed': + self.addEnds = 0 + self.external = 1 + else: + if form == 'external': + self.external = 1 + self.addEnds = 1 + + # Store line offsets in self.lines + self.lines = [0, 0] + pos = 0 + + # Add linenumbers + if self.dolinenums: + start=self.LINENUMHOLDER+' '+self.extraspace + else: + start=''+self.extraspace + newlines = [] + lines = self.raw.splitlines(0) + for l in lines: + # span and div escape for customizing and embedding raw text + if (l.startswith('#$#') + or l.startswith('#%#') + or l.startswith('#@#')): + newlines.append(l) + else: + # kludge for line spans in css,xhtml + if self.markup in ['XHTML','CSS']: + newlines.append(self.LINESTART+' '+start+l) + else: + newlines.append(start+l) + self.raw = "\n".join(newlines)+'\n'# plus an extra newline at the end + + # Gather lines + while 1: + pos = self.raw.find('\n', pos) + 1 + if not pos: break + self.lines.append(pos) + self.lines.append(len(self.raw)) + + # Wrap text in a filelike object + self.pos = 0 + text = StringIO.StringIO(self.raw) + + # Markup start + if self.addEnds: + self._doPageStart() + else: + self._doSnippetStart() + + ## Tokenize calls the __call__ + ## function for each token till done. + # Parse the source and write out the results. + try: + tokenize.tokenize(text.readline, self) + except tokenize.TokenError as ex: + msg = ex[0] + line = ex[1][0] + self.out.write("<h3>ERROR: %s</h3>%s\n"% + (msg, self.raw[self.lines[line]:])) + #traceback.print_exc() + + # Markup end + if self.addEnds: + self._doPageEnd() + else: + self._doSnippetEnd() + + def __call__(self, toktype, toktext, srow_col, erow_col, line): + """Token handler. Order is important do not rearrange.""" + self.line = line + srow, scol = srow_col + erow, ecol = erow_col + # Calculate new positions + oldpos = self.pos + newpos = self.lines[srow] + scol + self.pos = newpos + len(toktext) + # Handle newlines + if toktype in (token.NEWLINE, tokenize.NL): + self.decoratorFlag = self.argFlag = 0 + # kludge for line spans in css,xhtml + if self.markup in ['XHTML','CSS']: + self.out.write('</span>') + self.out.write('\n') + return + + # Send the original whitespace, and tokenize backslashes if present. + # Tokenizer.py just sends continued line backslashes with whitespace. + # This is a hack to tokenize continued line slashes as operators. + # Should continued line backslashes be treated as operators + # or some other token? + + if newpos > oldpos: + if self.raw[oldpos:newpos].isspace(): + # consume a single space after linestarts and linenumbers + # had to have them so tokenizer could seperate them. + # multiline strings are handled by do_Text functions + if self.lasttext != self.LINESTART \ + and self.lasttext != self.LINENUMHOLDER: + self.out.write(self.raw[oldpos:newpos]) + else: + self.out.write(self.raw[oldpos+1:newpos]) + else: + slash = self.raw[oldpos:newpos].find('\\')+oldpos + self.out.write(self.raw[oldpos:slash]) + getattr(self, '_send%sText'%(self.markup))(OPERATOR, '\\') + self.linenum+=1 + # kludge for line spans in css,xhtml + if self.markup in ['XHTML','CSS']: + self.out.write('</span>') + self.out.write(self.raw[slash+1:newpos]) + + # Skip indenting tokens + if toktype in (token.INDENT, token.DEDENT): + self.pos = newpos + return + + # Look for operators + if token.LPAR <= toktype and toktype <= token.OP: + # Trap decorators py2.4 > + if toktext == '@': + toktype = DECORATOR + # Set a flag if this was the decorator start so + # the decorator name and arguments can be identified + self.decoratorFlag = self.argFlag = 1 + else: + if self.doArgs: + # Find the start for arguments + if toktext == '(' and self.argFlag: + self.argFlag = 2 + # Find the end for arguments + elif toktext == ':': + self.argFlag = 0 + ## Seperate the diffrent operator types + # Brackets + if self.doBrackets and toktext in ['[',']','(',')','{','}']: + toktype = BRACKETS + # Math operators + elif self.doMathOps and toktext in ['*=','**=','-=','+=','|=', + '%=','>>=','<<=','=','^=', + '/=', '+','-','**','*','/','%']: + toktype = MATH_OPERATOR + # Operator + else: + toktype = OPERATOR + # example how flags should work. + # def fun(arg=argvalue,arg2=argvalue2): + # 0 1 2 A 1 N 2 A 1 N 0 + if toktext == "=" and self.argFlag == 2: + self.argFlag = 1 + elif toktext == "," and self.argFlag == 1: + self.argFlag = 2 + # Look for keywords + elif toktype == NAME and keyword.iskeyword(toktext): + toktype = KEYWORD + # Set a flag if this was the class / def start so + # the class / def name and arguments can be identified + if toktext in ['class', 'def']: + if toktext =='class' and \ + not line[:line.find('class')].endswith('.'): + self.classFlag = self.argFlag = 1 + elif toktext == 'def' and \ + not line[:line.find('def')].endswith('.'): + self.defFlag = self.argFlag = 1 + else: + # must have used a keyword as a name i.e. self.class + toktype = ERRORTOKEN + + # Look for class, def, decorator name + elif (self.classFlag or self.defFlag or self.decoratorFlag) \ + and self.doNames: + if self.classFlag: + self.classFlag = 0 + toktype = CLASS_NAME + elif self.defFlag: + self.defFlag = 0 + toktype = DEF_NAME + elif self.decoratorFlag: + self.decoratorFlag = 0 + toktype = DECORATOR_NAME + + # Look for strings + # Order of evaluation is important do not change. + elif toktype == token.STRING: + text = toktext.lower() + # TRIPLE DOUBLE QUOTE's + if (text[:3] == '"""'): + toktype = TRIPLEDOUBLEQUOTE + elif (text[:4] == 'r"""'): + toktype = TRIPLEDOUBLEQUOTE_R + elif (text[:4] == 'u"""' or + text[:5] == 'ur"""'): + toktype = TRIPLEDOUBLEQUOTE_U + # DOUBLE QUOTE's + elif (text[:1] == '"'): + toktype = DOUBLEQUOTE + elif (text[:2] == 'r"'): + toktype = DOUBLEQUOTE_R + elif (text[:2] == 'u"' or + text[:3] == 'ur"'): + toktype = DOUBLEQUOTE_U + # TRIPLE SINGLE QUOTE's + elif (text[:3] == "'''"): + toktype = TRIPLESINGLEQUOTE + elif (text[:4] == "r'''"): + toktype = TRIPLESINGLEQUOTE_R + elif (text[:4] == "u'''" or + text[:5] == "ur'''"): + toktype = TRIPLESINGLEQUOTE_U + # SINGLE QUOTE's + elif (text[:1] == "'"): + toktype = SINGLEQUOTE + elif (text[:2] == "r'"): + toktype = SINGLEQUOTE_R + elif (text[:2] == "u'" or + text[:3] == "ur'"): + toktype = SINGLEQUOTE_U + + # test for invalid string declaration + if self.lasttext.lower() == 'ru': + toktype = ERRORTOKEN + + # Look for comments + elif toktype == COMMENT: + if toktext[:2] == "##": + toktype = DOUBLECOMMENT + elif toktext[:3] == '#$#': + toktype = TEXT + self.textFlag = 'SPAN' + toktext = toktext[3:] + elif toktext[:3] == '#%#': + toktype = TEXT + self.textFlag = 'DIV' + toktext = toktext[3:] + elif toktext[:3] == '#@#': + toktype = TEXT + self.textFlag = 'RAW' + toktext = toktext[3:] + if self.doURL: + # this is a 'fake helper function' + # url(URI,Alias_name) or url(URI) + url_pos = toktext.find('url(') + if url_pos != -1: + before = toktext[:url_pos] + url = toktext[url_pos+4:] + splitpoint = url.find(',') + endpoint = url.find(')') + after = url[endpoint+1:] + url = url[:endpoint] + if splitpoint != -1: + urlparts = url.split(',',1) + toktext = '%s<a href="%s">%s</a>%s'%( + before,urlparts[0],urlparts[1].lstrip(),after) + else: + toktext = '%s<a href="%s">%s</a>%s'%(before,url,url,after) + + # Seperate errors from decorators + elif toktype == ERRORTOKEN: + # Bug fix for < py2.4 + # space between decorators + if self.argFlag and toktext.isspace(): + #toktype = NAME + self.out.write(toktext) + return + # Bug fix for py2.2 linenumbers with decorators + elif toktext.isspace(): + # What if we have a decorator after a >>> or ... + #p = line.find('@') + #if p >= 0 and not line[:p].isspace(): + #self.out.write(toktext) + #return + if self.skip: + self.skip=0 + return + else: + self.out.write(toktext) + return + # trap decorators < py2.4 + elif toktext == '@': + toktype = DECORATOR + # Set a flag if this was the decorator start so + # the decorator name and arguments can be identified + self.decoratorFlag = self.argFlag = 1 + + # Seperate args from names + elif (self.argFlag == 2 and + toktype == NAME and + toktext != 'None' and + self.doArgs): + toktype = ARGS + + # Look for line numbers + # The conversion code for them is in the send_text functions. + if toktext in [self.LINENUMHOLDER,self.LINESTART]: + toktype = LINENUMBER + # if we don't have linenumbers set flag + # to skip the trailing space from linestart + if toktext == self.LINESTART and not self.dolinenums \ + or toktext == self.LINENUMHOLDER: + self.skip=1 + + + # Skip blank token that made it thru + ## bugfix for the last empty tag. + if toktext == '': + return + + # Last token text history + self.lasttext = toktext + + # escape all but the urls in the comments + if toktype in (DOUBLECOMMENT, COMMENT): + if toktext.find('<a href=') == -1: + toktext = escape(toktext) + else: + pass + elif toktype == TEXT: + pass + else: + toktext = escape(toktext) + + # Send text for any markup + getattr(self, '_send%sText'%(self.markup))(toktype, toktext) + return + + ################################################################# Helpers + + def _doSnippetStart(self): + if self.markup == 'HTML': + # Start of html snippet + self.out.write('<pre>\n') + else: + # Start of css/xhtml snippet + self.out.write(self.colors.get(CODESTART,'<pre class="py">\n')) + + def _doSnippetEnd(self): + # End of html snippet + self.out.write(self.colors.get(CODEEND,'</pre>\n')) + + ######################################################## markup selectors + + def _getFile(self, filepath): + try: + _file = open(filepath,'r') + content = _file.read() + _file.close() + except: + traceback.print_exc() + content = '' + return content + + def _doPageStart(self): + getattr(self, '_do%sStart'%(self.markup))() + + def _doPageHeader(self): + if self.header != None: + if self.header.find('#$#') != -1 or \ + self.header.find('#$#') != -1 or \ + self.header.find('#%#') != -1: + self.out.write(self.header[3:]) + else: + if self.header != '': + self.header = self._getFile(self.header) + getattr(self, '_do%sHeader'%(self.markup))() + + def _doPageFooter(self): + if self.footer != None: + if self.footer.find('#$#') != -1 or \ + self.footer.find('#@#') != -1 or \ + self.footer.find('#%#') != -1: + self.out.write(self.footer[3:]) + else: + if self.footer != '': + self.footer = self._getFile(self.footer) + getattr(self, '_do%sFooter'%(self.markup))() + + def _doPageEnd(self): + getattr(self, '_do%sEnd'%(self.markup))() + + ################################################### color/style retrieval + ## Some of these are not used anymore but are kept for documentation + + def _getLineNumber(self): + num = self.linenum + self.linenum+=1 + return str(num).rjust(5)+" " + + def _getTags(self, key): + # style tags + return self.colors.get(key, self.colors[NAME])[0] + + def _getForeColor(self, key): + # get text foreground color, if not set to black + color = self.colors.get(key, self.colors[NAME])[1] + if color[:1] != '#': + color = '#000000' + return color + + def _getBackColor(self, key): + # get text background color + return self.colors.get(key, self.colors[NAME])[2] + + def _getPageColor(self): + # get page background color + return self.colors.get(PAGEBACKGROUND, '#FFFFFF') + + def _getStyle(self, key): + # get the token style from the color dictionary + return self.colors.get(key, self.colors[NAME]) + + def _getMarkupClass(self, key): + # get the markup class name from the markup dictionary + return MARKUPDICT.get(key, MARKUPDICT[NAME]) + + def _getDocumentCreatedBy(self): + return '<!--This document created by %s ver.%s on: %s-->\n'%( + __title__,__version__,time.ctime()) + + ################################################### HTML markup functions + + def _doHTMLStart(self): + # Start of html page + self.out.write('<!DOCTYPE html PUBLIC \ +"-//W3C//DTD HTML 4.01//EN">\n') + self.out.write('<html><head><title>%s</title>\n'%(self.title)) + self.out.write(self._getDocumentCreatedBy()) + self.out.write('<meta http-equiv="Content-Type" \ +content="text/html;charset=iso-8859-1">\n') + # Get background + self.out.write('</head><body bgcolor="%s">\n'%self._getPageColor()) + self._doPageHeader() + self.out.write('<pre>') + + def _getHTMLStyles(self, toktype, toktext): + # Get styles + tags, color = self.colors.get(toktype, self.colors[NAME])[:2]# + tagstart=[] + tagend=[] + # check for styles and set them if needed. + if 'b' in tags:#Bold + tagstart.append('<b>') + tagend.append('</b>') + if 'i' in tags:#Italics + tagstart.append('<i>') + tagend.append('</i>') + if 'u' in tags:#Underline + tagstart.append('<u>') + tagend.append('</u>') + # HTML tags should be paired like so : <b><i><u>Doh!</u></i></b> + tagend.reverse() + starttags="".join(tagstart) + endtags="".join(tagend) + return starttags,endtags,color + + def _sendHTMLText(self, toktype, toktext): + numberlinks = self.numberlinks + + # If it is an error, set a red box around the bad tokens + # older browsers should ignore it + if toktype == ERRORTOKEN: + style = ' style="border: solid 1.5pt #FF0000;"' + else: + style = '' + # Get styles + starttag, endtag, color = self._getHTMLStyles(toktype, toktext) + # This is a hack to 'fix' multi-line strings. + # Multi-line strings are treated as only one token + # even though they can be several physical lines. + # That makes it hard to spot the start of a line, + # because at this level all we know about are tokens. + + if toktext.count(self.LINENUMHOLDER): + # rip apart the string and separate it by line. + # count lines and change all linenum token to line numbers. + # embedded all the new font tags inside the current one. + # Do this by ending the tag first then writing our new tags, + # then starting another font tag exactly like the first one. + if toktype == LINENUMBER: + splittext = toktext.split(self.LINENUMHOLDER) + else: + splittext = toktext.split(self.LINENUMHOLDER+' ') + store = [] + store.append(splittext.pop(0)) + lstarttag, lendtag, lcolor = self._getHTMLStyles(LINENUMBER, toktext) + count = len(splittext) + for item in splittext: + num = self._getLineNumber() + if numberlinks: + numstrip = num.strip() + content = '<a name="%s" href="#%s">%s</a>' \ + %(numstrip,numstrip,num) + else: + content = num + if count <= 1: + endtag,starttag = '','' + linenumber = ''.join([endtag,'<font color=', lcolor, '>', + lstarttag, content, lendtag, '</font>' ,starttag]) + store.append(linenumber+item) + toktext = ''.join(store) + # send text + ## Output optimization + # skip font tag if black text, but styles will still be sent. (b,u,i) + if color !='#000000': + startfont = '<font color="%s"%s>'%(color, style) + endfont = '</font>' + else: + startfont, endfont = ('','') + if toktype != LINENUMBER: + self.out.write(''.join([startfont,starttag, + toktext,endtag,endfont])) + else: + self.out.write(toktext) + return + + def _doHTMLHeader(self): + # Optional + if self.header != '': + self.out.write('%s\n'%self.header) + else: + color = self._getForeColor(NAME) + self.out.write('<b><font color="%s"># %s \ + <br># %s</font></b><hr>\n'% + (color, self.title, time.ctime())) + + def _doHTMLFooter(self): + # Optional + if self.footer != '': + self.out.write('%s\n'%self.footer) + else: + color = self._getForeColor(NAME) + self.out.write('<b><font color="%s"> \ + <hr># %s<br># %s</font></b>\n'% + (color, self.title, time.ctime())) + + def _doHTMLEnd(self): + # End of html page + self.out.write('</pre>\n') + # Write a little info at the bottom + self._doPageFooter() + self.out.write('</body></html>\n') + + #################################################### CSS markup functions + + def _getCSSStyle(self, key): + # Get the tags and colors from the dictionary + tags, forecolor, backcolor = self._getStyle(key) + style=[] + border = None + bordercolor = None + tags = tags.lower() + if tags: + # get the border color if specified + # the border color will be appended to + # the list after we define a border + if '#' in tags:# border color + start = tags.find('#') + end = start + 7 + bordercolor = tags[start:end] + tags.replace(bordercolor,'',1) + # text styles + if 'b' in tags:# Bold + style.append('font-weight:bold;') + else: + style.append('font-weight:normal;') + if 'i' in tags:# Italic + style.append('font-style:italic;') + if 'u' in tags:# Underline + style.append('text-decoration:underline;') + # border size + if 'l' in tags:# thick border + size='thick' + elif 'm' in tags:# medium border + size='medium' + elif 't' in tags:# thin border + size='thin' + else:# default + size='medium' + # border styles + if 'n' in tags:# inset border + border='inset' + elif 'o' in tags:# outset border + border='outset' + elif 'r' in tags:# ridge border + border='ridge' + elif 'g' in tags:# groove border + border='groove' + elif '=' in tags:# double border + border='double' + elif '.' in tags:# dotted border + border='dotted' + elif '-' in tags:# dashed border + border='dashed' + elif 's' in tags:# solid border + border='solid' + # border type check + seperate_sides=0 + for side in ['<','>','^','v']: + if side in tags: + seperate_sides+=1 + # border box or seperate sides + if seperate_sides==0 and border: + style.append('border: %s %s;'%(border,size)) + else: + if border == None: + border = 'solid' + if 'v' in tags:# bottom border + style.append('border-bottom:%s %s;'%(border,size)) + if '<' in tags:# left border + style.append('border-left:%s %s;'%(border,size)) + if '>' in tags:# right border + style.append('border-right:%s %s;'%(border,size)) + if '^' in tags:# top border + style.append('border-top:%s %s;'%(border,size)) + else: + style.append('font-weight:normal;')# css inherited style fix + # we have to define our borders before we set colors + if bordercolor: + style.append('border-color:%s;'%bordercolor) + # text forecolor + style.append('color:%s;'% forecolor) + # text backcolor + if backcolor: + style.append('background-color:%s;'%backcolor) + return (self._getMarkupClass(key),' '.join(style)) + + def _sendCSSStyle(self, external=0): + """ create external and internal style sheets""" + styles = [] + external += self.external + if not external: + styles.append('<style type="text/css">\n<!--\n') + # Get page background color and write styles ignore any we don't know + styles.append('body { background:%s; }\n'%self._getPageColor()) + # write out the various css styles + for key in MARKUPDICT: + styles.append('.%s { %s }\n'%self._getCSSStyle(key)) + # If you want to style the pre tag you must modify the color dict. + # Example: + # lite[PY] = .py {border: solid thin #000000;background:#555555}\n''' + styles.append(self.colors.get(PY, '.py { }\n')) + # Extra css can be added here + # add CSSHOOK to the color dict if you need it. + # Example: + #lite[CSSHOOK] = """.mytag { border: solid thin #000000; } \n + # .myothertag { font-weight:bold; )\n""" + styles.append(self.colors.get(CSSHOOK,'')) + if not self.external: + styles.append('--></style>\n') + return ''.join(styles) + + def _doCSSStart(self): + # Start of css/html 4.01 page + self.out.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">\n') + self.out.write('<html><head><title>%s</title>\n'%(self.title)) + self.out.write(self._getDocumentCreatedBy()) + self.out.write('<meta http-equiv="Content-Type" \ +content="text/html;charset=iso-8859-1">\n') + self._doCSSStyleSheet() + self.out.write('</head>\n<body>\n') + # Write a little info at the top. + self._doPageHeader() + self.out.write(self.colors.get(CODESTART,'<pre class="py">\n')) + return + + def _doCSSStyleSheet(self): + if not self.external: + # write an embedded style sheet + self.out.write(self._sendCSSStyle()) + else: + # write a link to an external style sheet + self.out.write('<link rel="stylesheet" \ +href="pystyle.css" type="text/css">') + return + + def _sendCSSText(self, toktype, toktext): + # This is a hack to 'fix' multi-line strings. + # Multi-line strings are treated as only one token + # even though they can be several physical lines. + # That makes it hard to spot the start of a line, + # because at this level all we know about are tokens. + markupclass = MARKUPDICT.get(toktype, MARKUPDICT[NAME]) + # if it is a LINENUMBER type then we can skip the rest + if toktext == self.LINESTART and toktype == LINENUMBER: + self.out.write('<span class="py_line">') + return + if toktext.count(self.LINENUMHOLDER): + # rip apart the string and separate it by line + # count lines and change all linenum token to line numbers + # also convert linestart and lineend tokens + # <linestart> <lnumstart> lnum <lnumend> text <lineend> + ################################################# + newmarkup = MARKUPDICT.get(LINENUMBER, MARKUPDICT[NAME]) + lstartspan = '<span class="%s">'%(newmarkup) + if toktype == LINENUMBER: + splittext = toktext.split(self.LINENUMHOLDER) + else: + splittext = toktext.split(self.LINENUMHOLDER+' ') + store = [] + # we have already seen the first linenumber token + # so we can skip the first one + store.append(splittext.pop(0)) + for item in splittext: + num = self._getLineNumber() + if self.numberlinks: + numstrip = num.strip() + content= '<a name="%s" href="#%s">%s</a>' \ + %(numstrip,numstrip,num) + else: + content = num + linenumber= ''.join([lstartspan,content,'</span>']) + store.append(linenumber+item) + toktext = ''.join(store) + if toktext.count(self.LINESTART): + # wraps the textline in a line span + # this adds a lot of kludges, is it really worth it? + store = [] + parts = toktext.split(self.LINESTART+' ') + # handle the first part differently + # the whole token gets wraqpped in a span later on + first = parts.pop(0) + # place spans before the newline + pos = first.rfind('\n') + if pos != -1: + first=first[:pos]+'</span></span>'+first[pos:] + store.append(first) + #process the rest of the string + for item in parts: + #handle line numbers if present + if self.dolinenums: + item = item.replace('</span>', + '</span><span class="%s">'%(markupclass)) + else: + item = '<span class="%s">%s'%(markupclass,item) + # add endings for line and string tokens + pos = item.rfind('\n') + if pos != -1: + item=item[:pos]+'</span></span>\n' + store.append(item) + # add start tags for lines + toktext = '<span class="py_line">'.join(store) + # Send text + if toktype != LINENUMBER: + if toktype == TEXT and self.textFlag == 'DIV': + startspan = '<div class="%s">'%(markupclass) + endspan = '</div>' + elif toktype == TEXT and self.textFlag == 'RAW': + startspan,endspan = ('','') + else: + startspan = '<span class="%s">'%(markupclass) + endspan = '</span>' + self.out.write(''.join([startspan, toktext, endspan])) + else: + self.out.write(toktext) + return + + def _doCSSHeader(self): + if self.header != '': + self.out.write('%s\n'%self.header) + else: + name = MARKUPDICT.get(NAME) + self.out.write('<div class="%s"># %s <br> \ +# %s</div><hr>\n'%(name, self.title, time.ctime())) + + def _doCSSFooter(self): + # Optional + if self.footer != '': + self.out.write('%s\n'%self.footer) + else: + self.out.write('<hr><div class="%s"># %s <br> \ +# %s</div>\n'%(MARKUPDICT.get(NAME),self.title, time.ctime())) + + def _doCSSEnd(self): + # End of css/html page + self.out.write(self.colors.get(CODEEND,'</pre>\n')) + # Write a little info at the bottom + self._doPageFooter() + self.out.write('</body></html>\n') + return + + ################################################## XHTML markup functions + + def _doXHTMLStart(self): + # XHTML is really just XML + HTML 4.01. + # We only need to change the page headers, + # and a few tags to get valid XHTML. + # Start of xhtml page + self.out.write('<?xml version="1.0"?>\n \ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"\n \ + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n \ +<html xmlns="http://www.w3.org/1999/xhtml">\n') + self.out.write('<head><title>%s</title>\n'%(self.title)) + self.out.write(self._getDocumentCreatedBy()) + self.out.write('<meta http-equiv="Content-Type" \ +content="text/html;charset=iso-8859-1"/>\n') + self._doXHTMLStyleSheet() + self.out.write('</head>\n<body>\n') + # Write a little info at the top. + self._doPageHeader() + self.out.write(self.colors.get(CODESTART,'<pre class="py">\n')) + return + + def _doXHTMLStyleSheet(self): + if not self.external: + # write an embedded style sheet + self.out.write(self._sendCSSStyle()) + else: + # write a link to an external style sheet + self.out.write('<link rel="stylesheet" \ +href="pystyle.css" type="text/css"/>\n') + return + + def _sendXHTMLText(self, toktype, toktext): + self._sendCSSText(toktype, toktext) + + def _doXHTMLHeader(self): + # Optional + if self.header: + self.out.write('%s\n'%self.header) + else: + name = MARKUPDICT.get(NAME) + self.out.write('<div class="%s"># %s <br/> \ +# %s</div><hr/>\n '%( + name, self.title, time.ctime())) + + def _doXHTMLFooter(self): + # Optional + if self.footer: + self.out.write('%s\n'%self.footer) + else: + self.out.write('<hr/><div class="%s"># %s <br/> \ +# %s</div>\n'%(MARKUPDICT.get(NAME), self.title, time.ctime())) + + def _doXHTMLEnd(self): + self._doCSSEnd() + +############################################################################# + +if __name__ == '__main__': + cli() + +############################################################################# +# PySourceColor.py +# 2004, 2005 M.E.Farmer Jr. +# Python license diff --git a/paste/util/__init__.py b/paste/util/__init__.py new file mode 100644 index 0000000..ea4ff1e --- /dev/null +++ b/paste/util/__init__.py @@ -0,0 +1,4 @@ +""" +Package for miscellaneous routines that do not depend on other parts +of Paste +""" diff --git a/paste/util/classinit.py b/paste/util/classinit.py new file mode 100644 index 0000000..e4e6b28 --- /dev/null +++ b/paste/util/classinit.py @@ -0,0 +1,42 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +class ClassInitMeta(type): + + def __new__(meta, class_name, bases, new_attrs): + cls = type.__new__(meta, class_name, bases, new_attrs) + if (new_attrs.has_key('__classinit__') + and not isinstance(cls.__classinit__, staticmethod)): + setattr(cls, '__classinit__', + staticmethod(cls.__classinit__.im_func)) + if hasattr(cls, '__classinit__'): + cls.__classinit__(cls, new_attrs) + return cls + +def build_properties(cls, new_attrs): + """ + Given a class and a new set of attributes (as passed in by + __classinit__), create or modify properties based on functions + with special names ending in __get, __set, and __del. + """ + for name, value in new_attrs.items(): + if (name.endswith('__get') or name.endswith('__set') + or name.endswith('__del')): + base = name[:-5] + if hasattr(cls, base): + old_prop = getattr(cls, base) + if not isinstance(old_prop, property): + raise ValueError( + "Attribute %s is a %s, not a property; function %s is named like a property" + % (base, type(old_prop), name)) + attrs = {'fget': old_prop.fget, + 'fset': old_prop.fset, + 'fdel': old_prop.fdel, + 'doc': old_prop.__doc__} + else: + attrs = {} + attrs['f' + name[-3:]] = value + if name.endswith('__get') and value.__doc__: + attrs['doc'] = value.__doc__ + new_prop = property(**attrs) + setattr(cls, base, new_prop) diff --git a/paste/util/classinstance.py b/paste/util/classinstance.py new file mode 100644 index 0000000..6436a44 --- /dev/null +++ b/paste/util/classinstance.py @@ -0,0 +1,38 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +class classinstancemethod(object): + """ + Acts like a class method when called from a class, like an + instance method when called by an instance. The method should + take two arguments, 'self' and 'cls'; one of these will be None + depending on how the method was called. + """ + + def __init__(self, func): + self.func = func + self.__doc__ = func.__doc__ + + def __get__(self, obj, type=None): + return _methodwrapper(self.func, obj=obj, type=type) + +class _methodwrapper(object): + + def __init__(self, func, obj, type): + self.func = func + self.obj = obj + self.type = type + + def __call__(self, *args, **kw): + assert 'self' not in kw and 'cls' not in kw, ( + "You cannot use 'self' or 'cls' arguments to a " + "classinstancemethod") + return self.func(*((self.obj, self.type) + args), **kw) + + def __repr__(self): + if self.obj is None: + return ('<bound class method %s.%s>' + % (self.type.__name__, self.func.func_name)) + else: + return ('<bound method %s.%s of %r>' + % (self.type.__name__, self.func.func_name, self.obj)) diff --git a/paste/util/converters.py b/paste/util/converters.py new file mode 100644 index 0000000..11451bc --- /dev/null +++ b/paste/util/converters.py @@ -0,0 +1,30 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +import six + + +def asbool(obj): + if isinstance(obj, (six.binary_type, six.text_type)): + obj = obj.strip().lower() + if obj in ['true', 'yes', 'on', 'y', 't', '1']: + return True + elif obj in ['false', 'no', 'off', 'n', 'f', '0']: + return False + else: + raise ValueError( + "String is not true/false: %r" % obj) + return bool(obj) + +def aslist(obj, sep=None, strip=True): + if isinstance(obj, (six.binary_type, six.text_type)): + lst = obj.split(sep) + if strip: + lst = [v.strip() for v in lst] + return lst + elif isinstance(obj, (list, tuple)): + return obj + elif obj is None: + return [] + else: + return [obj] diff --git a/paste/util/dateinterval.py b/paste/util/dateinterval.py new file mode 100644 index 0000000..023bce4 --- /dev/null +++ b/paste/util/dateinterval.py @@ -0,0 +1,104 @@ +""" +DateInterval.py + +Convert interval strings (in the form of 1w2d, etc) to +seconds, and back again. Is not exactly about months or +years (leap years in particular). + +Accepts (y)ear, (b)month, (w)eek, (d)ay, (h)our, (m)inute, (s)econd. + +Exports only timeEncode and timeDecode functions. +""" + +import re + +__all__ = ['interval_decode', 'interval_encode'] + +second = 1 +minute = second*60 +hour = minute*60 +day = hour*24 +week = day*7 +month = day*30 +year = day*365 +timeValues = { + 'y': year, + 'b': month, + 'w': week, + 'd': day, + 'h': hour, + 'm': minute, + 's': second, + } +timeOrdered = list(timeValues.items()) +timeOrdered.sort(key=lambda x: x[1], reverse=True) + + +def interval_encode(seconds, include_sign=False): + """Encodes a number of seconds (representing a time interval) + into a form like 1h2d3s. + + >>> interval_encode(10) + '10s' + >>> interval_encode(493939) + '5d17h12m19s' + """ + s = '' + orig = seconds + seconds = abs(seconds) + for char, amount in timeOrdered: + if seconds >= amount: + i, seconds = divmod(seconds, amount) + s += '%i%s' % (i, char) + if orig < 0: + s = '-' + s + elif not orig: + return '0' + elif include_sign: + s = '+' + s + return s + +_timeRE = re.compile(r'[0-9]+[a-zA-Z]') +def interval_decode(s): + """Decodes a number in the format 1h4d3m (1 hour, 3 days, 3 minutes) + into a number of seconds + + >>> interval_decode('40s') + 40 + >>> interval_decode('10000s') + 10000 + >>> interval_decode('3d1w45s') + 864045 + """ + time = 0 + sign = 1 + s = s.strip() + if s.startswith('-'): + s = s[1:] + sign = -1 + elif s.startswith('+'): + s = s[1:] + for match in allMatches(s, _timeRE): + char = match.group(0)[-1].lower() + if char not in timeValues: + # @@: should signal error + continue + time += int(match.group(0)[:-1]) * timeValues[char] + return time + +# @@-sgd 2002-12-23 - this function does not belong in this module, find a better place. +def allMatches(source, regex): + """Return a list of matches for regex in source + """ + pos = 0 + end = len(source) + rv = [] + match = regex.search(source, pos) + while match: + rv.append(match) + match = regex.search(source, match.end() ) + return rv + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/paste/util/datetimeutil.py b/paste/util/datetimeutil.py new file mode 100644 index 0000000..3c6d7d9 --- /dev/null +++ b/paste/util/datetimeutil.py @@ -0,0 +1,359 @@ +# (c) 2005 Clark C. Evans and contributors +# This module is part of the Python Paste Project and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# Some of this code was funded by: http://prometheusresearch.com +""" +Date, Time, and Timespan Parsing Utilities + +This module contains parsing support to create "human friendly" +``datetime`` object parsing. The explicit goal of these routines is +to provide a multi-format date/time support not unlike that found in +Microsoft Excel. In most approaches, the input is very "strict" to +prevent errors -- however, this approach is much more liberal since we +are assuming the user-interface is parroting back the normalized value +and thus the user has immediate feedback if the data is not typed in +correctly. + + ``parse_date`` and ``normalize_date`` + + These functions take a value like '9 jan 2007' and returns either an + ``date`` object, or an ISO 8601 formatted date value such + as '2007-01-09'. There is an option to provide an Oracle database + style output as well, ``09 JAN 2007``, but this is not the default. + + This module always treats '/' delimiters as using US date order + (since the author's clients are US based), hence '1/9/2007' is + January 9th. Since this module treats the '-' as following + European order this supports both modes of data-entry; together + with immediate parroting back the result to the screen, the author + has found this approach to work well in pratice. + + ``parse_time`` and ``normalize_time`` + + These functions take a value like '1 pm' and returns either an + ``time`` object, or an ISO 8601 formatted 24h clock time + such as '13:00'. There is an option to provide for US style time + values, '1:00 PM', however this is not the default. + + ``parse_datetime`` and ``normalize_datetime`` + + These functions take a value like '9 jan 2007 at 1 pm' and returns + either an ``datetime`` object, or an ISO 8601 formatted + return (without the T) such as '2007-01-09 13:00'. There is an + option to provide for Oracle / US style, '09 JAN 2007 @ 1:00 PM', + however this is not the default. + + ``parse_delta`` and ``normalize_delta`` + + These functions take a value like '1h 15m' and returns either an + ``timedelta`` object, or an 2-decimal fixed-point + numerical value in hours, such as '1.25'. The rationale is to + support meeting or time-billing lengths, not to be an accurate + representation in mili-seconds. As such not all valid + ``timedelta`` values will have a normalized representation. + +""" +from datetime import timedelta, time, date +from time import localtime + +__all__ = ['parse_timedelta', 'normalize_timedelta', + 'parse_time', 'normalize_time', + 'parse_date', 'normalize_date'] + +def _number(val): + try: + return int(val) + except: + return None + +# +# timedelta +# +def parse_timedelta(val): + """ + returns a ``timedelta`` object, or None + """ + if not val: + return None + val = val.lower() + if "." in val: + val = float(val) + return timedelta(hours=int(val), minutes=60*(val % 1.0)) + fHour = ("h" in val or ":" in val) + fMin = ("m" in val or ":" in val) + for noise in "minu:teshour()": + val = val.replace(noise, ' ') + val = val.strip() + val = val.split() + hr = 0.0 + mi = 0 + val.reverse() + if fHour: + hr = int(val.pop()) + if fMin: + mi = int(val.pop()) + if len(val) > 0 and not hr: + hr = int(val.pop()) + return timedelta(hours=hr, minutes=mi) + +def normalize_timedelta(val): + """ + produces a normalized string value of the timedelta + + This module returns a normalized time span value consisting of the + number of hours in fractional form. For example '1h 15min' is + formatted as 01.25. + """ + if type(val) == str: + val = parse_timedelta(val) + if not val: + return '' + hr = val.seconds/3600 + mn = (val.seconds % 3600)/60 + return "%d.%02d" % (hr, mn * 100/60) + +# +# time +# +def parse_time(val): + if not val: + return None + hr = mi = 0 + val = val.lower() + amflag = (-1 != val.find('a')) # set if AM is found + pmflag = (-1 != val.find('p')) # set if PM is found + for noise in ":amp.": + val = val.replace(noise, ' ') + val = val.split() + if len(val) > 1: + hr = int(val[0]) + mi = int(val[1]) + else: + val = val[0] + if len(val) < 1: + pass + elif 'now' == val: + tm = localtime() + hr = tm[3] + mi = tm[4] + elif 'noon' == val: + hr = 12 + elif len(val) < 3: + hr = int(val) + if not amflag and not pmflag and hr < 7: + hr += 12 + elif len(val) < 5: + hr = int(val[:-2]) + mi = int(val[-2:]) + else: + hr = int(val[:1]) + if amflag and hr >= 12: + hr = hr - 12 + if pmflag and hr < 12: + hr = hr + 12 + return time(hr, mi) + +def normalize_time(value, ampm): + if not value: + return '' + if type(value) == str: + value = parse_time(value) + if not ampm: + return "%02d:%02d" % (value.hour, value.minute) + hr = value.hour + am = "AM" + if hr < 1 or hr > 23: + hr = 12 + elif hr >= 12: + am = "PM" + if hr > 12: + hr = hr - 12 + return "%02d:%02d %s" % (hr, value.minute, am) + +# +# Date Processing +# + +_one_day = timedelta(days=1) + +_str2num = {'jan':1, 'feb':2, 'mar':3, 'apr':4, 'may':5, 'jun':6, + 'jul':7, 'aug':8, 'sep':9, 'oct':10, 'nov':11, 'dec':12 } + +def _month(val): + for (key, mon) in _str2num.items(): + if key in val: + return mon + raise TypeError("unknown month '%s'" % val) + +_days_in_month = {1: 31, 2: 28, 3: 31, 4: 30, 5: 31, 6: 30, + 7: 31, 8: 31, 9: 30, 10: 31, 11: 30, 12: 31, + } +_num2str = {1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun', + 7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec', + } +_wkdy = ("mon", "tue", "wed", "thu", "fri", "sat", "sun") + +def parse_date(val): + if not(val): + return None + val = val.lower() + now = None + + # optimized check for YYYY-MM-DD + strict = val.split("-") + if len(strict) == 3: + (y, m, d) = strict + if "+" in d: + d = d.split("+")[0] + if " " in d: + d = d.split(" ")[0] + try: + now = date(int(y), int(m), int(d)) + val = "xxx" + val[10:] + except ValueError: + pass + + # allow for 'now', 'mon', 'tue', etc. + if not now: + chk = val[:3] + if chk in ('now','tod'): + now = date.today() + elif chk in _wkdy: + now = date.today() + idx = list(_wkdy).index(chk) + 1 + while now.isoweekday() != idx: + now += _one_day + + # allow dates to be modified via + or - /w number of days, so + # that now+3 is three days from now + if now: + tail = val[3:].strip() + tail = tail.replace("+"," +").replace("-"," -") + for item in tail.split(): + try: + days = int(item) + except ValueError: + pass + else: + now += timedelta(days=days) + return now + + # ok, standard parsing + yr = mo = dy = None + for noise in ('/', '-', ',', '*'): + val = val.replace(noise, ' ') + for noise in _wkdy: + val = val.replace(noise, ' ') + out = [] + last = False + ldig = False + for ch in val: + if ch.isdigit(): + if last and not ldig: + out.append(' ') + last = ldig = True + else: + if ldig: + out.append(' ') + ldig = False + last = True + out.append(ch) + val = "".join(out).split() + if 3 == len(val): + a = _number(val[0]) + b = _number(val[1]) + c = _number(val[2]) + if len(val[0]) == 4: + yr = a + if b: # 1999 6 23 + mo = b + dy = c + else: # 1999 Jun 23 + mo = _month(val[1]) + dy = c + elif a is not None and a > 0: + yr = c + if len(val[2]) < 4: + raise TypeError("four digit year required") + if b: # 6 23 1999 + dy = b + mo = a + else: # 23 Jun 1999 + dy = a + mo = _month(val[1]) + else: # Jun 23, 2000 + dy = b + yr = c + if len(val[2]) < 4: + raise TypeError("four digit year required") + mo = _month(val[0]) + elif 2 == len(val): + a = _number(val[0]) + b = _number(val[1]) + if a is not None and a > 999: + yr = a + dy = 1 + if b is not None and b > 0: # 1999 6 + mo = b + else: # 1999 Jun + mo = _month(val[1]) + elif a is not None and a > 0: + if b is not None and b > 999: # 6 1999 + mo = a + yr = b + dy = 1 + elif b is not None and b > 0: # 6 23 + mo = a + dy = b + else: # 23 Jun + dy = a + mo = _month(val[1]) + else: + if b > 999: # Jun 2001 + yr = b + dy = 1 + else: # Jun 23 + dy = b + mo = _month(val[0]) + elif 1 == len(val): + val = val[0] + if not val.isdigit(): + mo = _month(val) + if mo is not None: + dy = 1 + else: + v = _number(val) + val = str(v) + if 8 == len(val): # 20010623 + yr = _number(val[:4]) + mo = _number(val[4:6]) + dy = _number(val[6:]) + elif len(val) in (3,4): + if v is not None and v > 1300: # 2004 + yr = v + mo = 1 + dy = 1 + else: # 1202 + mo = _number(val[:-2]) + dy = _number(val[-2:]) + elif v < 32: + dy = v + else: + raise TypeError("four digit year required") + tm = localtime() + if mo is None: + mo = tm[1] + if dy is None: + dy = tm[2] + if yr is None: + yr = tm[0] + return date(yr, mo, dy) + +def normalize_date(val, iso8601=True): + if not val: + return '' + if type(val) == str: + val = parse_date(val) + if iso8601: + return "%4d-%02d-%02d" % (val.year, val.month, val.day) + return "%02d %s %4d" % (val.day, _num2str[val.month], val.year) diff --git a/paste/util/filemixin.py b/paste/util/filemixin.py new file mode 100644 index 0000000..b06b039 --- /dev/null +++ b/paste/util/filemixin.py @@ -0,0 +1,53 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +class FileMixin(object): + + """ + Used to provide auxiliary methods to objects simulating files. + Objects must implement write, and read if they are input files. + Also they should implement close. + + Other methods you may wish to override: + * flush() + * seek(offset[, whence]) + * tell() + * truncate([size]) + + Attributes you may wish to provide: + * closed + * encoding (you should also respect that in write()) + * mode + * newlines (hard to support) + * softspace + """ + + def flush(self): + pass + + def next(self): + return self.readline() + + def readline(self, size=None): + # @@: This is a lame implementation; but a buffer would probably + # be necessary for a better implementation + output = [] + while 1: + next = self.read(1) + if not next: + return ''.join(output) + output.append(next) + if size and size > 0 and len(output) >= size: + return ''.join(output) + if next == '\n': + # @@: also \r? + return ''.join(output) + + def xreadlines(self): + return self + + def writelines(self, lines): + for line in lines: + self.write(line) + + diff --git a/paste/util/finddata.py b/paste/util/finddata.py new file mode 100644 index 0000000..bb7c760 --- /dev/null +++ b/paste/util/finddata.py @@ -0,0 +1,98 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +# Note: you may want to copy this into your setup.py file verbatim, as +# you can't import this from another package, when you don't know if +# that package is installed yet. + +from __future__ import print_function +import os +import sys +from fnmatch import fnmatchcase +from distutils.util import convert_path + +# Provided as an attribute, so you can append to these instead +# of replicating them: +standard_exclude = ('*.py', '*.pyc', '*$py.class', '*~', '.*', '*.bak') +standard_exclude_directories = ('.*', 'CVS', '_darcs', './build', + './dist', 'EGG-INFO', '*.egg-info') + +def find_package_data( + where='.', package='', + exclude=standard_exclude, + exclude_directories=standard_exclude_directories, + only_in_packages=True, + show_ignored=False): + """ + Return a dictionary suitable for use in ``package_data`` + in a distutils ``setup.py`` file. + + The dictionary looks like:: + + {'package': [files]} + + Where ``files`` is a list of all the files in that package that + don't match anything in ``exclude``. + + If ``only_in_packages`` is true, then top-level directories that + are not packages won't be included (but directories under packages + will). + + Directories matching any pattern in ``exclude_directories`` will + be ignored; by default directories with leading ``.``, ``CVS``, + and ``_darcs`` will be ignored. + + If ``show_ignored`` is true, then all the files that aren't + included in package data are shown on stderr (for debugging + purposes). + + Note patterns use wildcards, or can be exact paths (including + leading ``./``), and all searching is case-insensitive. + """ + + out = {} + stack = [(convert_path(where), '', package, only_in_packages)] + while stack: + where, prefix, package, only_in_packages = stack.pop(0) + for name in os.listdir(where): + fn = os.path.join(where, name) + if os.path.isdir(fn): + bad_name = False + for pattern in exclude_directories: + if (fnmatchcase(name, pattern) + or fn.lower() == pattern.lower()): + bad_name = True + if show_ignored: + print("Directory %s ignored by pattern %s" + % (fn, pattern), file=sys.stderr) + break + if bad_name: + continue + if (os.path.isfile(os.path.join(fn, '__init__.py')) + and not prefix): + if not package: + new_package = name + else: + new_package = package + '.' + name + stack.append((fn, '', new_package, False)) + else: + stack.append((fn, prefix + name + '/', package, only_in_packages)) + elif package or not only_in_packages: + # is a file + bad_name = False + for pattern in exclude: + if (fnmatchcase(name, pattern) + or fn.lower() == pattern.lower()): + bad_name = True + if show_ignored: + print("File %s ignored by pattern %s" + % (fn, pattern), file=sys.stderr) + break + if bad_name: + continue + out.setdefault(package, []).append(prefix+name) + return out + +if __name__ == '__main__': + import pprint + pprint.pprint( + find_package_data(show_ignored=True)) diff --git a/paste/util/findpackage.py b/paste/util/findpackage.py new file mode 100644 index 0000000..9d653e5 --- /dev/null +++ b/paste/util/findpackage.py @@ -0,0 +1,26 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +import sys +import os + +def find_package(dir): + """ + Given a directory, finds the equivalent package name. If it + is directly in sys.path, returns ''. + """ + dir = os.path.abspath(dir) + orig_dir = dir + path = map(os.path.abspath, sys.path) + packages = [] + last_dir = None + while 1: + if dir in path: + return '.'.join(packages) + packages.insert(0, os.path.basename(dir)) + dir = os.path.dirname(dir) + if last_dir == dir: + raise ValueError( + "%s is not under any path found in sys.path" % orig_dir) + last_dir = dir + diff --git a/paste/util/import_string.py b/paste/util/import_string.py new file mode 100644 index 0000000..a10db18 --- /dev/null +++ b/paste/util/import_string.py @@ -0,0 +1,95 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +""" +'imports' a string -- converts a string to a Python object, importing +any necessary modules and evaluating the expression. Everything +before the : in an import expression is the module path; everything +after is an expression to be evaluated in the namespace of that +module. + +Alternately, if no : is present, then import the modules and get the +attributes as necessary. Arbitrary expressions are not allowed in +that case. +""" + +def eval_import(s): + """ + Import a module, or import an object from a module. + + A module name like ``foo.bar:baz()`` can be used, where + ``foo.bar`` is the module, and ``baz()`` is an expression + evaluated in the context of that module. Note this is not safe on + arbitrary strings because of the eval. + """ + if ':' not in s: + return simple_import(s) + module_name, expr = s.split(':', 1) + module = import_module(module_name) + obj = eval(expr, module.__dict__) + return obj + +def simple_import(s): + """ + Import a module, or import an object from a module. + + A name like ``foo.bar.baz`` can be a module ``foo.bar.baz`` or a + module ``foo.bar`` with an object ``baz`` in it, or a module + ``foo`` with an object ``bar`` with an attribute ``baz``. + """ + parts = s.split('.') + module = import_module(parts[0]) + name = parts[0] + parts = parts[1:] + last_import_error = None + while parts: + name += '.' + parts[0] + try: + module = import_module(name) + parts = parts[1:] + except ImportError as e: + last_import_error = e + break + obj = module + while parts: + try: + obj = getattr(module, parts[0]) + except AttributeError: + raise ImportError( + "Cannot find %s in module %r (stopped importing modules with error %s)" % (parts[0], module, last_import_error)) + parts = parts[1:] + return obj + +def import_module(s): + """ + Import a module. + """ + mod = __import__(s) + parts = s.split('.') + for part in parts[1:]: + mod = getattr(mod, part) + return mod + +def try_import_module(module_name): + """ + Imports a module, but catches import errors. Only catches errors + when that module doesn't exist; if that module itself has an + import error it will still get raised. Returns None if the module + doesn't exist. + """ + try: + return import_module(module_name) + except ImportError as e: + if not getattr(e, 'args', None): + raise + desc = e.args[0] + if not desc.startswith('No module named '): + raise + desc = desc[len('No module named '):] + # If you import foo.bar.baz, the bad import could be any + # of foo.bar.baz, bar.baz, or baz; we'll test them all: + parts = module_name.split('.') + for i in range(len(parts)): + if desc == '.'.join(parts[i:]): + return None + raise diff --git a/paste/util/intset.py b/paste/util/intset.py new file mode 100644 index 0000000..3e026e2 --- /dev/null +++ b/paste/util/intset.py @@ -0,0 +1,515 @@ +# -*- coding: iso-8859-15 -*- +"""Immutable integer set type. + +Integer set class. + +Copyright (C) 2006, Heiko Wundram. +Released under the MIT license. +""" +import six + +# Version information +# ------------------- + +__author__ = "Heiko Wundram <me@modelnine.org>" +__version__ = "0.2" +__revision__ = "6" +__date__ = "2006-01-20" + + +# Utility classes +# --------------- + +class _Infinity(object): + """Internal type used to represent infinity values.""" + + __slots__ = ["_neg"] + + def __init__(self,neg): + self._neg = neg + + def __lt__(self,value): + if not isinstance(value, _VALID_TYPES): + return NotImplemented + return ( self._neg and + not ( isinstance(value,_Infinity) and value._neg ) ) + + def __le__(self,value): + if not isinstance(value, _VALID_TYPES): + return NotImplemented + return self._neg + + def __gt__(self,value): + if not isinstance(value, _VALID_TYPES): + return NotImplemented + return not ( self._neg or + ( isinstance(value,_Infinity) and not value._neg ) ) + + def __ge__(self,value): + if not isinstance(value, _VALID_TYPES): + return NotImplemented + return not self._neg + + def __eq__(self,value): + if not isinstance(value, _VALID_TYPES): + return NotImplemented + return isinstance(value,_Infinity) and self._neg == value._neg + + def __ne__(self,value): + if not isinstance(value, _VALID_TYPES): + return NotImplemented + return not isinstance(value,_Infinity) or self._neg != value._neg + + def __repr__(self): + return "None" + +_VALID_TYPES = six.integer_types + (_Infinity,) + + + +# Constants +# --------- + +_MININF = _Infinity(True) +_MAXINF = _Infinity(False) + + +# Integer set class +# ----------------- + +class IntSet(object): + """Integer set class with efficient storage in a RLE format of ranges. + Supports minus and plus infinity in the range.""" + + __slots__ = ["_ranges","_min","_max","_hash"] + + def __init__(self,*args,**kwargs): + """Initialize an integer set. The constructor accepts an unlimited + number of arguments that may either be tuples in the form of + (start,stop) where either start or stop may be a number or None to + represent maximum/minimum in that direction. The range specified by + (start,stop) is always inclusive (differing from the builtin range + operator). + + Keyword arguments that can be passed to an integer set are min and + max, which specify the minimum and maximum number in the set, + respectively. You can also pass None here to represent minus or plus + infinity, which is also the default. + """ + + # Special case copy constructor. + if len(args) == 1 and isinstance(args[0],IntSet): + if kwargs: + raise ValueError("No keyword arguments for copy constructor.") + self._min = args[0]._min + self._max = args[0]._max + self._ranges = args[0]._ranges + self._hash = args[0]._hash + return + + # Initialize set. + self._ranges = [] + + # Process keyword arguments. + self._min = kwargs.pop("min",_MININF) + self._max = kwargs.pop("max",_MAXINF) + if self._min is None: + self._min = _MININF + if self._max is None: + self._max = _MAXINF + + # Check keyword arguments. + if kwargs: + raise ValueError("Invalid keyword argument.") + if not ( isinstance(self._min, six.integer_types) or self._min is _MININF ): + raise TypeError("Invalid type of min argument.") + if not ( isinstance(self._max, six.integer_types) or self._max is _MAXINF ): + raise TypeError("Invalid type of max argument.") + if ( self._min is not _MININF and self._max is not _MAXINF and + self._min > self._max ): + raise ValueError("Minimum is not smaller than maximum.") + if isinstance(self._max, six.integer_types): + self._max += 1 + + # Process arguments. + for arg in args: + if isinstance(arg, six.integer_types): + start, stop = arg, arg+1 + elif isinstance(arg,tuple): + if len(arg) != 2: + raise ValueError("Invalid tuple, must be (start,stop).") + + # Process argument. + start, stop = arg + if start is None: + start = self._min + if stop is None: + stop = self._max + + # Check arguments. + if not ( isinstance(start, six.integer_types) or start is _MININF ): + raise TypeError("Invalid type of tuple start.") + if not ( isinstance(stop, six.integer_types) or stop is _MAXINF ): + raise TypeError("Invalid type of tuple stop.") + if ( start is not _MININF and stop is not _MAXINF and + start > stop ): + continue + if isinstance(stop, six.integer_types): + stop += 1 + else: + raise TypeError("Invalid argument.") + + if start > self._max: + continue + elif start < self._min: + start = self._min + if stop < self._min: + continue + elif stop > self._max: + stop = self._max + self._ranges.append((start,stop)) + + # Normalize set. + self._normalize() + + # Utility functions for set operations + # ------------------------------------ + + def _iterranges(self,r1,r2,minval=_MININF,maxval=_MAXINF): + curval = minval + curstates = {"r1":False,"r2":False} + imax, jmax = 2*len(r1), 2*len(r2) + i, j = 0, 0 + while i < imax or j < jmax: + if i < imax and ( ( j < jmax and + r1[i>>1][i&1] < r2[j>>1][j&1] ) or + j == jmax ): + cur_r, newname, newstate = r1[i>>1][i&1], "r1", not (i&1) + i += 1 + else: + cur_r, newname, newstate = r2[j>>1][j&1], "r2", not (j&1) + j += 1 + if curval < cur_r: + if cur_r > maxval: + break + yield curstates, (curval,cur_r) + curval = cur_r + curstates[newname] = newstate + if curval < maxval: + yield curstates, (curval,maxval) + + def _normalize(self): + self._ranges.sort() + i = 1 + while i < len(self._ranges): + if self._ranges[i][0] < self._ranges[i-1][1]: + self._ranges[i-1] = (self._ranges[i-1][0], + max(self._ranges[i-1][1], + self._ranges[i][1])) + del self._ranges[i] + else: + i += 1 + self._ranges = tuple(self._ranges) + self._hash = hash(self._ranges) + + def __coerce__(self,other): + if isinstance(other,IntSet): + return self, other + elif isinstance(other, six.integer_types + (tuple,)): + try: + return self, self.__class__(other) + except TypeError: + # Catch a type error, in that case the structure specified by + # other is something we can't coerce, return NotImplemented. + # ValueErrors are not caught, they signal that the data was + # invalid for the constructor. This is appropriate to signal + # as a ValueError to the caller. + return NotImplemented + elif isinstance(other,list): + try: + return self, self.__class__(*other) + except TypeError: + # See above. + return NotImplemented + return NotImplemented + + # Set function definitions + # ------------------------ + + def _make_function(name,type,doc,pall,pany=None): + """Makes a function to match two ranges. Accepts two types: either + 'set', which defines a function which returns a set with all ranges + matching pall (pany is ignored), or 'bool', which returns True if pall + matches for all ranges and pany matches for any one range. doc is the + dostring to give this function. pany may be none to ignore the any + match. + + The predicates get a dict with two keys, 'r1', 'r2', which denote + whether the current range is present in range1 (self) and/or range2 + (other) or none of the two, respectively.""" + + if type == "set": + def f(self,other): + coerced = self.__coerce__(other) + if coerced is NotImplemented: + return NotImplemented + other = coerced[1] + newset = self.__class__.__new__(self.__class__) + newset._min = min(self._min,other._min) + newset._max = max(self._max,other._max) + newset._ranges = [] + for states, (start,stop) in \ + self._iterranges(self._ranges,other._ranges, + newset._min,newset._max): + if pall(states): + if newset._ranges and newset._ranges[-1][1] == start: + newset._ranges[-1] = (newset._ranges[-1][0],stop) + else: + newset._ranges.append((start,stop)) + newset._ranges = tuple(newset._ranges) + newset._hash = hash(self._ranges) + return newset + elif type == "bool": + def f(self,other): + coerced = self.__coerce__(other) + if coerced is NotImplemented: + return NotImplemented + other = coerced[1] + _min = min(self._min,other._min) + _max = max(self._max,other._max) + found = not pany + for states, (start,stop) in \ + self._iterranges(self._ranges,other._ranges,_min,_max): + if not pall(states): + return False + found = found or pany(states) + return found + else: + raise ValueError("Invalid type of function to create.") + try: + f.func_name = name + except TypeError: + pass + f.func_doc = doc + return f + + # Intersection. + __and__ = _make_function("__and__","set", + "Intersection of two sets as a new set.", + lambda s: s["r1"] and s["r2"]) + __rand__ = _make_function("__rand__","set", + "Intersection of two sets as a new set.", + lambda s: s["r1"] and s["r2"]) + intersection = _make_function("intersection","set", + "Intersection of two sets as a new set.", + lambda s: s["r1"] and s["r2"]) + + # Union. + __or__ = _make_function("__or__","set", + "Union of two sets as a new set.", + lambda s: s["r1"] or s["r2"]) + __ror__ = _make_function("__ror__","set", + "Union of two sets as a new set.", + lambda s: s["r1"] or s["r2"]) + union = _make_function("union","set", + "Union of two sets as a new set.", + lambda s: s["r1"] or s["r2"]) + + # Difference. + __sub__ = _make_function("__sub__","set", + "Difference of two sets as a new set.", + lambda s: s["r1"] and not s["r2"]) + __rsub__ = _make_function("__rsub__","set", + "Difference of two sets as a new set.", + lambda s: s["r2"] and not s["r1"]) + difference = _make_function("difference","set", + "Difference of two sets as a new set.", + lambda s: s["r1"] and not s["r2"]) + + # Symmetric difference. + __xor__ = _make_function("__xor__","set", + "Symmetric difference of two sets as a new set.", + lambda s: s["r1"] ^ s["r2"]) + __rxor__ = _make_function("__rxor__","set", + "Symmetric difference of two sets as a new set.", + lambda s: s["r1"] ^ s["r2"]) + symmetric_difference = _make_function("symmetric_difference","set", + "Symmetric difference of two sets as a new set.", + lambda s: s["r1"] ^ s["r2"]) + + # Containership testing. + __contains__ = _make_function("__contains__","bool", + "Returns true if self is superset of other.", + lambda s: s["r1"] or not s["r2"]) + issubset = _make_function("issubset","bool", + "Returns true if self is subset of other.", + lambda s: s["r2"] or not s["r1"]) + istruesubset = _make_function("istruesubset","bool", + "Returns true if self is true subset of other.", + lambda s: s["r2"] or not s["r1"], + lambda s: s["r2"] and not s["r1"]) + issuperset = _make_function("issuperset","bool", + "Returns true if self is superset of other.", + lambda s: s["r1"] or not s["r2"]) + istruesuperset = _make_function("istruesuperset","bool", + "Returns true if self is true superset of other.", + lambda s: s["r1"] or not s["r2"], + lambda s: s["r1"] and not s["r2"]) + overlaps = _make_function("overlaps","bool", + "Returns true if self overlaps with other.", + lambda s: True, + lambda s: s["r1"] and s["r2"]) + + # Comparison. + __eq__ = _make_function("__eq__","bool", + "Returns true if self is equal to other.", + lambda s: not ( s["r1"] ^ s["r2"] )) + __ne__ = _make_function("__ne__","bool", + "Returns true if self is different to other.", + lambda s: True, + lambda s: s["r1"] ^ s["r2"]) + + # Clean up namespace. + del _make_function + + # Define other functions. + def inverse(self): + """Inverse of set as a new set.""" + + newset = self.__class__.__new__(self.__class__) + newset._min = self._min + newset._max = self._max + newset._ranges = [] + laststop = self._min + for r in self._ranges: + if laststop < r[0]: + newset._ranges.append((laststop,r[0])) + laststop = r[1] + if laststop < self._max: + newset._ranges.append((laststop,self._max)) + return newset + + __invert__ = inverse + + # Hashing + # ------- + + def __hash__(self): + """Returns a hash value representing this integer set. As the set is + always stored normalized, the hash value is guaranteed to match for + matching ranges.""" + + return self._hash + + # Iterating + # --------- + + def __len__(self): + """Get length of this integer set. In case the length is larger than + 2**31 (including infinitely sized integer sets), it raises an + OverflowError. This is due to len() restricting the size to + 0 <= len < 2**31.""" + + if not self._ranges: + return 0 + if self._ranges[0][0] is _MININF or self._ranges[-1][1] is _MAXINF: + raise OverflowError("Infinitely sized integer set.") + rlen = 0 + for r in self._ranges: + rlen += r[1]-r[0] + if rlen >= 2**31: + raise OverflowError("Integer set bigger than 2**31.") + return rlen + + def len(self): + """Returns the length of this integer set as an integer. In case the + length is infinite, returns -1. This function exists because of a + limitation of the builtin len() function which expects values in + the range 0 <= len < 2**31. Use this function in case your integer + set might be larger.""" + + if not self._ranges: + return 0 + if self._ranges[0][0] is _MININF or self._ranges[-1][1] is _MAXINF: + return -1 + rlen = 0 + for r in self._ranges: + rlen += r[1]-r[0] + return rlen + + def __nonzero__(self): + """Returns true if this integer set contains at least one item.""" + + return bool(self._ranges) + + def __iter__(self): + """Iterate over all values in this integer set. Iteration always starts + by iterating from lowest to highest over the ranges that are bounded. + After processing these, all ranges that are unbounded (maximum 2) are + yielded intermixed.""" + + ubranges = [] + for r in self._ranges: + if r[0] is _MININF: + if r[1] is _MAXINF: + ubranges.extend(([0,1],[-1,-1])) + else: + ubranges.append([r[1]-1,-1]) + elif r[1] is _MAXINF: + ubranges.append([r[0],1]) + else: + for val in xrange(r[0],r[1]): + yield val + if ubranges: + while True: + for ubrange in ubranges: + yield ubrange[0] + ubrange[0] += ubrange[1] + + # Printing + # -------- + + def __repr__(self): + """Return a representation of this integer set. The representation is + executable to get an equal integer set.""" + + rv = [] + for start, stop in self._ranges: + if ( isinstance(start, six.integer_types) and isinstance(stop, six.integer_types) + and stop-start == 1 ): + rv.append("%r" % start) + elif isinstance(stop, six.integer_types): + rv.append("(%r,%r)" % (start,stop-1)) + else: + rv.append("(%r,%r)" % (start,stop)) + if self._min is not _MININF: + rv.append("min=%r" % self._min) + if self._max is not _MAXINF: + rv.append("max=%r" % self._max) + return "%s(%s)" % (self.__class__.__name__,",".join(rv)) + +if __name__ == "__main__": + # Little test script demonstrating functionality. + x = IntSet((10,20),30) + y = IntSet((10,20)) + z = IntSet((10,20),30,(15,19),min=0,max=40) + print(x) + print(x&110) + print(x|110) + print(x^(15,25)) + print(x-12) + print(12 in x) + print(x.issubset(x)) + print(y.issubset(x)) + print(x.istruesubset(x)) + print(y.istruesubset(x)) + for val in x: + print(val) + print(x.inverse()) + print(x == z) + print(x == y) + print(x != y) + print(hash(x)) + print(hash(z)) + print(len(x)) + print(x.len()) diff --git a/paste/util/ip4.py b/paste/util/ip4.py new file mode 100644 index 0000000..9ce17b8 --- /dev/null +++ b/paste/util/ip4.py @@ -0,0 +1,274 @@ +# -*- coding: iso-8859-15 -*- +"""IP4 address range set implementation. + +Implements an IPv4-range type. + +Copyright (C) 2006, Heiko Wundram. +Released under the MIT-license. +""" + +# Version information +# ------------------- + +__author__ = "Heiko Wundram <me@modelnine.org>" +__version__ = "0.2" +__revision__ = "3" +__date__ = "2006-01-20" + + +# Imports +# ------- + +from paste.util import intset +import socket +import six + + +# IP4Range class +# -------------- + +class IP4Range(intset.IntSet): + """IP4 address range class with efficient storage of address ranges. + Supports all set operations.""" + + _MINIP4 = 0 + _MAXIP4 = (1<<32) - 1 + _UNITYTRANS = "".join([chr(n) for n in range(256)]) + _IPREMOVE = "0123456789." + + def __init__(self,*args): + """Initialize an ip4range class. The constructor accepts an unlimited + number of arguments that may either be tuples in the form (start,stop), + integers, longs or strings, where start and stop in a tuple may + also be of the form integer, long or string. + + Passing an integer or long means passing an IPv4-address that's already + been converted to integer notation, whereas passing a string specifies + an address where this conversion still has to be done. A string + address may be in the following formats: + + - 1.2.3.4 - a plain address, interpreted as a single address + - 1.2.3 - a set of addresses, interpreted as 1.2.3.0-1.2.3.255 + - localhost - hostname to look up, interpreted as single address + - 1.2.3<->5 - a set of addresses, interpreted as 1.2.3.0-1.2.5.255 + - 1.2.0.0/16 - a set of addresses, interpreted as 1.2.0.0-1.2.255.255 + + Only the first three notations are valid if you use a string address in + a tuple, whereby notation 2 is interpreted as 1.2.3.0 if specified as + lower bound and 1.2.3.255 if specified as upper bound, not as a range + of addresses. + + Specifying a range is done with the <-> operator. This is necessary + because '-' might be present in a hostname. '<->' shouldn't be, ever. + """ + + # Special case copy constructor. + if len(args) == 1 and isinstance(args[0],IP4Range): + super(IP4Range,self).__init__(args[0]) + return + + # Convert arguments to tuple syntax. + args = list(args) + for i in range(len(args)): + argval = args[i] + if isinstance(argval,str): + if "<->" in argval: + # Type 4 address. + args[i] = self._parseRange(*argval.split("<->",1)) + continue + elif "/" in argval: + # Type 5 address. + args[i] = self._parseMask(*argval.split("/",1)) + else: + # Type 1, 2 or 3. + args[i] = self._parseAddrRange(argval) + elif isinstance(argval,tuple): + if len(tuple) != 2: + raise ValueError("Tuple is of invalid length.") + addr1, addr2 = argval + if isinstance(addr1,str): + addr1 = self._parseAddrRange(addr1)[0] + elif not isinstance(addr1, six.integer_types): + raise TypeError("Invalid argument.") + if isinstance(addr2,str): + addr2 = self._parseAddrRange(addr2)[1] + elif not isinstance(addr2, six.integer_types): + raise TypeError("Invalid argument.") + args[i] = (addr1,addr2) + elif not isinstance(argval, six.integer_types): + raise TypeError("Invalid argument.") + + # Initialize the integer set. + super(IP4Range,self).__init__(min=self._MINIP4,max=self._MAXIP4,*args) + + # Parsing functions + # ----------------- + + def _parseRange(self,addr1,addr2): + naddr1, naddr1len = _parseAddr(addr1) + naddr2, naddr2len = _parseAddr(addr2) + if naddr2len < naddr1len: + naddr2 += naddr1&(((1<<((naddr1len-naddr2len)*8))-1)<< + (naddr2len*8)) + naddr2len = naddr1len + elif naddr2len > naddr1len: + raise ValueError("Range has more dots than address.") + naddr1 <<= (4-naddr1len)*8 + naddr2 <<= (4-naddr2len)*8 + naddr2 += (1<<((4-naddr2len)*8))-1 + return (naddr1,naddr2) + + def _parseMask(self,addr,mask): + naddr, naddrlen = _parseAddr(addr) + naddr <<= (4-naddrlen)*8 + try: + if not mask: + masklen = 0 + else: + masklen = int(mask) + if not 0 <= masklen <= 32: + raise ValueError + except ValueError: + try: + mask = _parseAddr(mask,False) + except ValueError: + raise ValueError("Mask isn't parseable.") + remaining = 0 + masklen = 0 + if not mask: + masklen = 0 + else: + while not (mask&1): + remaining += 1 + while (mask&1): + mask >>= 1 + masklen += 1 + if remaining+masklen != 32: + raise ValueError("Mask isn't a proper host mask.") + naddr1 = naddr & (((1<<masklen)-1)<<(32-masklen)) + naddr2 = naddr1 + (1<<(32-masklen)) - 1 + return (naddr1,naddr2) + + def _parseAddrRange(self,addr): + naddr, naddrlen = _parseAddr(addr) + naddr1 = naddr<<((4-naddrlen)*8) + naddr2 = ( (naddr<<((4-naddrlen)*8)) + + (1<<((4-naddrlen)*8)) - 1 ) + return (naddr1,naddr2) + + # Utility functions + # ----------------- + + def _int2ip(self,num): + rv = [] + for i in range(4): + rv.append(str(num&255)) + num >>= 8 + return ".".join(reversed(rv)) + + # Iterating + # --------- + + def iteraddresses(self): + """Returns an iterator which iterates over ips in this iprange. An + IP is returned in string form (e.g. '1.2.3.4').""" + + for v in super(IP4Range,self).__iter__(): + yield self._int2ip(v) + + def iterranges(self): + """Returns an iterator which iterates over ip-ip ranges which build + this iprange if combined. An ip-ip pair is returned in string form + (e.g. '1.2.3.4-2.3.4.5').""" + + for r in self._ranges: + if r[1]-r[0] == 1: + yield self._int2ip(r[0]) + else: + yield '%s-%s' % (self._int2ip(r[0]),self._int2ip(r[1]-1)) + + def itermasks(self): + """Returns an iterator which iterates over ip/mask pairs which build + this iprange if combined. An IP/Mask pair is returned in string form + (e.g. '1.2.3.0/24').""" + + for r in self._ranges: + for v in self._itermasks(r): + yield v + + def _itermasks(self,r): + ranges = [r] + while ranges: + cur = ranges.pop() + curmask = 0 + while True: + curmasklen = 1<<(32-curmask) + start = (cur[0]+curmasklen-1)&(((1<<curmask)-1)<<(32-curmask)) + if start >= cur[0] and start+curmasklen <= cur[1]: + break + else: + curmask += 1 + yield "%s/%s" % (self._int2ip(start),curmask) + if cur[0] < start: + ranges.append((cur[0],start)) + if cur[1] > start+curmasklen: + ranges.append((start+curmasklen,cur[1])) + + __iter__ = iteraddresses + + # Printing + # -------- + + def __repr__(self): + """Returns a string which can be used to reconstruct this iprange.""" + + rv = [] + for start, stop in self._ranges: + if stop-start == 1: + rv.append("%r" % (self._int2ip(start),)) + else: + rv.append("(%r,%r)" % (self._int2ip(start), + self._int2ip(stop-1))) + return "%s(%s)" % (self.__class__.__name__,",".join(rv)) + +def _parseAddr(addr,lookup=True): + if lookup and any(ch not in IP4Range._IPREMOVE for ch in addr): + try: + addr = socket.gethostbyname(addr) + except socket.error: + raise ValueError("Invalid Hostname as argument.") + naddr = 0 + for naddrpos, part in enumerate(addr.split(".")): + if naddrpos >= 4: + raise ValueError("Address contains more than four parts.") + try: + if not part: + part = 0 + else: + part = int(part) + if not 0 <= part < 256: + raise ValueError + except ValueError: + raise ValueError("Address part out of range.") + naddr <<= 8 + naddr += part + return naddr, naddrpos+1 + +def ip2int(addr, lookup=True): + return _parseAddr(addr, lookup=lookup)[0] + +if __name__ == "__main__": + # Little test script. + x = IP4Range("172.22.162.250/24") + y = IP4Range("172.22.162.250","172.22.163.250","172.22.163.253<->255") + print(x) + for val in x.itermasks(): + print(val) + for val in y.itermasks(): + print(val) + for val in (x|y).itermasks(): + print(val) + for val in (x^y).iterranges(): + print(val) + for val in x: + print(val) diff --git a/paste/util/killthread.py b/paste/util/killthread.py new file mode 100644 index 0000000..4df4f42 --- /dev/null +++ b/paste/util/killthread.py @@ -0,0 +1,30 @@ +""" +Kill a thread, from http://sebulba.wikispaces.com/recipe+thread2 +""" +import six +try: + import ctypes +except ImportError: + raise ImportError( + "You cannot use paste.util.killthread without ctypes installed") +if not hasattr(ctypes, 'pythonapi'): + raise ImportError( + "You cannot use paste.util.killthread without ctypes.pythonapi") + +def async_raise(tid, exctype): + """raises the exception, performs cleanup if needed. + + tid is the value given by thread.get_ident() (an integer). + Raise SystemExit to kill a thread.""" + if not isinstance(exctype, (six.class_types, type)): + raise TypeError("Only types can be raised (not instances)") + if not isinstance(tid, int): + raise TypeError("tid must be an integer") + res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), ctypes.py_object(exctype)) + if res == 0: + raise ValueError("invalid thread id") + elif res != 1: + # """if it returns a number greater than one, you're in trouble, + # and you should call it again with exc=NULL to revert the effect""" + ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), 0) + raise SystemError("PyThreadState_SetAsyncExc failed") diff --git a/paste/util/looper.py b/paste/util/looper.py new file mode 100644 index 0000000..b56358a --- /dev/null +++ b/paste/util/looper.py @@ -0,0 +1,156 @@ +""" +Helper for looping over sequences, particular in templates. + +Often in a loop in a template it's handy to know what's next up, +previously up, if this is the first or last item in the sequence, etc. +These can be awkward to manage in a normal Python loop, but using the +looper you can get a better sense of the context. Use like:: + + >>> for loop, item in looper(['a', 'b', 'c']): + ... print("%s %s" % (loop.number, item)) + ... if not loop.last: + ... print('---') + 1 a + --- + 2 b + --- + 3 c + +""" + +__all__ = ['looper'] + +import six + + +class looper(object): + """ + Helper for looping (particularly in templates) + + Use this like:: + + for loop, item in looper(seq): + if loop.first: + ... + """ + + def __init__(self, seq): + self.seq = seq + + def __iter__(self): + return looper_iter(self.seq) + + def __repr__(self): + return '<%s for %r>' % ( + self.__class__.__name__, self.seq) + +class looper_iter(object): + + def __init__(self, seq): + self.seq = list(seq) + self.pos = 0 + + def __iter__(self): + return self + + def next(self): + if self.pos >= len(self.seq): + raise StopIteration + result = loop_pos(self.seq, self.pos), self.seq[self.pos] + self.pos += 1 + return result + __next__ = next + +class loop_pos(object): + + def __init__(self, seq, pos): + self.seq = seq + self.pos = pos + + def __repr__(self): + return '<loop pos=%r at %r>' % ( + self.seq[self.pos], self.pos) + + def index(self): + return self.pos + index = property(index) + + def number(self): + return self.pos + 1 + number = property(number) + + def item(self): + return self.seq[self.pos] + item = property(item) + + def next(self): + try: + return self.seq[self.pos+1] + except IndexError: + return None + next = property(next) + + def previous(self): + if self.pos == 0: + return None + return self.seq[self.pos-1] + previous = property(previous) + + def odd(self): + return not self.pos % 2 + odd = property(odd) + + def even(self): + return self.pos % 2 + even = property(even) + + def first(self): + return self.pos == 0 + first = property(first) + + def last(self): + return self.pos == len(self.seq)-1 + last = property(last) + + def length(self): + return len(self.seq) + length = property(length) + + def first_group(self, getter=None): + """ + Returns true if this item is the start of a new group, + where groups mean that some attribute has changed. The getter + can be None (the item itself changes), an attribute name like + ``'.attr'``, a function, or a dict key or list index. + """ + if self.first: + return True + return self._compare_group(self.item, self.previous, getter) + + def last_group(self, getter=None): + """ + Returns true if this item is the end of a new group, + where groups mean that some attribute has changed. The getter + can be None (the item itself changes), an attribute name like + ``'.attr'``, a function, or a dict key or list index. + """ + if self.last: + return True + return self._compare_group(self.item, self.next, getter) + + def _compare_group(self, item, other, getter): + if getter is None: + return item != other + elif (isinstance(getter, (six.binary_type, six.text_type)) + and getter.startswith('.')): + getter = getter[1:] + if getter.endswith('()'): + getter = getter[:-2] + return getattr(item, getter)() != getattr(other, getter)() + else: + return getattr(item, getter) != getattr(other, getter) + elif callable(getter): + return getter(item) != getter(other) + else: + return item[getter] != other[getter] + diff --git a/paste/util/mimeparse.py b/paste/util/mimeparse.py new file mode 100644 index 0000000..b796c8b --- /dev/null +++ b/paste/util/mimeparse.py @@ -0,0 +1,160 @@ +"""MIME-Type Parser + +This module provides basic functions for handling mime-types. It can handle +matching mime-types against a list of media-ranges. See section 14.1 of +the HTTP specification [RFC 2616] for a complete explanation. + + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + +Based on mimeparse 0.1.2 by Joe Gregorio: + + http://code.google.com/p/mimeparse/ + +Contents: + - parse_mime_type(): Parses a mime-type into its component parts. + - parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q' quality parameter. + - quality(): Determines the quality ('q') of a mime-type when compared against a list of media-ranges. + - quality_parsed(): Just like quality() except the second parameter must be pre-parsed. + - best_match(): Choose the mime-type with the highest quality ('q') from a list of candidates. + - desired_matches(): Filter against a list of desired mime-types in the order the server prefers. + +""" + + +def parse_mime_type(mime_type): + """Carves up a mime-type and returns a tuple of the + (type, subtype, params) where 'params' is a dictionary + of all the parameters for the media range. + For example, the media range 'application/xhtml;q=0.5' would + get parsed into: + + ('application', 'xhtml', {'q', '0.5'}) + """ + type = mime_type.split(';') + type, plist = type[0], type[1:] + try: + type, subtype = type.split('/', 1) + except ValueError: + type, subtype = type.strip() or '*', '*' + else: + type = type.strip() or '*' + subtype = subtype.strip() or '*' + params = {} + for param in plist: + param = param.split('=', 1) + if len(param) == 2: + key, value = param[0].strip(), param[1].strip() + if key and value: + params[key] = value + return type, subtype, params + +def parse_media_range(range): + """Carves up a media range and returns a tuple of the + (type, subtype, params) where 'params' is a dictionary + of all the parameters for the media range. + For example, the media range 'application/*;q=0.5' would + get parsed into: + + ('application', '*', {'q', '0.5'}) + + In addition this function also guarantees that there + is a value for 'q' in the params dictionary, filling it + in with a proper default if necessary. + """ + type, subtype, params = parse_mime_type(range) + try: + if not 0 <= float(params['q']) <= 1: + raise ValueError + except (KeyError, ValueError): + params['q'] = '1' + return type, subtype, params + +def fitness_and_quality_parsed(mime_type, parsed_ranges): + """Find the best match for a given mime-type against + a list of media_ranges that have already been + parsed by parse_media_range(). Returns a tuple of + the fitness value and the value of the 'q' quality + parameter of the best match, or (-1, 0) if no match + was found. Just as for quality_parsed(), 'parsed_ranges' + must be a list of parsed media ranges.""" + best_fitness, best_fit_q = -1, 0 + target_type, target_subtype, target_params = parse_media_range(mime_type) + for type, subtype, params in parsed_ranges: + if (type == target_type + or type == '*' or target_type == '*') and ( + subtype == target_subtype + or subtype == '*' or target_subtype == '*'): + fitness = 0 + if type == target_type: + fitness += 100 + if subtype == target_subtype: + fitness += 10 + for key in target_params: + if key != 'q' and key in params: + if params[key] == target_params[key]: + fitness += 1 + if fitness > best_fitness: + best_fitness = fitness + best_fit_q = params['q'] + return best_fitness, float(best_fit_q) + +def quality_parsed(mime_type, parsed_ranges): + """Find the best match for a given mime-type against + a list of media_ranges that have already been + parsed by parse_media_range(). Returns the + 'q' quality parameter of the best match, 0 if no + match was found. This function behaves the same as quality() + except that 'parsed_ranges' must be a list of + parsed media ranges.""" + return fitness_and_quality_parsed(mime_type, parsed_ranges)[1] + +def quality(mime_type, ranges): + """Returns the quality 'q' of a mime-type when compared + against the media-ranges in ranges. For example: + + >>> quality('text/html','text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5') + 0.7 + + """ + parsed_ranges = map(parse_media_range, ranges.split(',')) + return quality_parsed(mime_type, parsed_ranges) + +def best_match(supported, header): + """Takes a list of supported mime-types and finds the best + match for all the media-ranges listed in header. In case of + ambiguity, whatever comes first in the list will be chosen. + The value of header must be a string that conforms to the format + of the HTTP Accept: header. The value of 'supported' is a list + of mime-types. + + >>> best_match(['application/xbel+xml', 'text/xml'], 'text/*;q=0.5,*/*; q=0.1') + 'text/xml' + """ + if not supported: + return '' + parsed_header = list(map(parse_media_range, header.split(','))) + best_type = max([ + (fitness_and_quality_parsed(mime_type, parsed_header), -n) + for n, mime_type in enumerate(supported)]) + return best_type[0][1] and supported[-best_type[1]] or '' + +def desired_matches(desired, header): + """Takes a list of desired mime-types in the order the server prefers to + send them regardless of the browsers preference. + + Browsers (such as Firefox) technically want XML over HTML depending on how + one reads the specification. This function is provided for a server to + declare a set of desired mime-types it supports, and returns a subset of + the desired list in the same order should each one be Accepted by the + browser. + + >>> desired_matches(['text/html', 'application/xml'], \ + ... 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png') + ['text/html', 'application/xml'] + >>> desired_matches(['text/html', 'application/xml'], 'application/xml,application/json') + ['application/xml'] + """ + parsed_ranges = list(map(parse_media_range, header.split(','))) + return [mimetype for mimetype in desired + if quality_parsed(mimetype, parsed_ranges)] + diff --git a/paste/util/multidict.py b/paste/util/multidict.py new file mode 100644 index 0000000..701d1ac --- /dev/null +++ b/paste/util/multidict.py @@ -0,0 +1,429 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +import cgi +import copy +import six +import sys + +try: + # Python 3 + from collections import MutableMapping as DictMixin +except ImportError: + # Python 2 + from UserDict import DictMixin + +class MultiDict(DictMixin): + + """ + An ordered dictionary that can have multiple values for each key. + Adds the methods getall, getone, mixed, and add to the normal + dictionary interface. + """ + + def __init__(self, *args, **kw): + if len(args) > 1: + raise TypeError( + "MultiDict can only be called with one positional argument") + if args: + if hasattr(args[0], 'iteritems'): + items = args[0].iteritems() + elif hasattr(args[0], 'items'): + items = args[0].items() + else: + items = args[0] + self._items = list(items) + else: + self._items = [] + self._items.extend(six.iteritems(kw)) + + def __getitem__(self, key): + for k, v in self._items: + if k == key: + return v + raise KeyError(repr(key)) + + def __setitem__(self, key, value): + try: + del self[key] + except KeyError: + pass + self._items.append((key, value)) + + def add(self, key, value): + """ + Add the key and value, not overwriting any previous value. + """ + self._items.append((key, value)) + + def getall(self, key): + """ + Return a list of all values matching the key (may be an empty list) + """ + result = [] + for k, v in self._items: + if type(key) == type(k) and key == k: + result.append(v) + return result + + def getone(self, key): + """ + Get one value matching the key, raising a KeyError if multiple + values were found. + """ + v = self.getall(key) + if not v: + raise KeyError('Key not found: %r' % key) + if len(v) > 1: + raise KeyError('Multiple values match %r: %r' % (key, v)) + return v[0] + + def mixed(self): + """ + Returns a dictionary where the values are either single + values, or a list of values when a key/value appears more than + once in this dictionary. This is similar to the kind of + dictionary often used to represent the variables in a web + request. + """ + result = {} + multi = {} + for key, value in self._items: + if key in result: + # We do this to not clobber any lists that are + # *actual* values in this dictionary: + if key in multi: + result[key].append(value) + else: + result[key] = [result[key], value] + multi[key] = None + else: + result[key] = value + return result + + def dict_of_lists(self): + """ + Returns a dictionary where each key is associated with a + list of values. + """ + result = {} + for key, value in self._items: + if key in result: + result[key].append(value) + else: + result[key] = [value] + return result + + def __delitem__(self, key): + items = self._items + found = False + for i in range(len(items)-1, -1, -1): + if type(items[i][0]) == type(key) and items[i][0] == key: + del items[i] + found = True + if not found: + raise KeyError(repr(key)) + + def __contains__(self, key): + for k, v in self._items: + if type(k) == type(key) and k == key: + return True + return False + + has_key = __contains__ + + def clear(self): + self._items = [] + + def copy(self): + return MultiDict(self) + + def setdefault(self, key, default=None): + for k, v in self._items: + if key == k: + return v + self._items.append((key, default)) + return default + + def pop(self, key, *args): + if len(args) > 1: + raise TypeError("pop expected at most 2 arguments, got " + + repr(1 + len(args))) + for i in range(len(self._items)): + if type(self._items[i][0]) == type(key) and self._items[i][0] == key: + v = self._items[i][1] + del self._items[i] + return v + if args: + return args[0] + else: + raise KeyError(repr(key)) + + def popitem(self): + return self._items.pop() + + def update(self, other=None, **kwargs): + if other is None: + pass + elif hasattr(other, 'items'): + self._items.extend(other.items()) + elif hasattr(other, 'keys'): + for k in other.keys(): + self._items.append((k, other[k])) + else: + for k, v in other: + self._items.append((k, v)) + if kwargs: + self.update(kwargs) + + def __repr__(self): + items = ', '.join(['(%r, %r)' % v for v in self._items]) + return '%s([%s])' % (self.__class__.__name__, items) + + def __len__(self): + return len(self._items) + + ## + ## All the iteration: + ## + + def keys(self): + return [k for k, v in self._items] + + def iterkeys(self): + for k, v in self._items: + yield k + + __iter__ = iterkeys + + def items(self): + return self._items[:] + + def iteritems(self): + return iter(self._items) + + def values(self): + return [v for k, v in self._items] + + def itervalues(self): + for k, v in self._items: + yield v + +class UnicodeMultiDict(DictMixin): + """ + A MultiDict wrapper that decodes returned values to unicode on the + fly. Decoding is not applied to assigned values. + + The key/value contents are assumed to be ``str``/``strs`` or + ``str``/``FieldStorages`` (as is returned by the ``paste.request.parse_`` + functions). + + Can optionally also decode keys when the ``decode_keys`` argument is + True. + + ``FieldStorage`` instances are cloned, and the clone's ``filename`` + variable is decoded. Its ``name`` variable is decoded when ``decode_keys`` + is enabled. + + """ + def __init__(self, multi=None, encoding=None, errors='strict', + decode_keys=False): + self.multi = multi + if encoding is None: + encoding = sys.getdefaultencoding() + self.encoding = encoding + self.errors = errors + self.decode_keys = decode_keys + if self.decode_keys: + items = self.multi._items + for index, item in enumerate(items): + key, value = item + key = self._encode_key(key) + items[index] = (key, value) + + def _encode_key(self, key): + if self.decode_keys: + try: + key = key.encode(self.encoding, self.errors) + except AttributeError: + pass + return key + + def _decode_key(self, key): + if self.decode_keys: + try: + key = key.decode(self.encoding, self.errors) + except AttributeError: + pass + return key + + def _decode_value(self, value): + """ + Decode the specified value to unicode. Assumes value is a ``str`` or + `FieldStorage`` object. + + ``FieldStorage`` objects are specially handled. + """ + if isinstance(value, cgi.FieldStorage): + # decode FieldStorage's field name and filename + value = copy.copy(value) + if self.decode_keys and isinstance(value.name, six.binary_type): + value.name = value.name.decode(self.encoding, self.errors) + if six.PY2: + value.filename = value.filename.decode(self.encoding, self.errors) + else: + try: + value = value.decode(self.encoding, self.errors) + except AttributeError: + pass + return value + + def __getitem__(self, key): + key = self._encode_key(key) + return self._decode_value(self.multi.__getitem__(key)) + + def __setitem__(self, key, value): + key = self._encode_key(key) + self.multi.__setitem__(key, value) + + def add(self, key, value): + """ + Add the key and value, not overwriting any previous value. + """ + key = self._encode_key(key) + self.multi.add(key, value) + + def getall(self, key): + """ + Return a list of all values matching the key (may be an empty list) + """ + key = self._encode_key(key) + return [self._decode_value(v) for v in self.multi.getall(key)] + + def getone(self, key): + """ + Get one value matching the key, raising a KeyError if multiple + values were found. + """ + key = self._encode_key(key) + return self._decode_value(self.multi.getone(key)) + + def mixed(self): + """ + Returns a dictionary where the values are either single + values, or a list of values when a key/value appears more than + once in this dictionary. This is similar to the kind of + dictionary often used to represent the variables in a web + request. + """ + unicode_mixed = {} + for key, value in six.iteritems(self.multi.mixed()): + if isinstance(value, list): + value = [self._decode_value(value) for value in value] + else: + value = self._decode_value(value) + unicode_mixed[self._decode_key(key)] = value + return unicode_mixed + + def dict_of_lists(self): + """ + Returns a dictionary where each key is associated with a + list of values. + """ + unicode_dict = {} + for key, value in six.iteritems(self.multi.dict_of_lists()): + value = [self._decode_value(value) for value in value] + unicode_dict[self._decode_key(key)] = value + return unicode_dict + + def __delitem__(self, key): + key = self._encode_key(key) + self.multi.__delitem__(key) + + def __contains__(self, key): + key = self._encode_key(key) + return self.multi.__contains__(key) + + has_key = __contains__ + + def clear(self): + self.multi.clear() + + def copy(self): + return UnicodeMultiDict(self.multi.copy(), self.encoding, self.errors, + decode_keys=self.decode_keys) + + def setdefault(self, key, default=None): + key = self._encode_key(key) + return self._decode_value(self.multi.setdefault(key, default)) + + def pop(self, key, *args): + key = self._encode_key(key) + return self._decode_value(self.multi.pop(key, *args)) + + def popitem(self): + k, v = self.multi.popitem() + return (self._decode_key(k), self._decode_value(v)) + + def __repr__(self): + items = ', '.join(['(%r, %r)' % v for v in self.items()]) + return '%s([%s])' % (self.__class__.__name__, items) + + def __len__(self): + return self.multi.__len__() + + ## + ## All the iteration: + ## + + def keys(self): + return [self._decode_key(k) for k in self.multi.iterkeys()] + + def iterkeys(self): + for k in self.multi.iterkeys(): + yield self._decode_key(k) + + __iter__ = iterkeys + + def items(self): + return [(self._decode_key(k), self._decode_value(v)) for \ + k, v in six.iteritems(self.multi)] + + def iteritems(self): + for k, v in six.iteritems(self.multi): + yield (self._decode_key(k), self._decode_value(v)) + + def values(self): + return [self._decode_value(v) for v in self.multi.itervalues()] + + def itervalues(self): + for v in self.multi.itervalues(): + yield self._decode_value(v) + +__test__ = { + 'general': """ + >>> d = MultiDict(a=1, b=2) + >>> d['a'] + 1 + >>> d.getall('c') + [] + >>> d.add('a', 2) + >>> d['a'] + 1 + >>> d.getall('a') + [1, 2] + >>> d['b'] = 4 + >>> d.getall('b') + [4] + >>> d.keys() + ['a', 'a', 'b'] + >>> d.items() + [('a', 1), ('a', 2), ('b', 4)] + >>> d.mixed() + {'a': [1, 2], 'b': 4} + >>> MultiDict([('a', 'b')], c=2) + MultiDict([('a', 'b'), ('c', 2)]) + """} + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/paste/util/quoting.py b/paste/util/quoting.py new file mode 100644 index 0000000..df0d9da --- /dev/null +++ b/paste/util/quoting.py @@ -0,0 +1,85 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +import cgi +import six +import re +from six.moves import html_entities +from six.moves.urllib.parse import quote, unquote + + +__all__ = ['html_quote', 'html_unquote', 'url_quote', 'url_unquote', + 'strip_html'] + +default_encoding = 'UTF-8' + +def html_quote(v, encoding=None): + r""" + Quote the value (turned to a string) as HTML. This quotes <, >, + and quotes: + """ + encoding = encoding or default_encoding + if v is None: + return '' + elif isinstance(v, six.binary_type): + return cgi.escape(v, 1) + elif isinstance(v, six.text_type): + if six.PY3: + return cgi.escape(v, 1) + else: + return cgi.escape(v.encode(encoding), 1) + else: + if six.PY3: + return cgi.escape(six.text_type(v), 1) + else: + return cgi.escape(six.text_type(v).encode(encoding), 1) + +_unquote_re = re.compile(r'&([a-zA-Z]+);') +def _entity_subber(match, name2c=html_entities.name2codepoint): + code = name2c.get(match.group(1)) + if code: + return six.unichr(code) + else: + return match.group(0) + +def html_unquote(s, encoding=None): + r""" + Decode the value. + + """ + if isinstance(s, six.binary_type): + s = s.decode(encoding or default_encoding) + return _unquote_re.sub(_entity_subber, s) + +def strip_html(s): + # should this use html_unquote? + s = re.sub('<.*?>', '', s) + s = html_unquote(s) + return s + +def no_quote(s): + """ + Quoting that doesn't do anything + """ + return s + +_comment_quote_re = re.compile(r'\-\s*\>') +# Everything but \r, \n, \t: +_bad_chars_re = re.compile('[\x00-\x08\x0b-\x0c\x0e-\x1f]') +def comment_quote(s): + """ + Quote that makes sure text can't escape a comment + """ + comment = str(s) + #comment = _bad_chars_re.sub('', comment) + #print('in ', repr(str(s))) + #print('out', repr(comment)) + comment = _comment_quote_re.sub('->', comment) + return comment + +url_quote = quote +url_unquote = unquote + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/paste/util/scgiserver.py b/paste/util/scgiserver.py new file mode 100644 index 0000000..1c86c86 --- /dev/null +++ b/paste/util/scgiserver.py @@ -0,0 +1,172 @@ +""" +SCGI-->WSGI application proxy, "SWAP". + +(Originally written by Titus Brown.) + +This lets an SCGI front-end like mod_scgi be used to execute WSGI +application objects. To use it, subclass the SWAP class like so:: + + class TestAppHandler(swap.SWAP): + def __init__(self, *args, **kwargs): + self.prefix = '/canal' + self.app_obj = TestAppClass + swap.SWAP.__init__(self, *args, **kwargs) + +where 'TestAppClass' is the application object from WSGI and '/canal' +is the prefix for what is served by the SCGI Web-server-side process. + +Then execute the SCGI handler "as usual" by doing something like this:: + + scgi_server.SCGIServer(TestAppHandler, port=4000).serve() + +and point mod_scgi (or whatever your SCGI front end is) at port 4000. + +Kudos to the WSGI folk for writing a nice PEP & the Quixote folk for +writing a nice extensible SCGI server for Python! +""" + +import six +import sys +import time +from scgi import scgi_server + +def debug(msg): + timestamp = time.strftime("%Y-%m-%d %H:%M:%S", + time.localtime(time.time())) + sys.stderr.write("[%s] %s\n" % (timestamp, msg)) + +class SWAP(scgi_server.SCGIHandler): + """ + SCGI->WSGI application proxy: let an SCGI server execute WSGI + application objects. + """ + app_obj = None + prefix = None + + def __init__(self, *args, **kwargs): + assert self.app_obj, "must set app_obj" + assert self.prefix is not None, "must set prefix" + args = (self,) + args + scgi_server.SCGIHandler.__init__(*args, **kwargs) + + def handle_connection(self, conn): + """ + Handle an individual connection. + """ + input = conn.makefile("r") + output = conn.makefile("w") + + environ = self.read_env(input) + environ['wsgi.input'] = input + environ['wsgi.errors'] = sys.stderr + environ['wsgi.version'] = (1, 0) + environ['wsgi.multithread'] = False + environ['wsgi.multiprocess'] = True + environ['wsgi.run_once'] = False + + # dunno how SCGI does HTTPS signalling; can't test it myself... @CTB + if environ.get('HTTPS','off') in ('on','1'): + environ['wsgi.url_scheme'] = 'https' + else: + environ['wsgi.url_scheme'] = 'http' + + ## SCGI does some weird environ manglement. We need to set + ## SCRIPT_NAME from 'prefix' and then set PATH_INFO from + ## REQUEST_URI. + + prefix = self.prefix + path = environ['REQUEST_URI'][len(prefix):].split('?', 1)[0] + + environ['SCRIPT_NAME'] = prefix + environ['PATH_INFO'] = path + + headers_set = [] + headers_sent = [] + chunks = [] + def write(data): + chunks.append(data) + + def start_response(status, response_headers, exc_info=None): + if exc_info: + try: + if headers_sent: + # Re-raise original exception if headers sent + six.reraise(exc_info[0], exc_info[1], exc_info[2]) + finally: + exc_info = None # avoid dangling circular ref + elif headers_set: + raise AssertionError("Headers already set!") + + headers_set[:] = [status, response_headers] + return write + + ### + + result = self.app_obj(environ, start_response) + try: + for data in result: + chunks.append(data) + + # Before the first output, send the stored headers + if not headers_set: + # Error -- the app never called start_response + status = '500 Server Error' + response_headers = [('Content-type', 'text/html')] + chunks = ["XXX start_response never called"] + else: + status, response_headers = headers_sent[:] = headers_set + + output.write('Status: %s\r\n' % status) + for header in response_headers: + output.write('%s: %s\r\n' % header) + output.write('\r\n') + + for data in chunks: + output.write(data) + finally: + if hasattr(result,'close'): + result.close() + + # SCGI backends use connection closing to signal 'fini'. + try: + input.close() + output.close() + conn.close() + except IOError as err: + debug("IOError while closing connection ignored: %s" % err) + + +def serve_application(application, prefix, port=None, host=None, max_children=None): + """ + Serve the specified WSGI application via SCGI proxy. + + ``application`` + The WSGI application to serve. + + ``prefix`` + The prefix for what is served by the SCGI Web-server-side process. + + ``port`` + Optional port to bind the SCGI proxy to. Defaults to SCGIServer's + default port value. + + ``host`` + Optional host to bind the SCGI proxy to. Defaults to SCGIServer's + default host value. + + ``host`` + Optional maximum number of child processes the SCGIServer will + spawn. Defaults to SCGIServer's default max_children value. + """ + class SCGIAppHandler(SWAP): + def __init__ (self, *args, **kwargs): + self.prefix = prefix + self.app_obj = application + SWAP.__init__(self, *args, **kwargs) + + kwargs = dict(handler_class=SCGIAppHandler) + for kwarg in ('host', 'port', 'max_children'): + if locals()[kwarg] is not None: + kwargs[kwarg] = locals()[kwarg] + + scgi_server.SCGIServer(**kwargs).serve() diff --git a/paste/util/template.py b/paste/util/template.py new file mode 100644 index 0000000..5a63664 --- /dev/null +++ b/paste/util/template.py @@ -0,0 +1,756 @@ +""" +A small templating language + +This implements a small templating language for use internally in +Paste and Paste Script. This language implements if/elif/else, +for/continue/break, expressions, and blocks of Python code. The +syntax is:: + + {{any expression (function calls etc)}} + {{any expression | filter}} + {{for x in y}}...{{endfor}} + {{if x}}x{{elif y}}y{{else}}z{{endif}} + {{py:x=1}} + {{py: + def foo(bar): + return 'baz' + }} + {{default var = default_value}} + {{# comment}} + +You use this with the ``Template`` class or the ``sub`` shortcut. +The ``Template`` class takes the template string and the name of +the template (for errors) and a default namespace. Then (like +``string.Template``) you can call the ``tmpl.substitute(**kw)`` +method to make a substitution (or ``tmpl.substitute(a_dict)``). + +``sub(content, **kw)`` substitutes the template immediately. You +can use ``__name='tmpl.html'`` to set the name of the template. + +If there are syntax errors ``TemplateError`` will be raised. +""" + +import re +import six +import sys +import cgi +from six.moves.urllib.parse import quote +from paste.util.looper import looper + +__all__ = ['TemplateError', 'Template', 'sub', 'HTMLTemplate', + 'sub_html', 'html', 'bunch'] + +token_re = re.compile(r'\{\{|\}\}') +in_re = re.compile(r'\s+in\s+') +var_re = re.compile(r'^[a-z_][a-z0-9_]*$', re.I) + +class TemplateError(Exception): + """Exception raised while parsing a template + """ + + def __init__(self, message, position, name=None): + self.message = message + self.position = position + self.name = name + + def __str__(self): + msg = '%s at line %s column %s' % ( + self.message, self.position[0], self.position[1]) + if self.name: + msg += ' in %s' % self.name + return msg + +class _TemplateContinue(Exception): + pass + +class _TemplateBreak(Exception): + pass + +class Template(object): + + default_namespace = { + 'start_braces': '{{', + 'end_braces': '}}', + 'looper': looper, + } + + default_encoding = 'utf8' + + def __init__(self, content, name=None, namespace=None): + self.content = content + self._unicode = isinstance(content, six.text_type) + self.name = name + self._parsed = parse(content, name=name) + if namespace is None: + namespace = {} + self.namespace = namespace + + def from_filename(cls, filename, namespace=None, encoding=None): + f = open(filename, 'rb') + c = f.read() + f.close() + if encoding: + c = c.decode(encoding) + return cls(content=c, name=filename, namespace=namespace) + + from_filename = classmethod(from_filename) + + def __repr__(self): + return '<%s %s name=%r>' % ( + self.__class__.__name__, + hex(id(self))[2:], self.name) + + def substitute(self, *args, **kw): + if args: + if kw: + raise TypeError( + "You can only give positional *or* keyword arguments") + if len(args) > 1: + raise TypeError( + "You can only give on positional argument") + kw = args[0] + ns = self.default_namespace.copy() + ns.update(self.namespace) + ns.update(kw) + result = self._interpret(ns) + return result + + def _interpret(self, ns): + __traceback_hide__ = True + parts = [] + self._interpret_codes(self._parsed, ns, out=parts) + return ''.join(parts) + + def _interpret_codes(self, codes, ns, out): + __traceback_hide__ = True + for item in codes: + if isinstance(item, six.string_types): + out.append(item) + else: + self._interpret_code(item, ns, out) + + def _interpret_code(self, code, ns, out): + __traceback_hide__ = True + name, pos = code[0], code[1] + if name == 'py': + self._exec(code[2], ns, pos) + elif name == 'continue': + raise _TemplateContinue() + elif name == 'break': + raise _TemplateBreak() + elif name == 'for': + vars, expr, content = code[2], code[3], code[4] + expr = self._eval(expr, ns, pos) + self._interpret_for(vars, expr, content, ns, out) + elif name == 'cond': + parts = code[2:] + self._interpret_if(parts, ns, out) + elif name == 'expr': + parts = code[2].split('|') + base = self._eval(parts[0], ns, pos) + for part in parts[1:]: + func = self._eval(part, ns, pos) + base = func(base) + out.append(self._repr(base, pos)) + elif name == 'default': + var, expr = code[2], code[3] + if var not in ns: + result = self._eval(expr, ns, pos) + ns[var] = result + elif name == 'comment': + return + else: + assert 0, "Unknown code: %r" % name + + def _interpret_for(self, vars, expr, content, ns, out): + __traceback_hide__ = True + for item in expr: + if len(vars) == 1: + ns[vars[0]] = item + else: + if len(vars) != len(item): + raise ValueError( + 'Need %i items to unpack (got %i items)' + % (len(vars), len(item))) + for name, value in zip(vars, item): + ns[name] = value + try: + self._interpret_codes(content, ns, out) + except _TemplateContinue: + continue + except _TemplateBreak: + break + + def _interpret_if(self, parts, ns, out): + __traceback_hide__ = True + # @@: if/else/else gets through + for part in parts: + assert not isinstance(part, six.string_types) + name, pos = part[0], part[1] + if name == 'else': + result = True + else: + result = self._eval(part[2], ns, pos) + if result: + self._interpret_codes(part[3], ns, out) + break + + def _eval(self, code, ns, pos): + __traceback_hide__ = True + try: + value = eval(code, ns) + return value + except: + exc_info = sys.exc_info() + e = exc_info[1] + if getattr(e, 'args'): + arg0 = e.args[0] + else: + arg0 = str(e) + e.args = (self._add_line_info(arg0, pos),) + six.reraise(exc_info[0], e, exc_info[2]) + + def _exec(self, code, ns, pos): + __traceback_hide__ = True + try: + six.exec_(code, ns) + except: + exc_info = sys.exc_info() + e = exc_info[1] + e.args = (self._add_line_info(e.args[0], pos),) + six.reraise(exc_info[0], e, exc_info[2]) + + def _repr(self, value, pos): + __traceback_hide__ = True + try: + if value is None: + return '' + if self._unicode: + try: + value = six.text_type(value) + except UnicodeDecodeError: + value = str(value) + else: + value = str(value) + except: + exc_info = sys.exc_info() + e = exc_info[1] + e.args = (self._add_line_info(e.args[0], pos),) + six.reraise(exc_info[0], e, exc_info[2]) + else: + if self._unicode and isinstance(value, six.binary_type): + if not self.decode_encoding: + raise UnicodeDecodeError( + 'Cannot decode str value %r into unicode ' + '(no default_encoding provided)' % value) + value = value.decode(self.default_encoding) + elif not self._unicode and isinstance(value, six.text_type): + if not self.decode_encoding: + raise UnicodeEncodeError( + 'Cannot encode unicode value %r into str ' + '(no default_encoding provided)' % value) + value = value.encode(self.default_encoding) + return value + + + def _add_line_info(self, msg, pos): + msg = "%s at line %s column %s" % ( + msg, pos[0], pos[1]) + if self.name: + msg += " in file %s" % self.name + return msg + +def sub(content, **kw): + name = kw.get('__name') + tmpl = Template(content, name=name) + return tmpl.substitute(kw) + +def paste_script_template_renderer(content, vars, filename=None): + tmpl = Template(content, name=filename) + return tmpl.substitute(vars) + +class bunch(dict): + + def __init__(self, **kw): + for name, value in kw.items(): + setattr(self, name, value) + + def __setattr__(self, name, value): + self[name] = value + + def __getattr__(self, name): + try: + return self[name] + except KeyError: + raise AttributeError(name) + + def __getitem__(self, key): + if 'default' in self: + try: + return dict.__getitem__(self, key) + except KeyError: + return dict.__getitem__(self, 'default') + else: + return dict.__getitem__(self, key) + + def __repr__(self): + items = [ + (k, v) for k, v in self.items()] + items.sort() + return '<%s %s>' % ( + self.__class__.__name__, + ' '.join(['%s=%r' % (k, v) for k, v in items])) + +############################################################ +## HTML Templating +############################################################ + +class html(object): + def __init__(self, value): + self.value = value + def __str__(self): + return self.value + def __repr__(self): + return '<%s %r>' % ( + self.__class__.__name__, self.value) + +def html_quote(value): + if value is None: + return '' + if not isinstance(value, six.string_types): + if six.PY2 and hasattr(value, '__unicode__'): + value = unicode(value) + else: + value = str(value) + value = cgi.escape(value, 1) + if six.PY2 and isinstance(value, unicode): + value = value.encode('ascii', 'xmlcharrefreplace') + return value + +def url(v): + if not isinstance(v, six.string_types): + if six.PY2 and hasattr(v, '__unicode__'): + v = unicode(v) + else: + v = str(v) + if six.PY2 and isinstance(v, unicode): + v = v.encode('utf8') + return quote(v) + +def attr(**kw): + kw = sorted(kw.items()) + parts = [] + for name, value in kw: + if value is None: + continue + if name.endswith('_'): + name = name[:-1] + parts.append('%s="%s"' % (html_quote(name), html_quote(value))) + return html(' '.join(parts)) + +class HTMLTemplate(Template): + + default_namespace = Template.default_namespace.copy() + default_namespace.update(dict( + html=html, + attr=attr, + url=url, + )) + + def _repr(self, value, pos): + plain = Template._repr(self, value, pos) + if isinstance(value, html): + return plain + else: + return html_quote(plain) + +def sub_html(content, **kw): + name = kw.get('__name') + tmpl = HTMLTemplate(content, name=name) + return tmpl.substitute(kw) + + +############################################################ +## Lexing and Parsing +############################################################ + +def lex(s, name=None, trim_whitespace=True): + """ + Lex a string into chunks: + + >>> lex('hey') + ['hey'] + >>> lex('hey {{you}}') + ['hey ', ('you', (1, 7))] + >>> lex('hey {{') + Traceback (most recent call last): + ... + TemplateError: No }} to finish last expression at line 1 column 7 + >>> lex('hey }}') + Traceback (most recent call last): + ... + TemplateError: }} outside expression at line 1 column 7 + >>> lex('hey {{ {{') + Traceback (most recent call last): + ... + TemplateError: {{ inside expression at line 1 column 10 + + """ + in_expr = False + chunks = [] + last = 0 + last_pos = (1, 1) + for match in token_re.finditer(s): + expr = match.group(0) + pos = find_position(s, match.end()) + if expr == '{{' and in_expr: + raise TemplateError('{{ inside expression', position=pos, + name=name) + elif expr == '}}' and not in_expr: + raise TemplateError('}} outside expression', position=pos, + name=name) + if expr == '{{': + part = s[last:match.start()] + if part: + chunks.append(part) + in_expr = True + else: + chunks.append((s[last:match.start()], last_pos)) + in_expr = False + last = match.end() + last_pos = pos + if in_expr: + raise TemplateError('No }} to finish last expression', + name=name, position=last_pos) + part = s[last:] + if part: + chunks.append(part) + if trim_whitespace: + chunks = trim_lex(chunks) + return chunks + +statement_re = re.compile(r'^(?:if |elif |else |for |py:)') +single_statements = ['endif', 'endfor', 'continue', 'break'] +trail_whitespace_re = re.compile(r'\n[\t ]*$') +lead_whitespace_re = re.compile(r'^[\t ]*\n') + +def trim_lex(tokens): + r""" + Takes a lexed set of tokens, and removes whitespace when there is + a directive on a line by itself: + + >>> tokens = lex('{{if x}}\nx\n{{endif}}\ny', trim_whitespace=False) + >>> tokens + [('if x', (1, 3)), '\nx\n', ('endif', (3, 3)), '\ny'] + >>> trim_lex(tokens) + [('if x', (1, 3)), 'x\n', ('endif', (3, 3)), 'y'] + """ + for i in range(len(tokens)): + current = tokens[i] + if isinstance(tokens[i], six.string_types): + # we don't trim this + continue + item = current[0] + if not statement_re.search(item) and item not in single_statements: + continue + if not i: + prev = '' + else: + prev = tokens[i-1] + if i+1 >= len(tokens): + next = '' + else: + next = tokens[i+1] + if (not isinstance(next, six.string_types) + or not isinstance(prev, six.string_types)): + continue + if ((not prev or trail_whitespace_re.search(prev)) + and (not next or lead_whitespace_re.search(next))): + if prev: + m = trail_whitespace_re.search(prev) + # +1 to leave the leading \n on: + prev = prev[:m.start()+1] + tokens[i-1] = prev + if next: + m = lead_whitespace_re.search(next) + next = next[m.end():] + tokens[i+1] = next + return tokens + + +def find_position(string, index): + """Given a string and index, return (line, column)""" + leading = string[:index].splitlines() + return (len(leading), len(leading[-1])+1) + +def parse(s, name=None): + r""" + Parses a string into a kind of AST + + >>> parse('{{x}}') + [('expr', (1, 3), 'x')] + >>> parse('foo') + ['foo'] + >>> parse('{{if x}}test{{endif}}') + [('cond', (1, 3), ('if', (1, 3), 'x', ['test']))] + >>> parse('series->{{for x in y}}x={{x}}{{endfor}}') + ['series->', ('for', (1, 11), ('x',), 'y', ['x=', ('expr', (1, 27), 'x')])] + >>> parse('{{for x, y in z:}}{{continue}}{{endfor}}') + [('for', (1, 3), ('x', 'y'), 'z', [('continue', (1, 21))])] + >>> parse('{{py:x=1}}') + [('py', (1, 3), 'x=1')] + >>> parse('{{if x}}a{{elif y}}b{{else}}c{{endif}}') + [('cond', (1, 3), ('if', (1, 3), 'x', ['a']), ('elif', (1, 12), 'y', ['b']), ('else', (1, 23), None, ['c']))] + + Some exceptions:: + + >>> parse('{{continue}}') + Traceback (most recent call last): + ... + TemplateError: continue outside of for loop at line 1 column 3 + >>> parse('{{if x}}foo') + Traceback (most recent call last): + ... + TemplateError: No {{endif}} at line 1 column 3 + >>> parse('{{else}}') + Traceback (most recent call last): + ... + TemplateError: else outside of an if block at line 1 column 3 + >>> parse('{{if x}}{{for x in y}}{{endif}}{{endfor}}') + Traceback (most recent call last): + ... + TemplateError: Unexpected endif at line 1 column 25 + >>> parse('{{if}}{{endif}}') + Traceback (most recent call last): + ... + TemplateError: if with no expression at line 1 column 3 + >>> parse('{{for x y}}{{endfor}}') + Traceback (most recent call last): + ... + TemplateError: Bad for (no "in") in 'x y' at line 1 column 3 + >>> parse('{{py:x=1\ny=2}}') + Traceback (most recent call last): + ... + TemplateError: Multi-line py blocks must start with a newline at line 1 column 3 + """ + tokens = lex(s, name=name) + result = [] + while tokens: + next, tokens = parse_expr(tokens, name) + result.append(next) + return result + +def parse_expr(tokens, name, context=()): + if isinstance(tokens[0], six.string_types): + return tokens[0], tokens[1:] + expr, pos = tokens[0] + expr = expr.strip() + if expr.startswith('py:'): + expr = expr[3:].lstrip(' \t') + if expr.startswith('\n'): + expr = expr[1:] + else: + if '\n' in expr: + raise TemplateError( + 'Multi-line py blocks must start with a newline', + position=pos, name=name) + return ('py', pos, expr), tokens[1:] + elif expr in ('continue', 'break'): + if 'for' not in context: + raise TemplateError( + 'continue outside of for loop', + position=pos, name=name) + return (expr, pos), tokens[1:] + elif expr.startswith('if '): + return parse_cond(tokens, name, context) + elif (expr.startswith('elif ') + or expr == 'else'): + raise TemplateError( + '%s outside of an if block' % expr.split()[0], + position=pos, name=name) + elif expr in ('if', 'elif', 'for'): + raise TemplateError( + '%s with no expression' % expr, + position=pos, name=name) + elif expr in ('endif', 'endfor'): + raise TemplateError( + 'Unexpected %s' % expr, + position=pos, name=name) + elif expr.startswith('for '): + return parse_for(tokens, name, context) + elif expr.startswith('default '): + return parse_default(tokens, name, context) + elif expr.startswith('#'): + return ('comment', pos, tokens[0][0]), tokens[1:] + return ('expr', pos, tokens[0][0]), tokens[1:] + +def parse_cond(tokens, name, context): + start = tokens[0][1] + pieces = [] + context = context + ('if',) + while 1: + if not tokens: + raise TemplateError( + 'Missing {{endif}}', + position=start, name=name) + if (isinstance(tokens[0], tuple) + and tokens[0][0] == 'endif'): + return ('cond', start) + tuple(pieces), tokens[1:] + next, tokens = parse_one_cond(tokens, name, context) + pieces.append(next) + +def parse_one_cond(tokens, name, context): + (first, pos), tokens = tokens[0], tokens[1:] + content = [] + if first.endswith(':'): + first = first[:-1] + if first.startswith('if '): + part = ('if', pos, first[3:].lstrip(), content) + elif first.startswith('elif '): + part = ('elif', pos, first[5:].lstrip(), content) + elif first == 'else': + part = ('else', pos, None, content) + else: + assert 0, "Unexpected token %r at %s" % (first, pos) + while 1: + if not tokens: + raise TemplateError( + 'No {{endif}}', + position=pos, name=name) + if (isinstance(tokens[0], tuple) + and (tokens[0][0] == 'endif' + or tokens[0][0].startswith('elif ') + or tokens[0][0] == 'else')): + return part, tokens + next, tokens = parse_expr(tokens, name, context) + content.append(next) + +def parse_for(tokens, name, context): + first, pos = tokens[0] + tokens = tokens[1:] + context = ('for',) + context + content = [] + assert first.startswith('for ') + if first.endswith(':'): + first = first[:-1] + first = first[3:].strip() + match = in_re.search(first) + if not match: + raise TemplateError( + 'Bad for (no "in") in %r' % first, + position=pos, name=name) + vars = first[:match.start()] + if '(' in vars: + raise TemplateError( + 'You cannot have () in the variable section of a for loop (%r)' + % vars, position=pos, name=name) + vars = tuple([ + v.strip() for v in first[:match.start()].split(',') + if v.strip()]) + expr = first[match.end():] + while 1: + if not tokens: + raise TemplateError( + 'No {{endfor}}', + position=pos, name=name) + if (isinstance(tokens[0], tuple) + and tokens[0][0] == 'endfor'): + return ('for', pos, vars, expr, content), tokens[1:] + next, tokens = parse_expr(tokens, name, context) + content.append(next) + +def parse_default(tokens, name, context): + first, pos = tokens[0] + assert first.startswith('default ') + first = first.split(None, 1)[1] + parts = first.split('=', 1) + if len(parts) == 1: + raise TemplateError( + "Expression must be {{default var=value}}; no = found in %r" % first, + position=pos, name=name) + var = parts[0].strip() + if ',' in var: + raise TemplateError( + "{{default x, y = ...}} is not supported", + position=pos, name=name) + if not var_re.search(var): + raise TemplateError( + "Not a valid variable name for {{default}}: %r" + % var, position=pos, name=name) + expr = parts[1].strip() + return ('default', pos, var, expr), tokens[1:] + +_fill_command_usage = """\ +%prog [OPTIONS] TEMPLATE arg=value + +Use py:arg=value to set a Python value; otherwise all values are +strings. +""" + +def fill_command(args=None): + import sys, optparse, pkg_resources, os + if args is None: + args = sys.argv[1:] + dist = pkg_resources.get_distribution('Paste') + parser = optparse.OptionParser( + version=str(dist), + usage=_fill_command_usage) + parser.add_option( + '-o', '--output', + dest='output', + metavar="FILENAME", + help="File to write output to (default stdout)") + parser.add_option( + '--html', + dest='use_html', + action='store_true', + help="Use HTML style filling (including automatic HTML quoting)") + parser.add_option( + '--env', + dest='use_env', + action='store_true', + help="Put the environment in as top-level variables") + options, args = parser.parse_args(args) + if len(args) < 1: + print('You must give a template filename') + print(dir(parser)) + assert 0 + template_name = args[0] + args = args[1:] + vars = {} + if options.use_env: + vars.update(os.environ) + for value in args: + if '=' not in value: + print('Bad argument: %r' % value) + sys.exit(2) + name, value = value.split('=', 1) + if name.startswith('py:'): + name = name[:3] + value = eval(value) + vars[name] = value + if template_name == '-': + template_content = sys.stdin.read() + template_name = '<stdin>' + else: + f = open(template_name, 'rb') + template_content = f.read() + f.close() + if options.use_html: + TemplateClass = HTMLTemplate + else: + TemplateClass = Template + template = TemplateClass(template_content, name=template_name) + result = template.substitute(vars) + if options.output: + f = open(options.output, 'wb') + f.write(result) + f.close() + else: + sys.stdout.write(result) + +if __name__ == '__main__': + from paste.util.template import fill_command + fill_command() + + diff --git a/paste/util/threadedprint.py b/paste/util/threadedprint.py new file mode 100644 index 0000000..820311e --- /dev/null +++ b/paste/util/threadedprint.py @@ -0,0 +1,250 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +""" +threadedprint.py +================ + +:author: Ian Bicking +:date: 12 Jul 2004 + +Multi-threaded printing; allows the output produced via print to be +separated according to the thread. + +To use this, you must install the catcher, like:: + + threadedprint.install() + +The installation optionally takes one of three parameters: + +default + The default destination for print statements (e.g., ``sys.stdout``). +factory + A function that will produce the stream for a thread, given the + thread's name. +paramwriter + Instead of writing to a file-like stream, this function will be + called like ``paramwriter(thread_name, text)`` for every write. + +The thread name is the value returned by +``threading.currentThread().getName()``, a string (typically something +like Thread-N). + +You can also submit file-like objects for specific threads, which will +override any of these parameters. To do this, call ``register(stream, +[threadName])``. ``threadName`` is optional, and if not provided the +stream will be registered for the current thread. + +If no specific stream is registered for a thread, and no default has +been provided, then an error will occur when anything is written to +``sys.stdout`` (or printed). + +Note: the stream's ``write`` method will be called in the thread the +text came from, so you should consider thread safety, especially if +multiple threads share the same writer. + +Note: if you want access to the original standard out, use +``sys.__stdout__``. + +You may also uninstall this, via:: + + threadedprint.uninstall() + +TODO +---- + +* Something with ``sys.stderr``. +* Some default handlers. Maybe something that hooks into `logging`. +* Possibly cache the results of ``factory`` calls. This would be a + semantic change. + +""" + +import threading +import sys +from paste.util import filemixin + +class PrintCatcher(filemixin.FileMixin): + + def __init__(self, default=None, factory=None, paramwriter=None, + leave_stdout=False): + assert len(filter(lambda x: x is not None, + [default, factory, paramwriter])) <= 1, ( + "You can only provide one of default, factory, or paramwriter") + if leave_stdout: + assert not default, ( + "You cannot pass in both default (%r) and " + "leave_stdout=True" % default) + default = sys.stdout + if default: + self._defaultfunc = self._writedefault + elif factory: + self._defaultfunc = self._writefactory + elif paramwriter: + self._defaultfunc = self._writeparam + else: + self._defaultfunc = self._writeerror + self._default = default + self._factory = factory + self._paramwriter = paramwriter + self._catchers = {} + + def write(self, v, currentThread=threading.currentThread): + name = currentThread().getName() + catchers = self._catchers + if not catchers.has_key(name): + self._defaultfunc(name, v) + else: + catcher = catchers[name] + catcher.write(v) + + def seek(self, *args): + # Weird, but Google App Engine is seeking on stdout + name = threading.currentThread().getName() + catchers = self._catchers + if not name in catchers: + self._default.seek(*args) + else: + catchers[name].seek(*args) + + def read(self, *args): + name = threading.currentThread().getName() + catchers = self._catchers + if not name in catchers: + self._default.read(*args) + else: + catchers[name].read(*args) + + + def _writedefault(self, name, v): + self._default.write(v) + + def _writefactory(self, name, v): + self._factory(name).write(v) + + def _writeparam(self, name, v): + self._paramwriter(name, v) + + def _writeerror(self, name, v): + assert False, ( + "There is no PrintCatcher output stream for the thread %r" + % name) + + def register(self, catcher, name=None, + currentThread=threading.currentThread): + if name is None: + name = currentThread().getName() + self._catchers[name] = catcher + + def deregister(self, name=None, + currentThread=threading.currentThread): + if name is None: + name = currentThread().getName() + assert self._catchers.has_key(name), ( + "There is no PrintCatcher catcher for the thread %r" % name) + del self._catchers[name] + +_printcatcher = None +_oldstdout = None + +def install(**kw): + global _printcatcher, _oldstdout, register, deregister + if (not _printcatcher or sys.stdout is not _printcatcher): + _oldstdout = sys.stdout + _printcatcher = sys.stdout = PrintCatcher(**kw) + register = _printcatcher.register + deregister = _printcatcher.deregister + +def uninstall(): + global _printcatcher, _oldstdout, register, deregister + if _printcatcher: + sys.stdout = _oldstdout + _printcatcher = _oldstdout = None + register = not_installed_error + deregister = not_installed_error + +def not_installed_error(*args, **kw): + assert False, ( + "threadedprint has not yet been installed (call " + "threadedprint.install())") + +register = deregister = not_installed_error + +class StdinCatcher(filemixin.FileMixin): + + def __init__(self, default=None, factory=None, paramwriter=None): + assert len(filter(lambda x: x is not None, + [default, factory, paramwriter])) <= 1, ( + "You can only provide one of default, factory, or paramwriter") + if default: + self._defaultfunc = self._readdefault + elif factory: + self._defaultfunc = self._readfactory + elif paramwriter: + self._defaultfunc = self._readparam + else: + self._defaultfunc = self._readerror + self._default = default + self._factory = factory + self._paramwriter = paramwriter + self._catchers = {} + + def read(self, size=None, currentThread=threading.currentThread): + name = currentThread().getName() + catchers = self._catchers + if not catchers.has_key(name): + return self._defaultfunc(name, size) + else: + catcher = catchers[name] + return catcher.read(size) + + def _readdefault(self, name, size): + self._default.read(size) + + def _readfactory(self, name, size): + self._factory(name).read(size) + + def _readparam(self, name, size): + self._paramreader(name, size) + + def _readerror(self, name, size): + assert False, ( + "There is no StdinCatcher output stream for the thread %r" + % name) + + def register(self, catcher, name=None, + currentThread=threading.currentThread): + if name is None: + name = currentThread().getName() + self._catchers[name] = catcher + + def deregister(self, catcher, name=None, + currentThread=threading.currentThread): + if name is None: + name = currentThread().getName() + assert self._catchers.has_key(name), ( + "There is no StdinCatcher catcher for the thread %r" % name) + del self._catchers[name] + +_stdincatcher = None +_oldstdin = None + +def install_stdin(**kw): + global _stdincatcher, _oldstdin, register_stdin, deregister_stdin + if not _stdincatcher: + _oldstdin = sys.stdin + _stdincatcher = sys.stdin = StdinCatcher(**kw) + register_stdin = _stdincatcher.register + deregister_stdin = _stdincatcher.deregister + +def uninstall_stdin(): + global _stdincatcher, _oldstdin, register_stdin, deregister_stdin + if _stdincatcher: + sys.stdin = _oldstdin + _stdincatcher = _oldstdin = None + register_stdin = deregister_stdin = not_installed_error_stdin + +def not_installed_error_stdin(*args, **kw): + assert False, ( + "threadedprint has not yet been installed for stdin (call " + "threadedprint.install_stdin())") diff --git a/paste/util/threadinglocal.py b/paste/util/threadinglocal.py new file mode 100644 index 0000000..06f2643 --- /dev/null +++ b/paste/util/threadinglocal.py @@ -0,0 +1,43 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php +""" +Implementation of thread-local storage, for Python versions that don't +have thread local storage natively. +""" + +try: + import threading +except ImportError: + # No threads, so "thread local" means process-global + class local(object): + pass +else: + try: + local = threading.local + except AttributeError: + # Added in 2.4, but now we'll have to define it ourselves + import thread + class local(object): + + def __init__(self): + self.__dict__['__objs'] = {} + + def __getattr__(self, attr, g=thread.get_ident): + try: + return self.__dict__['__objs'][g()][attr] + except KeyError: + raise AttributeError( + "No variable %s defined for the thread %s" + % (attr, g())) + + def __setattr__(self, attr, value, g=thread.get_ident): + self.__dict__['__objs'].setdefault(g(), {})[attr] = value + + def __delattr__(self, attr, g=thread.get_ident): + try: + del self.__dict__['__objs'][g()][attr] + except KeyError: + raise AttributeError( + "No variable %s defined for thread %s" + % (attr, g())) + |