aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG9
-rw-r--r--libpathod/app.py23
-rw-r--r--libpathod/cmdline.py (renamed from libpathod/main.py)344
-rw-r--r--libpathod/contrib/pyparsing.py755
-rw-r--r--libpathod/language.py225
-rw-r--r--libpathod/pathoc.py170
-rw-r--r--libpathod/pathod.py146
-rw-r--r--libpathod/templates/docs_lang.html8
-rw-r--r--libpathod/test.py15
-rw-r--r--libpathod/utils.py28
-rwxr-xr-xpathoc5
-rwxr-xr-xpathod5
-rw-r--r--setup.py12
-rw-r--r--test/test_app.py3
-rw-r--r--test/test_language.py255
-rw-r--r--test/test_pathoc.py45
-rw-r--r--test/test_pathod.py35
-rw-r--r--test/test_test.py20
-rw-r--r--test/tutils.py21
19 files changed, 1261 insertions, 863 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 55b11342..2de445b4 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,12 @@
+7 November 2014: pathod 0.11:
+
+ * Hugely improved SSL support, including dynamic generation of certificates
+ using the mitproxy cacert
+ * pathoc -S dumps information on the remote SSL certificate chain
+ * Big improvements to fuzzing, including random spec selection and memoization to avoid repeating randomly generated patterns
+ * Reflected patterns, allowing you to embed a pathod server response specification in a pathoc request, resolving both on client side. This makes fuzzing proxies and other intermediate systems much better.
+
+
25 August 2013: pathod 0.9.2:
* Adapt to interface changes in netlib
diff --git a/libpathod/app.py b/libpathod/app.py
index fb1d6a2d..1910e80e 100644
--- a/libpathod/app.py
+++ b/libpathod/app.py
@@ -4,6 +4,8 @@ import version, language, utils
from netlib import http_uastrings
logging.basicConfig(level="DEBUG")
+
+
def make_app(noapi):
app = Flask(__name__)
@@ -14,20 +16,17 @@ def make_app(noapi):
version = version.IVERSION
)
-
@app.route('/api/log')
def api_log():
return jsonify(
log = app.config["pathod"].get_log()
)
-
@app.route('/api/clear_log')
def api_clear_log():
app.config["pathod"].clear_log()
return "OK"
-
def render(s, cacheable, **kwargs):
kwargs["noapi"] = app.config["pathod"].noapi
kwargs["nocraft"] = app.config["pathod"].nocraft
@@ -37,30 +36,25 @@ def make_app(noapi):
resp.headers["Cache-control"] = "public, max-age=4320"
return resp
-
@app.route('/')
@app.route('/index.html')
def index():
return render("index.html", True, section="main")
-
@app.route('/download')
@app.route('/download.html')
def download():
return render("download.html", True, section="download", version=version.VERSION)
-
@app.route('/about')
@app.route('/about.html')
def about():
return render("about.html", True, section="about")
-
@app.route('/docs/pathod')
def docs_pathod():
return render("docs_pathod.html", True, section="docs", subsection="pathod")
-
@app.route('/docs/language')
def docs_language():
return render(
@@ -69,29 +63,24 @@ def make_app(noapi):
subsection="lang"
)
-
@app.route('/docs/pathoc')
def docs_pathoc():
return render("docs_pathoc.html", True, section="docs", subsection="pathoc")
-
@app.route('/docs/libpathod')
def docs_libpathod():
return render("docs_libpathod.html", True, section="docs", subsection="libpathod")
-
@app.route('/docs/test')
def docs_test():
return render("docs_test.html", True, section="docs", subsection="test")
-
@app.route('/log')
def log():
if app.config["pathod"].noapi:
abort(404)
return render("log.html", False, section="log", log=app.config["pathod"].get_log())
-
@app.route('/log/<int:lid>')
def onelog(lid):
item = app.config["pathod"].log_by_id(int(lid))
@@ -100,7 +89,6 @@ def make_app(noapi):
l = pprint.pformat(item)
return render("onelog.html", False, section="log", alog=l, lid=lid)
-
def _preview(is_request):
if is_request:
template = "request_preview.html"
@@ -121,9 +109,9 @@ def make_app(noapi):
try:
if is_request:
- r = language.parse_request(app.config["pathod"].request_settings, spec)
+ r = language.parse_requests(spec)[0]
else:
- r = language.parse_response(app.config["pathod"].request_settings, spec)
+ r = language.parse_response(spec)
except language.ParseException, v:
args["syntaxerror"] = str(v)
args["marked"] = v.marked()
@@ -144,14 +132,11 @@ def make_app(noapi):
args["output"] = utils.escape_unprintables(s.getvalue())
return render(template, False, **args)
-
@app.route('/response_preview')
def response_preview():
return _preview(False)
-
@app.route('/request_preview')
def request_preview():
return _preview(True)
return app
-
diff --git a/libpathod/main.py b/libpathod/cmdline.py
index 6f53832d..4a2390ed 100644
--- a/libpathod/main.py
+++ b/libpathod/cmdline.py
@@ -1,10 +1,14 @@
#!/usr/bin/env python
-import argparse, sys, logging, logging.handlers, os
-from . import pathoc as _pathoc, pathod as _pathod, utils, version, language
-from netlib import tcp, http_uastrings
+import argparse
+import os
+import os.path
+import sys
+import re
+from . import pathoc, pathod, version, utils, language
+from netlib import http_uastrings
-def pathoc():
+def go_pathoc():
preparser = argparse.ArgumentParser(add_help=False)
preparser.add_argument(
"--show-uas", dest="showua", action="store_true", default=False,
@@ -17,20 +21,41 @@ def pathoc():
print " ", i[1], i[0]
sys.exit(0)
- parser = argparse.ArgumentParser(description='A perverse HTTP client.', parents=[preparser])
- parser.add_argument('--version', action='version', version="pathoc " + version.VERSION)
+ parser = argparse.ArgumentParser(
+ description='A perverse HTTP client.', parents=[preparser]
+ )
+ parser.add_argument(
+ '--version',
+ action='version',
+ version="pathoc " + version.VERSION
+ )
parser.add_argument(
"-c", dest="connect_to", type=str, default=False,
metavar = "HOST:PORT",
help="Issue an HTTP CONNECT to connect to the specified host."
)
parser.add_argument(
+ "--memo-limit", dest='memolimit', default=5000, type=int, metavar="N",
+ help='Stop if we do not find a valid request after N attempts.'
+ )
+ parser.add_argument(
+ "-m", dest='memo', action="store_true", default=False,
+ help="""
+ Remember specs, and never play the same one twice. Note that this
+ means requests have to be rendered in memory, which means that large
+ generated data can cause issues.
+ """
+ )
+ parser.add_argument(
"-n", dest='repeat', default=1, type=int, metavar="N",
- help='Repeat requests N times'
+ help='Repeat N times. If 0 repeat for ever.'
)
parser.add_argument(
- "-p", dest="port", type=int, default=None,
- help="Port. Defaults to 80, or 443 if SSL is active"
+ "-r", dest="random", action="store_true", default=False,
+ help="""
+ Select a random request from those specified. If this is not specified,
+ requests are all played in sequence.
+ """
)
parser.add_argument(
"-t", dest="timeout", type=int, default=None,
@@ -38,13 +63,16 @@ def pathoc():
)
parser.add_argument(
'host', type=str,
- help='Host to connect to'
+ metavar = "host[:port]",
+ help='Host and port to connect to'
)
parser.add_argument(
- 'request', type=str, nargs="+",
- help='Request specification'
+ 'requests', type=str, nargs="+",
+ help="""
+ Request specification, or path to a file containing request
+ specifcations
+ """
)
-
group = parser.add_argument_group(
'SSL',
)
@@ -67,7 +95,10 @@ def pathoc():
group.add_argument(
"--sslversion", dest="sslversion", type=int, default=4,
choices=[1, 2, 3, 4],
- help="Use a specified protocol - TLSv1, SSLv2, SSLv3, SSLv23. Default to SSLv23."
+ help="""
+ Use a specified protocol - TLSv1, SSLv2, SSLv3, SSLv23. Default
+ to SSLv23.
+ """
)
group = parser.add_argument_group(
@@ -98,7 +129,7 @@ def pathoc():
help="Print full request"
)
group.add_argument(
- "-r", dest="showresp", action="store_true", default=False,
+ "-p", dest="showresp", action="store_true", default=False,
help="Print full response"
)
group.add_argument(
@@ -112,13 +143,21 @@ def pathoc():
args = parser.parse_args()
+ args.port = None
+ if ":" in args.host:
+ h, p = args.host.rsplit(":", 1)
+ try:
+ p = int(p)
+ except ValueError:
+ parser.error("Invalid port in host spec: %s" % args.host)
+ args.host = h
+ args.port = p
+
if args.port is None:
- port = 443 if args.ssl else 80
- else:
- port = args.port
+ args.port = 443 if args.ssl else 80
try:
- codes = [int(i) for i in args.ignorecodes.split(",") if i]
+ args.ignorecodes = [int(i) for i in args.ignorecodes.split(",") if i]
except ValueError:
parser.error("Invalid return code specification: %s"%args.ignorecodes)
@@ -130,164 +169,59 @@ def pathoc():
parts[1] = int(parts[1])
except ValueError:
parser.error("Invalid CONNECT specification: %s"%args.connect_to)
- connect_to = parts
+ args.connect_to = parts
else:
- connect_to = None
-
- try:
- for i in range(args.repeat):
- p = _pathoc.Pathoc(
- (args.host, port),
- ssl=args.ssl,
- sni=args.sni,
- sslversion=args.sslversion,
- clientcert=args.clientcert,
- ciphers=args.ciphers
- )
- try:
- p.connect(connect_to)
- except (tcp.NetLibError, _pathoc.PathocError), v:
- print >> sys.stderr, str(v)
- sys.exit(1)
- if args.timeout:
- p.settimeout(args.timeout)
- for spec in args.request:
- ret = p.print_request(
- spec,
- showreq=args.showreq,
- showresp=args.showresp,
- explain=args.explain,
- showssl=args.showssl,
- hexdump=args.hexdump,
- ignorecodes=codes,
- ignoretimeout=args.ignoretimeout
- )
- sys.stdout.flush()
- if ret and args.oneshot:
- sys.exit(0)
- except KeyboardInterrupt:
- pass
-
-
-def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
- try:
- pid = os.fork()
- if pid > 0:
- sys.exit(0)
- except OSError, e:
- sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror))
- sys.exit(1)
- os.chdir("/")
- os.umask(0)
- os.setsid()
- try:
- pid = os.fork()
- if pid > 0:
- sys.exit(0)
- except OSError, e:
- sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror))
- sys.exit(1)
- si = open(stdin, 'rb')
- so = open(stdout, 'a+b')
- se = open(stderr, 'a+b', 0)
- os.dup2(si.fileno(), sys.stdin.fileno())
- os.dup2(so.fileno(), sys.stdout.fileno())
- os.dup2(se.fileno(), sys.stderr.fileno())
-
+ args.connect_to = None
-def pathod_main(parser, args):
- certs = []
- for i in args.ssl_certs:
- parts = i.split("=", 1)
- if len(parts) == 1:
- parts = ["*", parts[0]]
- parts[1] = os.path.expanduser(parts[1])
- if not os.path.exists(parts[1]):
- parser.error("Certificate file does not exist: %s"%parts[1])
- certs.append(parts)
-
- ssloptions = _pathod.SSLOptions(
- cn = args.cn,
- confdir = args.confdir,
- not_after_connect = args.ssl_not_after_connect,
- ciphers = args.ciphers,
- sslversion = utils.SSLVERSIONS[args.sslversion],
- certs = certs
- )
-
- alst = []
- for i in args.anchors:
- parts = utils.parse_anchor_spec(i)
- if not parts:
- parser.error("Invalid anchor specification: %s"%i)
- alst.append(parts)
-
- root = logging.getLogger()
- if root.handlers:
- for handler in root.handlers:
- root.removeHandler(handler)
-
- log = logging.getLogger('pathod')
- log.setLevel(logging.DEBUG)
- fmt = logging.Formatter(
- '%(asctime)s: %(message)s',
- datefmt='%d-%m-%y %H:%M:%S',
- )
- if args.logfile:
- fh = logging.handlers.WatchedFileHandler(args.logfile)
- fh.setFormatter(fmt)
- log.addHandler(fh)
- if not args.daemonize:
- sh = logging.StreamHandler()
- sh.setFormatter(fmt)
- log.addHandler(sh)
-
- sizelimit = None
- if args.sizelimit:
+ reqs = []
+ for r in args.requests:
+ if os.path.exists(r):
+ data = open(r).read()
+ r = data
try:
- sizelimit = utils.parse_size(args.sizelimit)
- except ValueError, v:
- parser.error(v)
+ reqs.extend(language.parse_requests(r))
+ except language.ParseException, v:
+ print >> sys.stderr, "Error parsing request spec: %s"%v.msg
+ print >> sys.stderr, v.marked()
+ sys.exit(1)
+ args.requests = reqs
+ pathoc.main(args)
- try:
- pd = _pathod.Pathod(
- (args.address, args.port),
- craftanchor = args.craftanchor,
- ssl = args.ssl,
- ssloptions = ssloptions,
- staticdir = args.staticdir,
- anchors = alst,
- sizelimit = sizelimit,
- noweb = args.noweb,
- nocraft = args.nocraft,
- noapi = args.noapi,
- nohang = args.nohang,
- timeout = args.timeout,
- logreq = args.logreq,
- logresp = args.logresp,
- hexdump = args.hexdump,
- explain = args.explain,
- )
- except _pathod.PathodError, v:
- parser.error(str(v))
- except language.FileAccessDenied, v:
- parser.error("%s You probably want to a -d argument."%str(v))
- try:
- print "%s listening on %s:%s"%(version.NAMEVERSION, pd.address.host, pd.address.port)
- pd.serve_forever()
- except KeyboardInterrupt:
- pass
-
-
-def pathod():
- parser = argparse.ArgumentParser(description='A pathological HTTP/S daemon.')
- parser.add_argument('--version', action='version', version="pathod " + version.VERSION)
- parser.add_argument("-p", dest='port', default=9999, type=int, help='Port. Specify 0 to pick an arbitrary empty port.')
- parser.add_argument("-l", dest='address', default="127.0.0.1", type=str, help='Listening address.')
+def go_pathod():
+ parser = argparse.ArgumentParser(
+ description='A pathological HTTP/S daemon.'
+ )
+ parser.add_argument(
+ '--version',
+ action='version',
+ version="pathod " + version.VERSION
+ )
parser.add_argument(
- "-a", dest='anchors', default=[], type=str, action="append", metavar="ANCHOR",
- help='Add an anchor. Specified as a string with the form pattern=pagespec'
+ "-p",
+ dest='port',
+ default=9999,
+ type=int,
+ help='Port. Specify 0 to pick an arbitrary empty port.'
+ )
+ parser.add_argument(
+ "-l",
+ dest='address',
+ default="127.0.0.1",
+ type=str,
+ help='Listening address.'
+ )
+ parser.add_argument(
+ "-a",
+ dest='anchors',
+ default=[],
+ type=str,
+ action="append",
+ metavar="ANCHOR",
+ help="""
+ Add an anchor. Specified as a string with the form pattern=pagespec, or
+ pattern=filepath
+ """
)
parser.add_argument(
"-c", dest='craftanchor', default="/p/", type=str,
@@ -340,7 +274,7 @@ def pathod():
)
group.add_argument(
"--cn", dest="cn", type=str, default=None,
- help="CN for generated SSL certs. Default: %s"%_pathod.DEFAULT_CERT_DOMAIN
+ help="CN for generated SSL certs. Default: %s"%pathod.DEFAULT_CERT_DOMAIN
)
group.add_argument(
"-C", dest='ssl_not_after_connect', default=False, action="store_true",
@@ -349,10 +283,13 @@ def pathod():
group.add_argument(
"--cert", dest='ssl_certs', default=[], type=str,
metavar = "SPEC", action="append",
- help='Add an SSL certificate. SPEC is of the form "[domain=]path". '\
- 'The domain may include a wildcard, and is equal to "*" if not specified. '\
- 'The file at path is a certificate in PEM format. If a private key is included in the PEM, '\
- 'it is used, else the default key in the conf dir is used. Can be passed multiple times.'
+ help = """
+ Add an SSL certificate. SPEC is of the form "[domain=]path". The domain
+ may include a wildcard, and is equal to "*" if not specified. The file
+ at path is a certificate in PEM format. If a private key is included in
+ the PEM, it is used, else the default key in the conf dir is used. Can
+ be passed multiple times.'
+ """
)
group.add_argument(
"--ciphers", dest="ciphers", type=str, default=False,
@@ -361,7 +298,8 @@ def pathod():
group.add_argument(
"--sslversion", dest="sslversion", type=int, default=4,
choices=[1, 2, 3, 4],
- help="Use a specified protocol - TLSv1, SSLv2, SSLv3, SSLv23. Default to SSLv23."
+ help=""""Use a specified protocol - TLSv1, SSLv2, SSLv3, SSLv23. Default
+ to SSLv23."""
)
group = parser.add_argument_group(
@@ -392,10 +330,52 @@ def pathod():
help="Log request/response in hexdump format"
)
args = parser.parse_args()
- if args.daemonize:
- daemonize()
- pathod_main(parser, args)
+ certs = []
+ for i in args.ssl_certs:
+ parts = i.split("=", 1)
+ if len(parts) == 1:
+ parts = ["*", parts[0]]
+ parts[1] = os.path.expanduser(parts[1])
+ if not os.path.exists(parts[1]):
+ parser.error("Certificate file does not exist: %s"%parts[1])
+ certs.append(parts)
+ args.ssl_certs = certs
+
+ alst = []
+ for i in args.anchors:
+ parts = utils.parse_anchor_spec(i)
+ if not parts:
+ parser.error("Invalid anchor specification: %s"%i)
+ alst.append(parts)
+ args.anchors = alst
+
+ sizelimit = None
+ if args.sizelimit:
+ try:
+ sizelimit = utils.parse_size(args.sizelimit)
+ except ValueError, v:
+ parser.error(v)
+ args.sizelimit = sizelimit
+
+ anchors = []
+ for patt, spec in args.anchors:
+ if os.path.exists(spec):
+ data = open(spec).read()
+ spec = data
+
+ try:
+ req = language.parse_response(spec)
+ except language.ParseException, v:
+ print >> sys.stderr, "Error parsing anchor spec: %s"%v.msg
+ print >> sys.stderr, v.marked()
+ sys.exit(1)
+ try:
+ arex = re.compile(patt)
+ except re.error:
+ print >> sys.stderr, "Invalid regex in anchor: %s" % patt
+ sys.exit(1)
+ anchors.append((arex, req))
+ args.anchors = anchors
-if __name__ == "__main__":
- pathoc() \ No newline at end of file
+ pathod.main(args)
diff --git a/libpathod/contrib/pyparsing.py b/libpathod/contrib/pyparsing.py
index 9be97dc2..7dfe1043 100644
--- a/libpathod/contrib/pyparsing.py
+++ b/libpathod/contrib/pyparsing.py
@@ -1,6 +1,6 @@
# module pyparsing.py
#
-# Copyright (c) 2003-2011 Paul T. McGuire
+# Copyright (c) 2003-2013 Paul T. McGuire
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -21,7 +21,6 @@
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
-#from __future__ import generators
__doc__ = \
"""
@@ -40,7 +39,7 @@ Here is a program to parse "Hello, World!" (or any greeting of the form C{"<salu
greet = Word( alphas ) + "," + Word( alphas ) + "!"
hello = "Hello, World!"
- print hello, "->", greet.parseString( hello )
+ print (hello, "->", greet.parseString( hello ))
The program outputs the following::
@@ -58,8 +57,8 @@ The pyparsing module handles some of the problems that are typically vexing when
- embedded comments
"""
-__version__ = "1.5.6"
-__versionTime__ = "26 June 2011 10:53"
+__version__ = "2.0.2"
+__versionTime__ = "13 April 2014 12:10"
__author__ = "Paul McGuire <ptmcg@users.sourceforge.net>"
import string
@@ -69,6 +68,8 @@ import sys
import warnings
import re
import sre_constants
+import collections
+import pprint
#~ sys.stderr.write( "testing pyparsing module, version %s, %s\n" % (__version__,__versionTime__ ) )
__all__ = [
@@ -81,32 +82,29 @@ __all__ = [
'White', 'Word', 'WordEnd', 'WordStart', 'ZeroOrMore',
'alphanums', 'alphas', 'alphas8bit', 'anyCloseTag', 'anyOpenTag', 'cStyleComment', 'col',
'commaSeparatedList', 'commonHTMLEntity', 'countedArray', 'cppStyleComment', 'dblQuotedString',
-'dblSlashComment', 'delimitedList', 'dictOf', 'downcaseTokens', 'empty', 'getTokensEndLoc', 'hexnums',
+'dblSlashComment', 'delimitedList', 'dictOf', 'downcaseTokens', 'empty', 'hexnums',
'htmlComment', 'javaStyleComment', 'keepOriginalText', 'line', 'lineEnd', 'lineStart', 'lineno',
'makeHTMLTags', 'makeXMLTags', 'matchOnlyAtCol', 'matchPreviousExpr', 'matchPreviousLiteral',
'nestedExpr', 'nullDebugAction', 'nums', 'oneOf', 'opAssoc', 'operatorPrecedence', 'printables',
'punc8bit', 'pythonStyleComment', 'quotedString', 'removeQuotes', 'replaceHTMLEntity',
'replaceWith', 'restOfLine', 'sglQuotedString', 'srange', 'stringEnd',
'stringStart', 'traceParseAction', 'unicodeString', 'upcaseTokens', 'withAttribute',
-'indentedBlock', 'originalTextFor',
+'indentedBlock', 'originalTextFor', 'ungroup', 'infixNotation','locatedExpr',
]
-"""
-Detect if we are running version 3.X and make appropriate changes
-Robert A. Clark
-"""
-_PY3K = sys.version_info[0] > 2
-if _PY3K:
+PY_3 = sys.version.startswith('3')
+if PY_3:
_MAX_INT = sys.maxsize
basestring = str
unichr = chr
_ustr = str
- alphas = string.ascii_lowercase + string.ascii_uppercase
+
+ # build list of single arg builtins, that can be used as parse actions
+ singleArgBuiltins = [sum, len, sorted, reversed, list, tuple, set, any, all, min, max]
+
else:
_MAX_INT = sys.maxint
range = xrange
- set = lambda s : dict( [(c,0) for c in s] )
- alphas = string.lowercase + string.uppercase
def _ustr(obj):
"""Drop-in replacement for str(obj) that tries to be Unicode friendly. It first tries
@@ -134,24 +132,24 @@ else:
# Replace unprintables with question marks?
#return unicode(obj).encode(sys.getdefaultencoding(), 'replace')
# ...
-
- alphas = string.lowercase + string.uppercase
-
-# build list of single arg builtins, tolerant of Python version, that can be used as parse actions
-singleArgBuiltins = []
-import __builtin__
-for fname in "sum len enumerate sorted reversed list tuple set any all".split():
- try:
- singleArgBuiltins.append(getattr(__builtin__,fname))
- except AttributeError:
- continue
+ # build list of single arg builtins, tolerant of Python version, that can be used as parse actions
+ singleArgBuiltins = []
+ import __builtin__
+ for fname in "sum len sorted reversed list tuple set any all min max".split():
+ try:
+ singleArgBuiltins.append(getattr(__builtin__,fname))
+ except AttributeError:
+ continue
+
+_generatorType = type((y for y in range(1)))
+
def _xml_escape(data):
"""Escape &, <, >, ", ', etc. in a string of data."""
# ampersand must be replaced first
from_symbols = '&><"\''
- to_symbols = ['&'+s+';' for s in "amp gt lt quot apos".split()]
+ to_symbols = ('&'+s+';' for s in "amp gt lt quot apos".split())
for from_,to_ in zip(from_symbols, to_symbols):
data = data.replace(from_, to_)
return data
@@ -159,11 +157,12 @@ def _xml_escape(data):
class _Constants(object):
pass
-nums = string.digits
+alphas = string.ascii_lowercase + string.ascii_uppercase
+nums = "0123456789"
hexnums = nums + "ABCDEFabcdef"
alphanums = alphas + nums
_bslash = chr(92)
-printables = "".join( [ c for c in string.printable if c not in string.whitespace ] )
+printables = "".join(c for c in string.printable if c not in string.whitespace)
class ParseBaseException(Exception):
"""base exception class for all parsing runtime exceptions"""
@@ -206,12 +205,12 @@ class ParseBaseException(Exception):
line_str = self.line
line_column = self.column - 1
if markerString:
- line_str = "".join( [line_str[:line_column],
- markerString, line_str[line_column:]])
+ line_str = "".join((line_str[:line_column],
+ markerString, line_str[line_column:]))
return line_str.strip()
def __dir__(self):
return "loc msg pstr parserElement lineno col line " \
- "markInputLine __str__ __repr__".split()
+ "markInputline __str__ __repr__".split()
class ParseException(ParseBaseException):
"""exception thrown when parse expressions don't match class;
@@ -228,8 +227,8 @@ class ParseFatalException(ParseBaseException):
pass
class ParseSyntaxException(ParseFatalException):
- """just like C{ParseFatalException}, but thrown internally when an
- C{ErrorStop} ('-' operator) indicates that parsing is to stop immediately because
+ """just like C{L{ParseFatalException}}, but thrown internally when an
+ C{L{ErrorStop<And._ErrorStop>}} ('-' operator) indicates that parsing is to stop immediately because
an unbacktrackable syntax error has been found"""
def __init__(self, pe):
super(ParseSyntaxException, self).__init__(
@@ -272,7 +271,6 @@ class ParseResults(object):
- by list index (C{results[0], results[1]}, etc.)
- by attribute (C{results.<resultsName>})
"""
- #~ __slots__ = ( "__toklist", "__tokdict", "__doinit", "__name", "__parent", "__accumNames", "__weakref__" )
def __new__(cls, toklist, name=None, asList=True, modal=True ):
if isinstance(toklist, cls):
return toklist
@@ -290,6 +288,8 @@ class ParseResults(object):
self.__accumNames = {}
if isinstance(toklist, list):
self.__toklist = toklist[:]
+ elif isinstance(toklist, _generatorType):
+ self.__toklist = list(toklist)
else:
self.__toklist = [toklist]
self.__tokdict = dict()
@@ -367,16 +367,64 @@ class ParseResults(object):
__nonzero__ = __bool__
def __iter__( self ): return iter( self.__toklist )
def __reversed__( self ): return iter( self.__toklist[::-1] )
- def keys( self ):
+ def iterkeys( self ):
"""Returns all named result keys."""
- return self.__tokdict.keys()
+ if hasattr(self.__tokdict, "iterkeys"):
+ return self.__tokdict.iterkeys()
+ else:
+ return iter(self.__tokdict)
- def pop( self, index=-1 ):
+ def itervalues( self ):
+ """Returns all named result values."""
+ return (self[k] for k in self.iterkeys())
+
+ def iteritems( self ):
+ return ((k, self[k]) for k in self.iterkeys())
+
+ if PY_3:
+ keys = iterkeys
+ values = itervalues
+ items = iteritems
+ else:
+ def keys( self ):
+ """Returns all named result keys."""
+ return list(self.iterkeys())
+
+ def values( self ):
+ """Returns all named result values."""
+ return list(self.itervalues())
+
+ def items( self ):
+ """Returns all named result keys and values as a list of tuples."""
+ return list(self.iteritems())
+
+ def haskeys( self ):
+ """Since keys() returns an iterator, this method is helpful in bypassing
+ code that looks for the existence of any defined results names."""
+ return bool(self.__tokdict)
+
+ def pop( self, *args, **kwargs):
"""Removes and returns item at specified index (default=last).
- Will work with either numeric indices or dict-key indicies."""
- ret = self[index]
- del self[index]
- return ret
+ Supports both list and dict semantics for pop(). If passed no
+ argument or an integer argument, it will use list semantics
+ and pop tokens from the list of parsed tokens. If passed a
+ non-integer argument (most likely a string), it will use dict
+ semantics and pop the corresponding value from any defined
+ results names. A second default return value argument is
+ supported, just as in dict.pop()."""
+ if not args:
+ args = [-1]
+ if 'default' in kwargs:
+ args.append(kwargs['default'])
+ if (isinstance(args[0], int) or
+ len(args) == 1 or
+ args[0] in self):
+ ret = self[index]
+ del self[index]
+ return ret
+ else:
+ defaultvalue = args[1]
+ return defaultvalue
def get(self, key, defaultValue=None):
"""Returns named result matching the given key, or if there is no
@@ -396,24 +444,35 @@ class ParseResults(object):
for k, (value, position) in enumerate(occurrences):
occurrences[k] = _ParseResultsWithOffset(value, position + (position > index))
- def items( self ):
- """Returns all named result keys and values as a list of tuples."""
- return [(k,self[k]) for k in self.__tokdict]
+ def append( self, item ):
+ """Add single element to end of ParseResults list of elements."""
+ self.__toklist.append(item)
- def values( self ):
- """Returns all named result values."""
- return [ v[-1][0] for v in self.__tokdict.values() ]
+ def extend( self, itemseq ):
+ """Add sequence of elements to end of ParseResults list of elements."""
+ if isinstance(itemseq, ParseResults):
+ self += itemseq
+ else:
+ self.__toklist.extend(itemseq)
+
+ def clear( self ):
+ """Clear all elements and results names."""
+ del self.__toklist[:]
+ self.__tokdict.clear()
def __getattr__( self, name ):
- if True: #name not in self.__slots__:
- if name in self.__tokdict:
- if name not in self.__accumNames:
- return self.__tokdict[name][-1][0]
- else:
- return ParseResults([ v[0] for v in self.__tokdict[name] ])
+ try:
+ return self[name]
+ except KeyError:
+ return ""
+
+ if name in self.__tokdict:
+ if name not in self.__accumNames:
+ return self.__tokdict[name][-1][0]
else:
- return ""
- return None
+ return ParseResults([ v[0] for v in self.__tokdict[name] ])
+ else:
+ return ""
def __add__( self, other ):
ret = self.copy()
@@ -444,16 +503,13 @@ class ParseResults(object):
return "(%s, %s)" % ( repr( self.__toklist ), repr( self.__tokdict ) )
def __str__( self ):
- out = "["
- sep = ""
+ out = []
for i in self.__toklist:
if isinstance(i, ParseResults):
- out += sep + _ustr(i)
+ out.append(_ustr(i))
else:
- out += sep + repr(i)
- sep = ", "
- out += "]"
- return out
+ out.append(repr(i))
+ return '[' + ', '.join(out) + ']'
def _asStringList( self, sep='' ):
out = []
@@ -478,7 +534,10 @@ class ParseResults(object):
def asDict( self ):
"""Returns the named parse results as dictionary."""
- return dict( self.items() )
+ if PY_3:
+ return dict( self.items() )
+ else:
+ return dict( self.iteritems() )
def copy( self ):
"""Returns a new copy of a C{ParseResults} object."""
@@ -493,8 +552,8 @@ class ParseResults(object):
"""Returns the parse results as XML. Tags are created for tokens and lists that have defined results names."""
nl = "\n"
out = []
- namedItems = dict( [ (v[1],k) for (k,vlist) in self.__tokdict.items()
- for v in vlist ] )
+ namedItems = dict((v[1],k) for (k,vlist) in self.__tokdict.items()
+ for v in vlist)
nextLevelIndent = indent + " "
# collapse out indents if formatting is not desired
@@ -579,14 +638,13 @@ class ParseResults(object):
in a nested display of other data."""
out = []
out.append( indent+_ustr(self.asList()) )
- keys = self.items()
- keys.sort()
- for k,v in keys:
+ items = sorted(self.items())
+ for k,v in items:
if out:
out.append('\n')
out.append( "%s%s- %s: " % (indent,(' '*depth), k) )
if isinstance(v,ParseResults):
- if v.keys():
+ if v.haskeys():
out.append( v.dump(indent,depth+1) )
else:
out.append(_ustr(v))
@@ -594,6 +652,12 @@ class ParseResults(object):
out.append(_ustr(v))
return "".join(out)
+ def pprint(self, *args, **kwargs):
+ """Pretty-printer for parsed results as a list, using the C{pprint} module.
+ Accepts additional positional or keyword args as defined for the
+ C{pprint.pprint} method. (U{http://docs.python.org/3/library/pprint.html#pprint.pprint})"""
+ pprint.pprint(self.asList(), *args, **kwargs)
+
# add support for pickle protocol
def __getstate__(self):
return ( self.__toklist,
@@ -616,7 +680,9 @@ class ParseResults(object):
self.__parent = None
def __dir__(self):
- return dir(super(ParseResults,self)) + self.keys()
+ return dir(super(ParseResults,self)) + list(self.keys())
+
+collections.MutableMapping.register(ParseResults)
def col (loc,strg):
"""Returns current column within a string, counting newlines as line separators.
@@ -624,7 +690,7 @@ def col (loc,strg):
Note: the default parsing behavior is to expand tabs in the input string
before starting the parsing process. See L{I{ParserElement.parseString}<ParserElement.parseString>} for more information
- on parsing strings containing <TAB>s, and suggested methods to maintain a
+ on parsing strings containing C{<TAB>}s, and suggested methods to maintain a
consistent view of the parsed string, the parse location, and line and column
positions within the parsed string.
"""
@@ -636,7 +702,7 @@ def lineno(loc,strg):
Note: the default parsing behavior is to expand tabs in the input string
before starting the parsing process. See L{I{ParserElement.parseString}<ParserElement.parseString>} for more information
- on parsing strings containing <TAB>s, and suggested methods to maintain a
+ on parsing strings containing C{<TAB>}s, and suggested methods to maintain a
consistent view of the parsed string, the parse location, and line and column
positions within the parsed string.
"""
@@ -653,7 +719,7 @@ def line( loc, strg ):
return strg[lastCR+1:]
def _defaultStartDebugAction( instring, loc, expr ):
- print ("Match " + _ustr(expr) + " at loc " + _ustr(loc) + "(%d,%d)" % ( lineno(loc,instring), col(loc,instring) ))
+ print (("Match " + _ustr(expr) + " at loc " + _ustr(loc) + "(%d,%d)" % ( lineno(loc,instring), col(loc,instring) )))
def _defaultSuccessDebugAction( instring, startloc, endloc, expr, toks ):
print ("Matched " + _ustr(expr) + " -> " + str(toks.asList()))
@@ -665,35 +731,47 @@ def nullDebugAction(*args):
"""'Do-nothing' debug action, to suppress debugging output during parsing."""
pass
+# Only works on Python 3.x - nonlocal is toxic to Python 2 installs
+#~ 'decorator to trim function calls to match the arity of the target'
+#~ def _trim_arity(func, maxargs=3):
+ #~ if func in singleArgBuiltins:
+ #~ return lambda s,l,t: func(t)
+ #~ limit = 0
+ #~ foundArity = False
+ #~ def wrapper(*args):
+ #~ nonlocal limit,foundArity
+ #~ while 1:
+ #~ try:
+ #~ ret = func(*args[limit:])
+ #~ foundArity = True
+ #~ return ret
+ #~ except TypeError:
+ #~ if limit == maxargs or foundArity:
+ #~ raise
+ #~ limit += 1
+ #~ continue
+ #~ return wrapper
+
+# this version is Python 2.x-3.x cross-compatible
'decorator to trim function calls to match the arity of the target'
-if not _PY3K:
- def _trim_arity(func, maxargs=2):
- limit = [0]
- def wrapper(*args):
- while 1:
- try:
- return func(*args[limit[0]:])
- except TypeError:
- if limit[0] <= maxargs:
- limit[0] += 1
- continue
- raise
- return wrapper
-else:
- def _trim_arity(func, maxargs=2):
- limit = maxargs
- def wrapper(*args):
- #~ nonlocal limit
- while 1:
- try:
- return func(*args[limit:])
- except TypeError:
- if limit:
- limit -= 1
- continue
- raise
- return wrapper
-
+def _trim_arity(func, maxargs=2):
+ if func in singleArgBuiltins:
+ return lambda s,l,t: func(t)
+ limit = [0]
+ foundArity = [False]
+ def wrapper(*args):
+ while 1:
+ try:
+ ret = func(*args[limit[0]:])
+ foundArity[0] = True
+ return ret
+ except TypeError:
+ if limit[0] <= maxargs and not foundArity[0]:
+ limit[0] += 1
+ continue
+ raise
+ return wrapper
+
class ParserElement(object):
"""Abstract base level parser element class."""
DEFAULT_WHITE_CHARS = " \n\t\r"
@@ -705,6 +783,13 @@ class ParserElement(object):
ParserElement.DEFAULT_WHITE_CHARS = chars
setDefaultWhitespaceChars = staticmethod(setDefaultWhitespaceChars)
+ def inlineLiteralsUsing(cls):
+ """
+ Set class to be used for inclusion of string literals into a parser.
+ """
+ ParserElement.literalStringClass = cls
+ inlineLiteralsUsing = staticmethod(inlineLiteralsUsing)
+
def __init__( self, savelist=False ):
self.parseAction = list()
self.failAction = None
@@ -789,14 +874,14 @@ class ParserElement(object):
C{fn(loc,toks)}, C{fn(toks)}, or just C{fn()}, where:
- s = the original string being parsed (see note below)
- loc = the location of the matching substring
- - toks = a list of the matched tokens, packaged as a ParseResults object
+ - toks = a list of the matched tokens, packaged as a C{L{ParseResults}} object
If the functions in fns modify the tokens, they can return them as the return
value from fn, and the modified list of tokens will replace the original.
Otherwise, fn does not need to return any value.
Note: the default parsing behavior is to expand tabs in the input string
before starting the parsing process. See L{I{parseString}<parseString>} for more information
- on parsing strings containing <TAB>s, and suggested methods to maintain a
+ on parsing strings containing C{<TAB>}s, and suggested methods to maintain a
consistent view of the parsed string, the parse location, and line and column
positions within the parsed string.
"""
@@ -818,7 +903,7 @@ class ParserElement(object):
- loc = location where expression match was attempted and failed
- expr = the parse expression that failed
- err = the exception thrown
- The function returns no value. It may throw C{ParseFatalException}
+ The function returns no value. It may throw C{L{ParseFatalException}}
if it is desired to stop parsing immediately."""
self.failAction = fn
return self
@@ -872,15 +957,11 @@ class ParserElement(object):
loc,tokens = self.parseImpl( instring, preloc, doActions )
except IndexError:
raise ParseException( instring, len(instring), self.errmsg, self )
- except ParseBaseException:
+ except ParseBaseException as err:
#~ print ("Exception raised:", err)
- err = None
if self.debugActions[2]:
- err = sys.exc_info()[1]
self.debugActions[2]( instring, tokensStart, self, err )
if self.failAction:
- if err is None:
- err = sys.exc_info()[1]
self.failAction( instring, tokensStart, self, err )
raise
else:
@@ -910,10 +991,9 @@ class ParserElement(object):
self.resultsName,
asList=self.saveAsList and isinstance(tokens,(ParseResults,list)),
modal=self.modalResults )
- except ParseBaseException:
+ except ParseBaseException as err:
#~ print "Exception raised in user parse action:", err
if (self.debugActions[2] ):
- err = sys.exc_info()[1]
self.debugActions[2]( instring, tokensStart, self, err )
raise
else:
@@ -952,8 +1032,8 @@ class ParserElement(object):
value = self._parseNoCache( instring, loc, doActions, callPreParse )
ParserElement._exprArgCache[ lookup ] = (value[0],value[1].copy())
return value
- except ParseBaseException:
- pe = sys.exc_info()[1]
+ except ParseBaseException as pe:
+ pe.__traceback__ = None
ParserElement._exprArgCache[ lookup ] = pe
raise
@@ -994,7 +1074,7 @@ class ParserElement(object):
If you want the grammar to require that the entire input string be
successfully parsed, then set C{parseAll} to True (equivalent to ending
- the grammar with C{StringEnd()}).
+ the grammar with C{L{StringEnd()}}).
Note: C{parseString} implicitly calls C{expandtabs()} on the input string,
in order to report proper column numbers in parse actions.
@@ -1023,12 +1103,11 @@ class ParserElement(object):
loc = self.preParse( instring, loc )
se = Empty() + StringEnd()
se._parse( instring, loc )
- except ParseBaseException:
+ except ParseBaseException as exc:
if ParserElement.verbose_stacktrace:
raise
else:
# catch and re-raise exception from here, clears out pyparsing internal stack trace
- exc = sys.exc_info()[1]
raise exc
else:
return tokens
@@ -1076,16 +1155,15 @@ class ParserElement(object):
loc = nextLoc
else:
loc = preloc+1
- except ParseBaseException:
+ except ParseBaseException as exc:
if ParserElement.verbose_stacktrace:
raise
else:
# catch and re-raise exception from here, clears out pyparsing internal stack trace
- exc = sys.exc_info()[1]
raise exc
def transformString( self, instring ):
- """Extension to C{scanString}, to modify matching text with modified tokens that may
+ """Extension to C{L{scanString}}, to modify matching text with modified tokens that may
be returned from a parse action. To use C{transformString}, define a grammar and
attach a parse action to it that modifies the returned token list.
Invoking C{transformString()} on a target string will then scan for matches,
@@ -1110,33 +1188,31 @@ class ParserElement(object):
out.append(instring[lastE:])
out = [o for o in out if o]
return "".join(map(_ustr,_flatten(out)))
- except ParseBaseException:
+ except ParseBaseException as exc:
if ParserElement.verbose_stacktrace:
raise
else:
# catch and re-raise exception from here, clears out pyparsing internal stack trace
- exc = sys.exc_info()[1]
raise exc
def searchString( self, instring, maxMatches=_MAX_INT ):
- """Another extension to C{scanString}, simplifying the access to the tokens found
+ """Another extension to C{L{scanString}}, simplifying the access to the tokens found
to match the given parse expression. May be called with optional
C{maxMatches} argument, to clip searching after 'n' matches are found.
"""
try:
return ParseResults([ t for t,s,e in self.scanString( instring, maxMatches ) ])
- except ParseBaseException:
+ except ParseBaseException as exc:
if ParserElement.verbose_stacktrace:
raise
else:
# catch and re-raise exception from here, clears out pyparsing internal stack trace
- exc = sys.exc_info()[1]
raise exc
def __add__(self, other ):
- """Implementation of + operator - returns And"""
+ """Implementation of + operator - returns C{L{And}}"""
if isinstance( other, basestring ):
- other = Literal( other )
+ other = ParserElement.literalStringClass( other )
if not isinstance( other, ParserElement ):
warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
SyntaxWarning, stacklevel=2)
@@ -1144,9 +1220,9 @@ class ParserElement(object):
return And( [ self, other ] )
def __radd__(self, other ):
- """Implementation of + operator when left operand is not a C{ParserElement}"""
+ """Implementation of + operator when left operand is not a C{L{ParserElement}}"""
if isinstance( other, basestring ):
- other = Literal( other )
+ other = ParserElement.literalStringClass( other )
if not isinstance( other, ParserElement ):
warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
SyntaxWarning, stacklevel=2)
@@ -1154,9 +1230,9 @@ class ParserElement(object):
return other + self
def __sub__(self, other):
- """Implementation of - operator, returns C{And} with error stop"""
+ """Implementation of - operator, returns C{L{And}} with error stop"""
if isinstance( other, basestring ):
- other = Literal( other )
+ other = ParserElement.literalStringClass( other )
if not isinstance( other, ParserElement ):
warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
SyntaxWarning, stacklevel=2)
@@ -1164,9 +1240,9 @@ class ParserElement(object):
return And( [ self, And._ErrorStop(), other ] )
def __rsub__(self, other ):
- """Implementation of - operator when left operand is not a C{ParserElement}"""
+ """Implementation of - operator when left operand is not a C{L{ParserElement}}"""
if isinstance( other, basestring ):
- other = Literal( other )
+ other = ParserElement.literalStringClass( other )
if not isinstance( other, ParserElement ):
warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
SyntaxWarning, stacklevel=2)
@@ -1179,12 +1255,12 @@ class ParserElement(object):
tuple, similar to C{{min,max}} multipliers in regular expressions. Tuples
may also include C{None} as in:
- C{expr*(n,None)} or C{expr*(n,)} is equivalent
- to C{expr*n + ZeroOrMore(expr)}
+ to C{expr*n + L{ZeroOrMore}(expr)}
(read as "at least n instances of C{expr}")
- C{expr*(None,n)} is equivalent to C{expr*(0,n)}
(read as "0 to n instances of C{expr}")
- - C{expr*(None,None)} is equivalent to C{ZeroOrMore(expr)}
- - C{expr*(1,None)} is equivalent to C{OneOrMore(expr)}
+ - C{expr*(None,None)} is equivalent to C{L{ZeroOrMore}(expr)}
+ - C{expr*(1,None)} is equivalent to C{L{OneOrMore}(expr)}
Note that C{expr*(None,n)} does not raise an exception if
more than n exprs exist in the input stream; that is,
@@ -1245,9 +1321,9 @@ class ParserElement(object):
return self.__mul__(other)
def __or__(self, other ):
- """Implementation of | operator - returns C{MatchFirst}"""
+ """Implementation of | operator - returns C{L{MatchFirst}}"""
if isinstance( other, basestring ):
- other = Literal( other )
+ other = ParserElement.literalStringClass( other )
if not isinstance( other, ParserElement ):
warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
SyntaxWarning, stacklevel=2)
@@ -1255,9 +1331,9 @@ class ParserElement(object):
return MatchFirst( [ self, other ] )
def __ror__(self, other ):
- """Implementation of | operator when left operand is not a C{ParserElement}"""
+ """Implementation of | operator when left operand is not a C{L{ParserElement}}"""
if isinstance( other, basestring ):
- other = Literal( other )
+ other = ParserElement.literalStringClass( other )
if not isinstance( other, ParserElement ):
warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
SyntaxWarning, stacklevel=2)
@@ -1265,9 +1341,9 @@ class ParserElement(object):
return other | self
def __xor__(self, other ):
- """Implementation of ^ operator - returns C{Or}"""
+ """Implementation of ^ operator - returns C{L{Or}}"""
if isinstance( other, basestring ):
- other = Literal( other )
+ other = ParserElement.literalStringClass( other )
if not isinstance( other, ParserElement ):
warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
SyntaxWarning, stacklevel=2)
@@ -1275,9 +1351,9 @@ class ParserElement(object):
return Or( [ self, other ] )
def __rxor__(self, other ):
- """Implementation of ^ operator when left operand is not a C{ParserElement}"""
+ """Implementation of ^ operator when left operand is not a C{L{ParserElement}}"""
if isinstance( other, basestring ):
- other = Literal( other )
+ other = ParserElement.literalStringClass( other )
if not isinstance( other, ParserElement ):
warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
SyntaxWarning, stacklevel=2)
@@ -1285,9 +1361,9 @@ class ParserElement(object):
return other ^ self
def __and__(self, other ):
- """Implementation of & operator - returns C{Each}"""
+ """Implementation of & operator - returns C{L{Each}}"""
if isinstance( other, basestring ):
- other = Literal( other )
+ other = ParserElement.literalStringClass( other )
if not isinstance( other, ParserElement ):
warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
SyntaxWarning, stacklevel=2)
@@ -1295,9 +1371,9 @@ class ParserElement(object):
return Each( [ self, other ] )
def __rand__(self, other ):
- """Implementation of & operator when left operand is not a C{ParserElement}"""
+ """Implementation of & operator when left operand is not a C{L{ParserElement}}"""
if isinstance( other, basestring ):
- other = Literal( other )
+ other = ParserElement.literalStringClass( other )
if not isinstance( other, ParserElement ):
warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
SyntaxWarning, stacklevel=2)
@@ -1305,19 +1381,24 @@ class ParserElement(object):
return other & self
def __invert__( self ):
- """Implementation of ~ operator - returns C{NotAny}"""
+ """Implementation of ~ operator - returns C{L{NotAny}}"""
return NotAny( self )
- def __call__(self, name):
- """Shortcut for C{setResultsName}, with C{listAllMatches=default}::
+ def __call__(self, name=None):
+ """Shortcut for C{L{setResultsName}}, with C{listAllMatches=default}::
userdata = Word(alphas).setResultsName("name") + Word(nums+"-").setResultsName("socsecno")
could be written as::
userdata = Word(alphas)("name") + Word(nums+"-")("socsecno")
If C{name} is given with a trailing C{'*'} character, then C{listAllMatches} will be
passed as C{True}.
+
+ If C{name} is omitted, same as calling C{L{copy}}.
"""
- return self.setResultsName(name)
+ if name is not None:
+ return self.setResultsName(name)
+ else:
+ return self.copy()
def suppress( self ):
"""Suppresses the output of this C{ParserElement}; useful to keep punctuation from
@@ -1403,25 +1484,17 @@ class ParserElement(object):
try:
file_contents = file_or_filename.read()
except AttributeError:
- f = open(file_or_filename, "rb")
+ f = open(file_or_filename, "r")
file_contents = f.read()
f.close()
try:
return self.parseString(file_contents, parseAll)
- except ParseBaseException:
- # catch and re-raise exception from here, clears out pyparsing internal stack trace
- exc = sys.exc_info()[1]
- raise exc
-
- def getException(self):
- return ParseException("",0,self.errmsg,self)
-
- def __getattr__(self,aname):
- if aname == "myException":
- self.myException = ret = self.getException();
- return ret;
- else:
- raise AttributeError("no such attribute " + aname)
+ except ParseBaseException as exc:
+ if ParserElement.verbose_stacktrace:
+ raise
+ else:
+ # catch and re-raise exception from here, clears out pyparsing internal stack trace
+ raise exc
def __eq__(self,other):
if isinstance(other, ParserElement):
@@ -1478,10 +1551,7 @@ class NoMatch(Token):
self.errmsg = "Unmatchable token"
def parseImpl( self, instring, loc, doActions=True ):
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
class Literal(Token):
@@ -1509,16 +1579,13 @@ class Literal(Token):
if (instring[loc] == self.firstMatchChar and
(self.matchLen==1 or instring.startswith(self.match,loc)) ):
return loc+self.matchLen, self.match
- #~ raise ParseException( instring, loc, self.errmsg )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
_L = Literal
+ParserElement.literalStringClass = Literal
class Keyword(Token):
"""Token to exactly match a specified string as a keyword, that is, it must be
- immediately followed by a non-keyword character. Compare with C{Literal}::
+ immediately followed by a non-keyword character. Compare with C{L{Literal}}::
Literal("if") will match the leading C{'if'} in C{'ifAndOnlyIf'}.
Keyword("if") will not; it will only match the leading C{'if'} in C{'if x=1'}, or C{'if(y==2)'}
Accepts two optional constructor arguments in addition to the keyword string:
@@ -1559,11 +1626,7 @@ class Keyword(Token):
(loc >= len(instring)-self.matchLen or instring[loc+self.matchLen] not in self.identChars) and
(loc == 0 or instring[loc-1] not in self.identChars) ):
return loc+self.matchLen, self.match
- #~ raise ParseException( instring, loc, self.errmsg )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
def copy(self):
c = super(Keyword,self).copy()
@@ -1591,11 +1654,7 @@ class CaselessLiteral(Literal):
def parseImpl( self, instring, loc, doActions=True ):
if instring[ loc:loc+self.matchLen ].upper() == self.match:
return loc+self.matchLen, self.returnString
- #~ raise ParseException( instring, loc, self.errmsg )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
class CaselessKeyword(Keyword):
def __init__( self, matchString, identChars=Keyword.DEFAULT_KEYWORD_CHARS ):
@@ -1605,11 +1664,7 @@ class CaselessKeyword(Keyword):
if ( (instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and
(loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars) ):
return loc+self.matchLen, self.match
- #~ raise ParseException( instring, loc, self.errmsg )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
class Word(Token):
"""Token for matching words composed of allowed character sets.
@@ -1626,9 +1681,9 @@ class Word(Token):
def __init__( self, initChars, bodyChars=None, min=1, max=0, exact=0, asKeyword=False, excludeChars=None ):
super(Word,self).__init__()
if excludeChars:
- initChars = ''.join([c for c in initChars if c not in excludeChars])
+ initChars = ''.join(c for c in initChars if c not in excludeChars)
if bodyChars:
- bodyChars = ''.join([c for c in bodyChars if c not in excludeChars])
+ bodyChars = ''.join(c for c in bodyChars if c not in excludeChars)
self.initCharsOrig = initChars
self.initChars = set(initChars)
if bodyChars :
@@ -1681,20 +1736,14 @@ class Word(Token):
if self.re:
result = self.re.match(instring,loc)
if not result:
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
loc = result.end()
return loc, result.group()
if not(instring[ loc ] in self.initChars):
- #~ raise ParseException( instring, loc, self.errmsg )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
+
start = loc
loc += 1
instrlen = len(instring)
@@ -1714,11 +1763,7 @@ class Word(Token):
throwException = True
if throwException:
- #~ raise ParseException( instring, loc, self.errmsg )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
return loc, instring[start:loc]
@@ -1787,10 +1832,7 @@ class Regex(Token):
def parseImpl( self, instring, loc, doActions=True ):
result = self.re.match(instring,loc)
if not result:
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
loc = result.end()
d = result.groupdict()
@@ -1821,9 +1863,9 @@ class QuotedString(Token):
- quoteChar - string of one or more characters defining the quote delimiting string
- escChar - character to escape quotes, typically backslash (default=None)
- escQuote - special quote sequence to escape an embedded quote string (such as SQL's "" to escape an embedded ") (default=None)
- - multiline - boolean indicating whether quotes can span multiple lines (default=False)
- - unquoteResults - boolean indicating whether the matched text should be unquoted (default=True)
- - endQuoteChar - string of one or more characters defining the end of the quote delimited string (default=None => same as quoteChar)
+ - multiline - boolean indicating whether quotes can span multiple lines (default=C{False})
+ - unquoteResults - boolean indicating whether the matched text should be unquoted (default=C{True})
+ - endQuoteChar - string of one or more characters defining the end of the quote delimited string (default=C{None} => same as quoteChar)
"""
super(QuotedString,self).__init__()
@@ -1864,9 +1906,9 @@ class QuotedString(Token):
(escChar is not None and _escapeRegexRangeChars(escChar) or '') )
if len(self.endQuoteChar) > 1:
self.pattern += (
- '|(?:' + ')|(?:'.join(["%s[^%s]" % (re.escape(self.endQuoteChar[:i]),
+ '|(?:' + ')|(?:'.join("%s[^%s]" % (re.escape(self.endQuoteChar[:i]),
_escapeRegexRangeChars(self.endQuoteChar[i]))
- for i in range(len(self.endQuoteChar)-1,0,-1)]) + ')'
+ for i in range(len(self.endQuoteChar)-1,0,-1)) + ')'
)
if escQuote:
self.pattern += (r'|(?:%s)' % re.escape(escQuote))
@@ -1892,10 +1934,7 @@ class QuotedString(Token):
def parseImpl( self, instring, loc, doActions=True ):
result = instring[loc] == self.firstQuoteChar and self.re.match(instring,loc) or None
if not result:
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
loc = result.end()
ret = result.group()
@@ -1961,11 +2000,7 @@ class CharsNotIn(Token):
def parseImpl( self, instring, loc, doActions=True ):
if instring[loc] in self.notChars:
- #~ raise ParseException( instring, loc, self.errmsg )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
start = loc
loc += 1
@@ -1976,11 +2011,7 @@ class CharsNotIn(Token):
loc += 1
if loc - start < self.minLen:
- #~ raise ParseException( instring, loc, self.errmsg )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
return loc, instring[start:loc]
@@ -2003,7 +2034,7 @@ class White(Token):
by pyparsing grammars. This class is included when some whitespace structures
are significant. Define with a string containing the whitespace characters to be
matched; default is C{" \\t\\r\\n"}. Also takes optional C{min}, C{max}, and C{exact} arguments,
- as defined for the C{Word} class."""
+ as defined for the C{L{Word}} class."""
whiteStrs = {
" " : "<SPC>",
"\t": "<TAB>",
@@ -2014,9 +2045,9 @@ class White(Token):
def __init__(self, ws=" \t\r\n", min=1, max=0, exact=0):
super(White,self).__init__()
self.matchWhite = ws
- self.setWhitespaceChars( "".join([c for c in self.whiteChars if c not in self.matchWhite]) )
+ self.setWhitespaceChars( "".join(c for c in self.whiteChars if c not in self.matchWhite) )
#~ self.leaveWhitespace()
- self.name = ("".join([White.whiteStrs[c] for c in self.matchWhite]))
+ self.name = ("".join(White.whiteStrs[c] for c in self.matchWhite))
self.mayReturnEmpty = True
self.errmsg = "Expected " + self.name
@@ -2033,11 +2064,7 @@ class White(Token):
def parseImpl( self, instring, loc, doActions=True ):
if not(instring[ loc ] in self.matchWhite):
- #~ raise ParseException( instring, loc, self.errmsg )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
start = loc
loc += 1
maxloc = start + self.maxLen
@@ -2046,11 +2073,7 @@ class White(Token):
loc += 1
if loc - start < self.minLen:
- #~ raise ParseException( instring, loc, self.errmsg )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
return loc, instring[start:loc]
@@ -2102,11 +2125,7 @@ class LineStart(_PositionToken):
if not( loc==0 or
(loc == self.preParse( instring, 0 )) or
(instring[loc-1] == "\n") ): #col(loc, instring) != 1:
- #~ raise ParseException( instring, loc, "Expected start of line" )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
return loc, []
class LineEnd(_PositionToken):
@@ -2121,18 +2140,11 @@ class LineEnd(_PositionToken):
if instring[loc] == "\n":
return loc+1, "\n"
else:
- #~ raise ParseException( instring, loc, "Expected end of line" )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
elif loc == len(instring):
return loc+1, []
else:
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
class StringStart(_PositionToken):
"""Matches if current position is at the beginning of the parse string"""
@@ -2144,11 +2156,7 @@ class StringStart(_PositionToken):
if loc != 0:
# see if entire string up to here is just whitespace and ignoreables
if loc != self.preParse( instring, 0 ):
- #~ raise ParseException( instring, loc, "Expected start of text" )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
return loc, []
class StringEnd(_PositionToken):
@@ -2159,20 +2167,13 @@ class StringEnd(_PositionToken):
def parseImpl( self, instring, loc, doActions=True ):
if loc < len(instring):
- #~ raise ParseException( instring, loc, "Expected end of text" )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
elif loc == len(instring):
return loc+1, []
elif loc > len(instring):
return loc, []
else:
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
class WordStart(_PositionToken):
"""Matches if the current position is at the beginning of a Word, and
@@ -2190,10 +2191,7 @@ class WordStart(_PositionToken):
if loc != 0:
if (instring[loc-1] in self.wordChars or
instring[loc] not in self.wordChars):
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
return loc, []
class WordEnd(_PositionToken):
@@ -2214,11 +2212,7 @@ class WordEnd(_PositionToken):
if instrlen>0 and loc<instrlen:
if (instring[loc] in self.wordChars or
instring[loc-1] not in self.wordChars):
- #~ raise ParseException( instring, loc, "Expected end of word" )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
return loc, []
@@ -2226,10 +2220,16 @@ class ParseExpression(ParserElement):
"""Abstract subclass of ParserElement, for combining and post-processing parsed tokens."""
def __init__( self, exprs, savelist = False ):
super(ParseExpression,self).__init__(savelist)
- if isinstance( exprs, list ):
- self.exprs = exprs
- elif isinstance( exprs, basestring ):
+ if isinstance( exprs, _generatorType ):
+ exprs = list(exprs)
+
+ if isinstance( exprs, basestring ):
self.exprs = [ Literal( exprs ) ]
+ elif isinstance( exprs, collections.Sequence ):
+ # if sequence of strings provided, wrap with Literal
+ if all(isinstance(expr, basestring) for expr in exprs):
+ exprs = map(Literal, exprs)
+ self.exprs = list(exprs)
else:
try:
self.exprs = list( exprs )
@@ -2331,16 +2331,13 @@ class And(ParseExpression):
class _ErrorStop(Empty):
def __init__(self, *args, **kwargs):
- super(Empty,self).__init__(*args, **kwargs)
+ super(And._ErrorStop,self).__init__(*args, **kwargs)
+ self.name = '-'
self.leaveWhitespace()
def __init__( self, exprs, savelist = True ):
super(And,self).__init__(exprs, savelist)
- self.mayReturnEmpty = True
- for e in self.exprs:
- if not e.mayReturnEmpty:
- self.mayReturnEmpty = False
- break
+ self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
self.setWhitespaceChars( exprs[0].whiteChars )
self.skipWhitespace = exprs[0].skipWhitespace
self.callPreparse = True
@@ -2359,14 +2356,14 @@ class And(ParseExpression):
loc, exprtokens = e._parse( instring, loc, doActions )
except ParseSyntaxException:
raise
- except ParseBaseException:
- pe = sys.exc_info()[1]
+ except ParseBaseException as pe:
+ pe.__traceback__ = None
raise ParseSyntaxException(pe)
except IndexError:
raise ParseSyntaxException( ParseException(instring, len(instring), self.errmsg, self) )
else:
loc, exprtokens = e._parse( instring, loc, doActions )
- if exprtokens or exprtokens.keys():
+ if exprtokens or exprtokens.haskeys():
resultlist += exprtokens
return loc, resultlist
@@ -2387,7 +2384,7 @@ class And(ParseExpression):
return self.name
if self.strRepr is None:
- self.strRepr = "{" + " ".join( [ _ustr(e) for e in self.exprs ] ) + "}"
+ self.strRepr = "{" + " ".join(_ustr(e) for e in self.exprs) + "}"
return self.strRepr
@@ -2399,11 +2396,10 @@ class Or(ParseExpression):
"""
def __init__( self, exprs, savelist = False ):
super(Or,self).__init__(exprs, savelist)
- self.mayReturnEmpty = False
- for e in self.exprs:
- if e.mayReturnEmpty:
- self.mayReturnEmpty = True
- break
+ if self.exprs:
+ self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)
+ else:
+ self.mayReturnEmpty = True
def parseImpl( self, instring, loc, doActions=True ):
maxExcLoc = -1
@@ -2412,8 +2408,8 @@ class Or(ParseExpression):
for e in self.exprs:
try:
loc2 = e.tryParse( instring, loc )
- except ParseException:
- err = sys.exc_info()[1]
+ except ParseException as err:
+ err.__traceback__ = None
if err.loc > maxExcLoc:
maxException = err
maxExcLoc = err.loc
@@ -2436,7 +2432,7 @@ class Or(ParseExpression):
def __ixor__(self, other ):
if isinstance( other, basestring ):
- other = Literal( other )
+ other = ParserElement.literalStringClass( other )
return self.append( other ) #Or( [ self, other ] )
def __str__( self ):
@@ -2444,7 +2440,7 @@ class Or(ParseExpression):
return self.name
if self.strRepr is None:
- self.strRepr = "{" + " ^ ".join( [ _ustr(e) for e in self.exprs ] ) + "}"
+ self.strRepr = "{" + " ^ ".join(_ustr(e) for e in self.exprs) + "}"
return self.strRepr
@@ -2461,12 +2457,8 @@ class MatchFirst(ParseExpression):
"""
def __init__( self, exprs, savelist = False ):
super(MatchFirst,self).__init__(exprs, savelist)
- if exprs:
- self.mayReturnEmpty = False
- for e in self.exprs:
- if e.mayReturnEmpty:
- self.mayReturnEmpty = True
- break
+ if self.exprs:
+ self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)
else:
self.mayReturnEmpty = True
@@ -2477,7 +2469,7 @@ class MatchFirst(ParseExpression):
try:
ret = e._parse( instring, loc, doActions )
return ret
- except ParseException, err:
+ except ParseException as err:
if err.loc > maxExcLoc:
maxException = err
maxExcLoc = err.loc
@@ -2495,7 +2487,7 @@ class MatchFirst(ParseExpression):
def __ior__(self, other ):
if isinstance( other, basestring ):
- other = Literal( other )
+ other = ParserElement.literalStringClass( other )
return self.append( other ) #MatchFirst( [ self, other ] )
def __str__( self ):
@@ -2503,7 +2495,7 @@ class MatchFirst(ParseExpression):
return self.name
if self.strRepr is None:
- self.strRepr = "{" + " | ".join( [ _ustr(e) for e in self.exprs ] ) + "}"
+ self.strRepr = "{" + " | ".join(_ustr(e) for e in self.exprs) + "}"
return self.strRepr
@@ -2520,11 +2512,7 @@ class Each(ParseExpression):
"""
def __init__( self, exprs, savelist = True ):
super(Each,self).__init__(exprs, savelist)
- self.mayReturnEmpty = True
- for e in self.exprs:
- if not e.mayReturnEmpty:
- self.mayReturnEmpty = False
- break
+ self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
self.skipWhitespace = True
self.initExprGroups = True
@@ -2562,7 +2550,7 @@ class Each(ParseExpression):
keepMatching = False
if tmpReqd:
- missing = ", ".join( [ _ustr(e) for e in tmpReqd ] )
+ missing = ", ".join(_ustr(e) for e in tmpReqd)
raise ParseException(instring,loc,"Missing one or more required elements (%s)" % missing )
# add any unmatched Optionals, in case they have default values defined
@@ -2577,7 +2565,7 @@ class Each(ParseExpression):
for r in resultlist:
dups = {}
for k in r.keys():
- if k in finalResults.keys():
+ if k in finalResults:
tmp = ParseResults(finalResults[k])
tmp += ParseResults(r[k])
dups[k] = tmp
@@ -2591,7 +2579,7 @@ class Each(ParseExpression):
return self.name
if self.strRepr is None:
- self.strRepr = "{" + " & ".join( [ _ustr(e) for e in self.exprs ] ) + "}"
+ self.strRepr = "{" + " & ".join(_ustr(e) for e in self.exprs) + "}"
return self.strRepr
@@ -2706,11 +2694,7 @@ class NotAny(ParseElementEnhance):
except (ParseException,IndexError):
pass
else:
- #~ raise ParseException(instring, loc, self.errmsg )
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
return loc, []
def __str__( self ):
@@ -2740,7 +2724,7 @@ class ZeroOrMore(ParseElementEnhance):
else:
preloc = loc
loc, tmptokens = self.expr._parse( instring, preloc, doActions )
- if tmptokens or tmptokens.keys():
+ if tmptokens or tmptokens.haskeys():
tokens += tmptokens
except (ParseException,IndexError):
pass
@@ -2775,7 +2759,7 @@ class OneOrMore(ParseElementEnhance):
else:
preloc = loc
loc, tmptokens = self.expr._parse( instring, preloc, doActions )
- if tmptokens or tmptokens.keys():
+ if tmptokens or tmptokens.haskeys():
tokens += tmptokens
except (ParseException,IndexError):
pass
@@ -2809,8 +2793,8 @@ class Optional(ParseElementEnhance):
A default return string can also be specified, if the optional expression
is not found.
"""
- def __init__( self, exprs, default=_optionalNotMatched ):
- super(Optional,self).__init__( exprs, savelist=False )
+ def __init__( self, expr, default=_optionalNotMatched ):
+ super(Optional,self).__init__( expr, savelist=False )
self.defaultValue = default
self.mayReturnEmpty = True
@@ -2878,7 +2862,7 @@ class SkipTo(ParseElementEnhance):
while 1:
try:
loc = self.ignoreExpr.tryParse(instring,loc)
- # print "found ignoreExpr, advance to", loc
+ # print("found ignoreExpr, advance to", loc)
except ParseBaseException:
break
expr._parse( instring, loc, doActions=False, callPreParse=False )
@@ -2898,10 +2882,7 @@ class SkipTo(ParseElementEnhance):
raise
else:
loc += 1
- exc = self.myException
- exc.loc = loc
- exc.pstr = instring
- raise exc
+ raise ParseException(instring, loc, self.errmsg, self)
class Forward(ParseElementEnhance):
"""Forward declaration of an expression to be defined later -
@@ -2916,13 +2897,14 @@ class Forward(ParseElementEnhance):
thereby leaving b and c out as parseable alternatives. It is recommended that you
explicitly group the values inserted into the C{Forward}::
fwdExpr << (a | b | c)
+ Converting to use the '<<=' operator instead will avoid this problem.
"""
def __init__( self, other=None ):
super(Forward,self).__init__( other, savelist=False )
def __lshift__( self, other ):
if isinstance( other, basestring ):
- other = Literal(other)
+ other = ParserElement.literalStringClass(other)
self.expr = other
self.mayReturnEmpty = other.mayReturnEmpty
self.strRepr = None
@@ -2932,8 +2914,11 @@ class Forward(ParseElementEnhance):
self.skipWhitespace = self.expr.skipWhitespace
self.saveAsList = self.expr.saveAsList
self.ignoreExprs.extend(self.expr.ignoreExprs)
- return None
-
+ return self
+
+ def __ilshift__(self, other):
+ return self << other
+
def leaveWhitespace( self ):
self.skipWhitespace = False
return self
@@ -2972,7 +2957,7 @@ class Forward(ParseElementEnhance):
return super(Forward,self).copy()
else:
ret = Forward()
- ret << self
+ ret <<= self
return ret
class _ForwardNoRecurse(Forward):
@@ -2993,7 +2978,7 @@ class Upcase(TokenConverter):
DeprecationWarning,stacklevel=2)
def postParse( self, instring, loc, tokenlist ):
- return list(map( string.upper, tokenlist ))
+ return list(map( str.upper, tokenlist ))
class Combine(TokenConverter):
@@ -3023,13 +3008,13 @@ class Combine(TokenConverter):
del retToks[:]
retToks += ParseResults([ "".join(tokenlist._asStringList(self.joinString)) ], modal=self.modalResults)
- if self.resultsName and len(retToks.keys())>0:
+ if self.resultsName and retToks.haskeys():
return [ retToks ]
else:
return retToks
class Group(TokenConverter):
- """Converter to return the matched tokens as a list - useful for returning tokens of C{ZeroOrMore} and C{OneOrMore} expressions."""
+ """Converter to return the matched tokens as a list - useful for returning tokens of C{L{ZeroOrMore}} and C{L{OneOrMore}} expressions."""
def __init__( self, expr ):
super(Group,self).__init__( expr )
self.saveAsList = True
@@ -3042,8 +3027,8 @@ class Dict(TokenConverter):
Each element can also be referenced using the first token in the expression as its key.
Useful for tabular report scraping when the first column can be used as a item key.
"""
- def __init__( self, exprs ):
- super(Dict,self).__init__( exprs )
+ def __init__( self, expr ):
+ super(Dict,self).__init__( expr )
self.saveAsList = True
def postParse( self, instring, loc, tokenlist ):
@@ -3060,7 +3045,7 @@ class Dict(TokenConverter):
else:
dictvalue = tok.copy() #ParseResults(i)
del dictvalue[0]
- if len(dictvalue)!= 1 or (isinstance(dictvalue,ParseResults) and dictvalue.keys()):
+ if len(dictvalue)!= 1 or (isinstance(dictvalue,ParseResults) and dictvalue.haskeys()):
tokenlist[ikey] = _ParseResultsWithOffset(dictvalue,i)
else:
tokenlist[ikey] = _ParseResultsWithOffset(dictvalue[0],i)
@@ -3105,8 +3090,7 @@ def traceParseAction(f):
sys.stderr.write( ">>entering %s(line: '%s', %d, %s)\n" % (thisFunc,line(l,s),l,t) )
try:
ret = f(*paArgs)
- except Exception:
- exc = sys.exc_info()[1]
+ except Exception as exc:
sys.stderr.write( "<<leaving %s (exception: %s)\n" % (thisFunc,exc) )
raise
sys.stderr.write( "<<leaving %s (ret: %s)\n" % (thisFunc,ret) )
@@ -3124,7 +3108,7 @@ def delimitedList( expr, delim=",", combine=False ):
"""Helper to define a delimited list of expressions - the delimiter defaults to ','.
By default, the list elements and delimiters can have intervening whitespace, and
comments, but this can be overridden by passing C{combine=True} in the constructor.
- If C{combine} is set to True, the matching tokens are returned as a single token
+ If C{combine} is set to C{True}, the matching tokens are returned as a single token
string, with the delimiters included; otherwise, the matching tokens are returned
as a list of tokens, with the delimiters suppressed.
"""
@@ -3204,7 +3188,7 @@ def matchPreviousExpr(expr):
"""
rep = Forward()
e2 = expr.copy()
- rep << e2
+ rep <<= e2
def copyTokenToRepeater(s,l,t):
matchTokens = _flatten(t.asList())
def mustMatchTheseTokens(s,l,t):
@@ -3226,7 +3210,7 @@ def _escapeRegexRangeChars(s):
def oneOf( strs, caseless=False, useRegex=True ):
"""Helper to quickly define a set of alternative Literals, and makes sure to do
longest-first testing when there is a conflict, regardless of the input order,
- but returns a C{MatchFirst} for best performance.
+ but returns a C{L{MatchFirst}} for best performance.
Parameters:
- strs - a string of space-delimited literals, or a list of string literals
@@ -3244,10 +3228,12 @@ def oneOf( strs, caseless=False, useRegex=True ):
masks = ( lambda a,b: b.startswith(a) )
parseElementClass = Literal
- if isinstance(strs,(list,tuple)):
- symbols = list(strs[:])
- elif isinstance(strs,basestring):
+ if isinstance(strs,basestring):
symbols = strs.split()
+ elif isinstance(strs, collections.Sequence):
+ symbols = list(strs[:])
+ elif isinstance(strs, _generatorType):
+ symbols = list(strs)
else:
warnings.warn("Invalid argument to oneOf, expected string or list",
SyntaxWarning, stacklevel=2)
@@ -3271,9 +3257,9 @@ def oneOf( strs, caseless=False, useRegex=True ):
#~ print (strs,"->", "|".join( [ _escapeRegexChars(sym) for sym in symbols] ))
try:
if len(symbols)==len("".join(symbols)):
- return Regex( "[%s]" % "".join( [ _escapeRegexRangeChars(sym) for sym in symbols] ) )
+ return Regex( "[%s]" % "".join(_escapeRegexRangeChars(sym) for sym in symbols) )
else:
- return Regex( "|".join( [ re.escape(sym) for sym in symbols] ) )
+ return Regex( "|".join(re.escape(sym) for sym in symbols) )
except:
warnings.warn("Exception creating Regex for oneOf, building MatchFirst",
SyntaxWarning, stacklevel=2)
@@ -3284,7 +3270,7 @@ def oneOf( strs, caseless=False, useRegex=True ):
def dictOf( key, value ):
"""Helper to easily and clearly define a dictionary by specifying the respective patterns
- for the key and value. Takes care of defining the C{Dict}, C{ZeroOrMore}, and C{Group} tokens
+ for the key and value. Takes care of defining the C{L{Dict}}, C{L{ZeroOrMore}}, and C{L{Group}} tokens
in the proper order. The key pattern can include delimiting markers or punctuation,
as long as they are suppressed, thereby leaving the significant key text. The value
pattern can include named results, so that the C{Dict} results can include named token
@@ -3301,7 +3287,7 @@ def originalTextFor(expr, asString=True):
string containing the original parsed text.
If the optional C{asString} argument is passed as C{False}, then the return value is a
- C{ParseResults} containing any results names that were originally matched, and a
+ C{L{ParseResults}} containing any results names that were originally matched, and a
single token containing the original matched text from the input string. So if
the expression passed to C{L{originalTextFor}} contains expressions with defined
results names, you must set C{asString} to C{False} if you want to preserve those
@@ -3326,6 +3312,20 @@ def ungroup(expr):
if all but one are non-empty."""
return TokenConverter(expr).setParseAction(lambda t:t[0])
+def locatedExpr(expr):
+ """Helper to decorate a returned token with its starting and ending locations in the input string.
+ This helper adds the following results names:
+ - locn_start = location where matched expression begins
+ - locn_end = location where matched expression ends
+ - value = the actual parsed results
+
+ Be careful if the input text contains C{<TAB>} characters, you may want to call
+ C{L{ParserElement.parseWithTabs}}
+ """
+ locator = Empty().setParseAction(lambda s,l,t: l)
+ return Group(locator("locn_start") + expr("value") + locator.copy().leaveWhitespace()("locn_end"))
+
+
# convenience constants for positional expressions
empty = Empty().setName("empty")
lineStart = LineStart().setName("lineStart")
@@ -3334,15 +3334,12 @@ stringStart = StringStart().setName("stringStart")
stringEnd = StringEnd().setName("stringEnd")
_escapedPunc = Word( _bslash, r"\[]-*.$+^?()~ ", exact=2 ).setParseAction(lambda s,l,t:t[0][1])
-_printables_less_backslash = "".join([ c for c in printables if c not in r"\]" ])
-_escapedHexChar = Regex(r"\\0?[xX][0-9a-fA-F]+").setParseAction(lambda s,l,t:unichr(int(t[0][1:],16)))
+_escapedHexChar = Regex(r"\\0?[xX][0-9a-fA-F]+").setParseAction(lambda s,l,t:unichr(int(t[0].lstrip(r'\0x'),16)))
_escapedOctChar = Regex(r"\\0[0-7]+").setParseAction(lambda s,l,t:unichr(int(t[0][1:],8)))
-_singleChar = _escapedPunc | _escapedHexChar | _escapedOctChar | Word(_printables_less_backslash,exact=1)
+_singleChar = _escapedPunc | _escapedHexChar | _escapedOctChar | Word(printables, excludeChars=r'\]', exact=1)
_charRange = Group(_singleChar + Suppress("-") + _singleChar)
_reBracketExpr = Literal("[") + Optional("^").setResultsName("negate") + Group( OneOrMore( _charRange | _singleChar ) ).setResultsName("body") + "]"
-_expanded = lambda p: (isinstance(p,ParseResults) and ''.join([ unichr(c) for c in range(ord(p[0]),ord(p[1])+1) ]) or p)
-
def srange(s):
r"""Helper to easily define string ranges for use in Word construction. Borrows
syntax from regexp '[]' string range definitions::
@@ -3360,8 +3357,9 @@ def srange(s):
a range of any of the above, separated by a dash ('a-z', etc.)
any combination of the above ('aeiouy', 'a-zA-Z0-9_$', etc.)
"""
+ _expanded = lambda p: p if not isinstance(p,ParseResults) else ''.join(unichr(c) for c in range(ord(p[0]),ord(p[1])+1))
try:
- return "".join([_expanded(part) for part in _reBracketExpr.parseString(s).body])
+ return "".join(_expanded(part) for part in _reBracketExpr.parseString(s).body)
except:
return ""
@@ -3376,7 +3374,7 @@ def matchOnlyAtCol(n):
def replaceWith(replStr):
"""Helper method for common parse actions that simply return a literal value. Especially
- useful when used with C{transformString()}.
+ useful when used with C{L{transformString<ParserElement.transformString>}()}.
"""
def _replFunc(*args):
return [replStr]
@@ -3398,7 +3396,7 @@ def downcaseTokens(s,l,t):
return [ tt.lower() for tt in map(_ustr,t) ]
def keepOriginalText(s,startLoc,t):
- """DEPRECATED - use new helper method C{originalTextFor}.
+ """DEPRECATED - use new helper method C{L{originalTextFor}}.
Helper parse action to preserve original parsed text,
overriding any nested parse actions."""
try:
@@ -3440,7 +3438,7 @@ def _makeTags(tagStr, xml):
Dict(ZeroOrMore(Group( tagAttrName + Suppress("=") + tagAttrValue ))) + \
Optional("/",default=[False]).setResultsName("empty").setParseAction(lambda s,l,t:t[0]=='/') + Suppress(">")
else:
- printablesLessRAbrack = "".join( [ c for c in printables if c not in ">" ] )
+ printablesLessRAbrack = "".join(c for c in printables if c not in ">")
tagAttrValue = quotedString.copy().setParseAction( removeQuotes ) | Word(printablesLessRAbrack)
openTag = Suppress("<") + tagStr("tag") + \
Dict(ZeroOrMore(Group( tagAttrName.setParseAction(downcaseTokens) + \
@@ -3464,7 +3462,7 @@ def makeXMLTags(tagStr):
def withAttribute(*args,**attrDict):
"""Helper to create a validating parse action to be used with start tags created
- with C{makeXMLTags} or C{makeHTMLTags}. Use C{withAttribute} to qualify a starting tag
+ with C{L{makeXMLTags}} or C{L{makeHTMLTags}}. Use C{withAttribute} to qualify a starting tag
with a required attribute value, to avoid false matches on common tags such as
C{<TD>} or C{<DIV>}.
@@ -3499,7 +3497,7 @@ opAssoc = _Constants()
opAssoc.LEFT = object()
opAssoc.RIGHT = object()
-def operatorPrecedence( baseExpr, opList ):
+def infixNotation( baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')') ):
"""Helper method for constructing grammars of expressions made up of
operators working in a precedence hierarchy. Operators may be unary or
binary, left- or right-associative. Parse actions can also be attached
@@ -3518,13 +3516,15 @@ def operatorPrecedence( baseExpr, opList ):
be 1, 2, or 3)
- rightLeftAssoc is the indicator whether the operator is
right or left associative, using the pyparsing-defined
- constants opAssoc.RIGHT and opAssoc.LEFT.
+ constants C{opAssoc.RIGHT} and C{opAssoc.LEFT}.
- parseAction is the parse action to be associated with
expressions matching this operator expression (the
parse action tuple member may be omitted)
+ - lpar - expression for matching left-parentheses (default=Suppress('('))
+ - rpar - expression for matching right-parentheses (default=Suppress(')'))
"""
ret = Forward()
- lastExpr = baseExpr | ( Suppress('(') + ret + Suppress(')') )
+ lastExpr = baseExpr | ( lpar + ret + rpar )
for i,operDef in enumerate(opList):
opExpr,arity,rightLeftAssoc,pa = (operDef + (None,))[:4]
if arity == 3:
@@ -3565,10 +3565,11 @@ def operatorPrecedence( baseExpr, opList ):
raise ValueError("operator must indicate right or left associativity")
if pa:
matchExpr.setParseAction( pa )
- thisExpr << ( matchExpr | lastExpr )
+ thisExpr <<= ( matchExpr | lastExpr )
lastExpr = thisExpr
- ret << lastExpr
+ ret <<= lastExpr
return ret
+operatorPrecedence = infixNotation
dblQuotedString = Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\x[0-9a-fA-F]+)|(?:\\.))*"').setName("string enclosed in double quotes")
sglQuotedString = Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\x[0-9a-fA-F]+)|(?:\\.))*'").setName("string enclosed in single quotes")
@@ -3622,9 +3623,9 @@ def nestedExpr(opener="(", closer=")", content=None, ignoreExpr=quotedString.cop
raise ValueError("opening and closing arguments must be strings if no content expression is given")
ret = Forward()
if ignoreExpr is not None:
- ret << Group( Suppress(opener) + ZeroOrMore( ignoreExpr | ret | content ) + Suppress(closer) )
+ ret <<= Group( Suppress(opener) + ZeroOrMore( ignoreExpr | ret | content ) + Suppress(closer) )
else:
- ret << Group( Suppress(opener) + ZeroOrMore( ret | content ) + Suppress(closer) )
+ ret <<= Group( Suppress(opener) + ZeroOrMore( ret | content ) + Suppress(closer) )
return ret
def indentedBlock(blockStatementExpr, indentStack, indent=True):
@@ -3697,8 +3698,7 @@ cppStyleComment = Regex(r"/(?:\*(?:[^*]*\*+)+?/|/[^\n]*(?:\n[^\n]*)*?(?:(?<!\\)|
javaStyleComment = cppStyleComment
pythonStyleComment = Regex(r"#.*").setName("Python style comment")
-_noncomma = "".join( [ c for c in printables if c != "," ] )
-_commasepitem = Combine(OneOrMore(Word(_noncomma) +
+_commasepitem = Combine(OneOrMore(Word(printables, excludeChars=',') +
Optional( Word(" \t") +
~Literal(",") + ~LineEnd() ) ) ).streamline().setName("commaItem")
commaSeparatedList = delimitedList( Optional( quotedString.copy() | _commasepitem, default="") ).setName("commaSeparatedList")
@@ -3715,8 +3715,7 @@ if __name__ == "__main__":
print ("tokens.columns = " + str(tokens.columns))
print ("tokens.tables = " + str(tokens.tables))
print (tokens.asXML("SQL",True))
- except ParseBaseException:
- err = sys.exc_info()[1]
+ except ParseBaseException as err:
print (teststring + "->")
print (err.line)
print (" "*(err.column-1) + "^")
diff --git a/libpathod/language.py b/libpathod/language.py
index 286a1a8e..e1f6820f 100644
--- a/libpathod/language.py
+++ b/libpathod/language.py
@@ -1,6 +1,11 @@
-import operator, string, random, mmap, os, time, copy
+import operator
+import string
+import random
+import mmap
+import os
+import time
+import copy
import abc
-from email.utils import formatdate
import contrib.pyparsing as pp
from netlib import http_status, tcp, http_uastrings
@@ -9,7 +14,20 @@ import utils
BLOCKSIZE = 1024
TRUNCATE = 1024
-class FileAccessDenied(Exception): pass
+
+def escape_backslash(s):
+ return s.replace("\\", "\\\\")
+
+
+def quote(s):
+ quotechar = s[0]
+ s = s[1:-1]
+ s = s.replace(quotechar, "\\" + quotechar)
+ return quotechar + s + quotechar
+
+
+class FileAccessDenied(Exception):
+ pass
class ParseException(Exception):
@@ -20,7 +38,7 @@ class ParseException(Exception):
self.col = col
def marked(self):
- return "%s\n%s"%(self.s, " "*(self.col-1) + "^")
+ return "%s\n%s"%(self.s, " "*(self.col - 1) + "^")
def __str__(self):
return "%s at char %s"%(self.msg, self.col)
@@ -40,7 +58,9 @@ def send_chunk(fp, val, blocksize, start, end):
def write_values(fp, vals, actions, sofar=0, skip=0, blocksize=BLOCKSIZE):
"""
vals: A list of values, which may be strings or Value objects.
- actions: A list of (offset, action, arg) tuples. Action may be "pause" or "disconnect".
+
+ actions: A list of (offset, action, arg) tuples. Action may be "pause"
+ or "disconnect".
Both vals and actions are in reverse order, with the first items last.
@@ -53,7 +73,13 @@ def write_values(fp, vals, actions, sofar=0, skip=0, blocksize=BLOCKSIZE):
offset = 0
while actions and actions[-1][0] < (sofar + len(v)):
a = actions.pop()
- offset += send_chunk(fp, v, blocksize, offset, a[0]-sofar-offset)
+ offset += send_chunk(
+ fp,
+ v,
+ blocksize,
+ offset,
+ a[0]-sofar-offset
+ )
if a[1] == "pause":
time.sleep(a[2])
elif a[1] == "disconnect":
@@ -121,15 +147,25 @@ DATATYPES = dict(
)
-v_integer = pp.Regex(r"\d+")\
+v_integer = pp.Word(pp.nums)\
.setName("integer")\
.setParseAction(lambda toks: int(toks[0]))
v_literal = pp.MatchFirst(
[
- pp.QuotedString("\"", escChar="\\", unquoteResults=True, multiline=True),
- pp.QuotedString("'", escChar="\\", unquoteResults=True, multiline=True),
+ pp.QuotedString(
+ "\"",
+ escChar="\\",
+ unquoteResults=True,
+ multiline=True
+ ),
+ pp.QuotedString(
+ "'",
+ escChar="\\",
+ unquoteResults=True,
+ multiline=True
+ ),
]
)
@@ -155,7 +191,7 @@ class LiteralGenerator:
return self.s.__getslice__(a, b)
def __repr__(self):
- return '"%s"'%self.s
+ return "'%s'"%self.s
class RandomGenerator:
@@ -202,6 +238,7 @@ class _Token(object):
A specification token. Tokens are immutable.
"""
__metaclass__ = abc.ABCMeta
+
@abc.abstractmethod
def expr(klass): # pragma: no cover
"""
@@ -242,10 +279,16 @@ class ValueLiteral(_ValueLiteral):
@classmethod
def expr(klass):
e = v_literal.copy()
- return e.setParseAction(lambda x: klass(*x))
+ return e.setParseAction(klass.parseAction)
+
+ @classmethod
+ def parseAction(klass, x):
+ v = klass(*x)
+ return v
def spec(self):
- return '"%s"'%self.val.encode("string_escape")
+ ret = "'%s'"%self.val.encode("string_escape")
+ return ret
class ValueNakedLiteral(_ValueLiteral):
@@ -278,7 +321,10 @@ class ValueGenerate(_Token):
def expr(klass):
e = pp.Literal("@").suppress() + v_integer
- u = reduce(operator.or_, [pp.Literal(i) for i in utils.SIZE_UNITS.keys()])
+ u = reduce(
+ operator.or_,
+ [pp.Literal(i) for i in utils.SIZE_UNITS.keys()]
+ ).leaveWhitespace()
e = e + pp.Optional(u, default=None)
s = pp.Literal(",").suppress()
@@ -318,13 +364,15 @@ class ValueFile(_Token):
s = os.path.expanduser(self.path)
s = os.path.normpath(os.path.abspath(os.path.join(sd, s)))
if not uf and not s.startswith(sd):
- raise FileAccessDenied("File access outside of configured directory")
+ raise FileAccessDenied(
+ "File access outside of configured directory"
+ )
if not os.path.isfile(s):
raise FileAccessDenied("File not readable")
return FileGenerator(s)
def spec(self):
- return '<"%s"'%self.path.encode("string_escape")
+ return "<'%s'"%self.path.encode("string_escape")
Value = pp.MatchFirst(
@@ -347,12 +395,12 @@ NakedValue = pp.MatchFirst(
Offset = pp.MatchFirst(
- [
- v_integer,
- pp.Literal("r"),
- pp.Literal("a")
- ]
- )
+ [
+ v_integer,
+ pp.Literal("r"),
+ pp.Literal("a")
+ ]
+)
class Raw(_Token):
@@ -392,11 +440,11 @@ class _Header(_Component):
def values(self, settings):
return [
- self.key.get_generator(settings),
- ": ",
- self.value.get_generator(settings),
- "\r\n",
- ]
+ self.key.get_generator(settings),
+ ": ",
+ self.value.get_generator(settings),
+ "\r\n",
+ ]
class Header(_Header):
@@ -459,7 +507,10 @@ class ShortcutUserAgent(_Header):
@classmethod
def expr(klass):
e = pp.Literal("u").suppress()
- u = reduce(operator.or_, [pp.Literal(i[1]) for i in http_uastrings.UASTRINGS])
+ u = reduce(
+ operator.or_,
+ [pp.Literal(i[1]) for i in http_uastrings.UASTRINGS]
+ )
e += u | Value
return e.setParseAction(lambda x: klass(*x))
@@ -470,7 +521,6 @@ class ShortcutUserAgent(_Header):
return ShortcutUserAgent(self.value.freeze(settings))
-
class Body(_Component):
def __init__(self, value):
self.value = value
@@ -483,8 +533,8 @@ class Body(_Component):
def values(self, settings):
return [
- self.value.get_generator(settings),
- ]
+ self.value.get_generator(settings),
+ ]
def spec(self):
return "b%s"%(self.value.spec())
@@ -493,6 +543,38 @@ class Body(_Component):
return Body(self.value.freeze(settings))
+class PathodSpec(_Token):
+ def __init__(self, value):
+ self.value = value
+ try:
+ self.parsed = Response(
+ Response.expr().parseString(
+ value.val,
+ parseAll=True
+ )
+ )
+ except pp.ParseException, v:
+ raise ParseException(v.msg, v.line, v.col)
+
+ @classmethod
+ def expr(klass):
+ e = pp.Literal("s").suppress()
+ e = e + ValueLiteral.expr()
+ return e.setParseAction(lambda x: klass(*x))
+
+ def values(self, settings):
+ return [
+ self.value.get_generator(settings),
+ ]
+
+ def spec(self):
+ return "s%s"%(self.value.spec())
+
+ def freeze(self, settings):
+ f = self.parsed.freeze(settings).spec()
+ return PathodSpec(ValueLiteral(f.encode("string_escape")))
+
+
class Path(_Component):
def __init__(self, value):
if isinstance(value, basestring):
@@ -506,8 +588,8 @@ class Path(_Component):
def values(self, settings):
return [
- self.value.get_generator(settings),
- ]
+ self.value.get_generator(settings),
+ ]
def spec(self):
return "%s"%(self.value.spec())
@@ -527,6 +609,7 @@ class Method(_Component):
"trace",
"connect",
]
+
def __init__(self, value):
# If it's a string, we were passed one of the methods, so we upper-case
# it to be canonical. The user can specify a different case by using a
@@ -645,11 +728,11 @@ class PauseAt(_Action):
e += Offset
e += pp.Literal(",").suppress()
e += pp.MatchFirst(
- [
- v_integer,
- pp.Literal("f")
- ]
- )
+ [
+ v_integer,
+ pp.Literal("f")
+ ]
+ )
return e.setParseAction(lambda x: klass(*x))
def spec(self):
@@ -700,10 +783,10 @@ class InjectAt(_Action):
def intermediate(self, settings):
return (
- self.offset,
- "inject",
- self.value.get_generator(settings)
- )
+ self.offset,
+ "inject",
+ self.value.get_generator(settings)
+ )
def freeze(self, settings):
return InjectAt(self.offset, self.value.freeze(settings))
@@ -712,6 +795,7 @@ class InjectAt(_Action):
class _Message(object):
__metaclass__ = abc.ABCMeta
version = "HTTP/1.1"
+
def __init__(self, tokens):
self.tokens = tokens
@@ -741,7 +825,8 @@ class _Message(object):
def length(self, settings):
"""
- Calculate the length of the base message without any applied actions.
+ Calculate the length of the base message without any applied
+ actions.
"""
return sum(len(x) for x in self.values(settings))
@@ -754,7 +839,8 @@ class _Message(object):
def maximum_length(self, settings):
"""
- Calculate the maximum length of the base message with all applied actions.
+ Calculate the maximum length of the base message with all applied
+ actions.
"""
l = self.length(settings)
for i in self.actions:
@@ -781,14 +867,6 @@ class _Message(object):
ValueLiteral(request_host)
)
)
- else:
- if not utils.get_header("Date", self.headers):
- tokens.append(
- Header(
- ValueLiteral("Date"),
- ValueLiteral(formatdate(timeval=None, localtime=False, usegmt=True))
- )
- )
intermediate = self.__class__(tokens)
return self.__class__([i.resolve(intermediate, settings) for i in tokens])
@@ -807,7 +885,8 @@ class _Message(object):
ret = {}
for i in self.logattrs:
v = getattr(self, i)
- # Careful not to log any VALUE specs without sanitizing them first. We truncate at 1k.
+ # Careful not to log any VALUE specs without sanitizing them first.
+ # We truncate at 1k.
if hasattr(v, "values"):
v = [x[:TRUNCATE] for x in v.values(settings)]
v = "".join(v).encode("string_escape")
@@ -838,6 +917,7 @@ class _Message(object):
Sep = pp.Optional(pp.Literal(":")).suppress()
+
class Response(_Message):
comps = (
Body,
@@ -851,6 +931,7 @@ class Response(_Message):
Reason
)
logattrs = ["code", "reason", "version", "body"]
+
@property
def code(self):
return self._get_token(Code)
@@ -866,7 +947,14 @@ class Response(_Message):
if self.reason:
l.extend(self.reason.values(settings))
else:
- l.append(LiteralGenerator(http_status.RESPONSES.get(int(self.code.code), "Unknown code")))
+ l.append(
+ LiteralGenerator(
+ http_status.RESPONSES.get(
+ int(self.code.code),
+ "Unknown code"
+ )
+ )
+ )
return l
@classmethod
@@ -894,9 +982,11 @@ class Request(_Message):
InjectAt,
ShortcutContentType,
ShortcutUserAgent,
- Raw
+ Raw,
+ PathodSpec,
)
logattrs = ["method", "path", "body"]
+
@property
def method(self):
return self._get_token(Method)
@@ -905,10 +995,16 @@ class Request(_Message):
def path(self):
return self._get_token(Path)
+ @property
+ def pathodspec(self):
+ return self._get_token(PathodSpec)
+
def preamble(self, settings):
v = self.method.values(settings)
v.append(" ")
v.extend(self.path.values(settings))
+ if self.pathodspec:
+ v.append(self.pathodspec.parsed.spec())
v.append(" ")
v.append(self.version)
return v
@@ -944,7 +1040,7 @@ def make_error_response(reason, body=None):
]
return PathodErrorResponse(tokens)
-FILESTART = "+"
+
def read_file(settings, s):
uf = settings.get("unconstrained_file_access")
sd = settings.get("staticdir")
@@ -961,33 +1057,34 @@ def read_file(settings, s):
return file(s, "rb").read()
-def parse_response(settings, s):
+def parse_response(s):
"""
- May raise ParseException or FileAccessDenied
+ May raise ParseException
"""
try:
s = s.decode("ascii")
except UnicodeError:
raise ParseException("Spec must be valid ASCII.", 0, 0)
- if s.startswith(FILESTART):
- s = read_file(settings, s)
try:
return Response(Response.expr().parseString(s, parseAll=True))
except pp.ParseException, v:
raise ParseException(v.msg, v.line, v.col)
-def parse_request(settings, s):
+def parse_requests(s):
"""
- May raise ParseException or FileAccessDenied
+ May raise ParseException
"""
try:
s = s.decode("ascii")
except UnicodeError:
raise ParseException("Spec must be valid ASCII.", 0, 0)
- if s.startswith(FILESTART):
- s = read_file(settings, s)
try:
- return Request(Request.expr().parseString(s, parseAll=True))
+ parts = pp.OneOrMore(
+ pp.Group(
+ Request.expr()
+ )
+ ).parseString(s, parseAll=True)
+ return [Request(i) for i in parts]
except pp.ParseException, v:
raise ParseException(v.msg, v.line, v.col)
diff --git a/libpathod/pathoc.py b/libpathod/pathoc.py
index 938dbeba..e7aff520 100644
--- a/libpathod/pathoc.py
+++ b/libpathod/pathoc.py
@@ -1,10 +1,17 @@
-import sys, os
+import sys
+import os
+import hashlib
+import random
from netlib import tcp, http, certutils
import netlib.utils
-import language, utils
+
+import language
+import utils
import OpenSSL.crypto
-class PathocError(Exception): pass
+
+class PathocError(Exception):
+ pass
class SSLInfo:
@@ -13,8 +20,17 @@ class SSLInfo:
class Response:
- def __init__(self, httpversion, status_code, msg, headers, content, sslinfo):
- self.httpversion, self.status_code, self.msg = httpversion, status_code, msg
+ def __init__(
+ self,
+ httpversion,
+ status_code,
+ msg,
+ headers,
+ content,
+ sslinfo
+ ):
+ self.httpversion, self.status_code = httpversion, status_code
+ self.msg = msg
self.headers, self.content = headers, content
self.sslinfo = sslinfo
@@ -23,7 +39,14 @@ class Response:
class Pathoc(tcp.TCPClient):
- def __init__(self, address, ssl=None, sni=None, sslversion=4, clientcert=None, ciphers=None):
+ def __init__(
+ self,
+ address,
+ ssl=None,
+ sni=None,
+ sslversion=4,
+ clientcert=None,
+ ciphers=None):
tcp.TCPClient.__init__(self, address)
self.settings = dict(
staticdir = os.getcwd(),
@@ -36,9 +59,9 @@ class Pathoc(tcp.TCPClient):
def http_connect(self, connect_to):
self.wfile.write(
- 'CONNECT %s:%s HTTP/1.1\r\n'%tuple(connect_to) +
- '\r\n'
- )
+ 'CONNECT %s:%s HTTP/1.1\r\n'%tuple(connect_to) +
+ '\r\n'
+ )
self.wfile.flush()
l = self.rfile.readline()
if not l:
@@ -60,17 +83,17 @@ class Pathoc(tcp.TCPClient):
if self.ssl:
try:
self.convert_to_ssl(
- sni=self.sni,
- cert=self.clientcert,
- method=self.sslversion,
- cipher_list = self.ciphers
- )
+ sni=self.sni,
+ cert=self.clientcert,
+ method=self.sslversion,
+ cipher_list = self.ciphers
+ )
except tcp.NetLibError, v:
raise PathocError(str(v))
self.sslinfo = SSLInfo(
- self.connection.get_peer_cert_chain(),
- self.get_current_cipher()
- )
+ self.connection.get_peer_cert_chain(),
+ self.get_current_cipher()
+ )
def request(self, spec):
"""
@@ -79,7 +102,7 @@ class Pathoc(tcp.TCPClient):
May raise language.ParseException, netlib.http.HttpError or
language.FileAccessDenied.
"""
- r = language.parse_request(self.settings, spec)
+ r = language.parse_requests(spec)[0]
language.serve(r, self.wfile, self.settings, self.address.host)
self.wfile.flush()
ret = list(http.read_response(self.rfile, r.method.string(), None))
@@ -87,7 +110,9 @@ class Pathoc(tcp.TCPClient):
return Response(*ret)
def _show_summary(self, fp, httpversion, code, msg, headers, content):
- print >> fp, "<< %s %s: %s bytes"%(code, utils.xrepr(msg), len(content))
+ print >> fp, "<< %s %s: %s bytes"%(
+ code, utils.xrepr(msg), len(content)
+ )
def _show(self, fp, header, data, hexdump):
if hexdump:
@@ -98,7 +123,18 @@ class Pathoc(tcp.TCPClient):
print >> fp, "%s (unprintables escaped):"%header
print >> fp, netlib.utils.cleanBin(data)
- def print_request(self, spec, showreq, showresp, explain, showssl, hexdump, ignorecodes, ignoretimeout, fp=sys.stdout):
+ def print_request(
+ self,
+ r,
+ showreq,
+ showresp,
+ explain,
+ showssl,
+ hexdump,
+ ignorecodes,
+ ignoretimeout,
+ fp=sys.stdout
+ ):
"""
Performs a series of requests, and prints results to the specified
file descriptor.
@@ -113,26 +149,18 @@ class Pathoc(tcp.TCPClient):
Returns True if we have a non-ignored response.
"""
- try:
- r = language.parse_request(self.settings, spec)
- except language.ParseException, v:
- print >> fp, "Error parsing request spec: %s"%v.msg
- print >> fp, v.marked()
- return
- except language.FileAccessDenied, v:
- print >> fp, "File access error: %s"%v
- return
-
- if explain:
- r = r.freeze(self.settings, self.address.host)
-
resp, req = None, None
if showreq:
self.wfile.start_log()
if showresp:
self.rfile.start_log()
try:
- req = language.serve(r, self.wfile, self.settings, self.address.host)
+ req = language.serve(
+ r,
+ self.wfile,
+ self.settings,
+ self.address.host
+ )
self.wfile.flush()
resp = http.read_response(self.rfile, r.method.string(), None)
except http.HttpError, v:
@@ -160,7 +188,7 @@ class Pathoc(tcp.TCPClient):
if resp:
self._show_summary(fp, *resp)
- if self.sslinfo:
+ if showssl and self.sslinfo:
print >> fp, "Cipher: %s, %s bit, %s"%self.sslinfo.cipher
print >> fp, "SSL certificate chain:\n"
for i in self.sslinfo.certchain:
@@ -173,13 +201,15 @@ class Pathoc(tcp.TCPClient):
print >> fp, "%s=%s"%cn,
print >> fp
print >> fp, "\tVersion: %s"%i.get_version()
- print >> fp, "\tValidity: %s - %s"%(i.get_notBefore(),i.get_notAfter())
+ print >> fp, "\tValidity: %s - %s"%(
+ i.get_notBefore(), i.get_notAfter()
+ )
print >> fp, "\tSerial: %s"%i.get_serial_number()
print >> fp, "\tAlgorithm: %s"%i.get_signature_algorithm()
pk = i.get_pubkey()
types = {
- OpenSSL.crypto.TYPE_RSA: "RSA",
- OpenSSL.crypto.TYPE_DSA: "DSA"
+ OpenSSL.crypto.TYPE_RSA: "RSA",
+ OpenSSL.crypto.TYPE_DSA: "DSA"
}
t = types.get(pk.type(), "Uknown")
print >> fp, "\tPubkey: %s bit %s"%(pk.bits(), t)
@@ -190,4 +220,68 @@ class Pathoc(tcp.TCPClient):
return True
+def main(args):
+ memo = set([])
+ trycount = 0
+ try:
+ cnt = 0
+ while 1:
+ if trycount > args.memolimit:
+ print >> sys.stderr, "Memo limit exceeded..."
+ return
+ cnt += 1
+ if args.random:
+ playlist = [random.choice(args.requests)]
+ else:
+ playlist = args.requests
+ p = Pathoc(
+ (args.host, args.port),
+ ssl=args.ssl,
+ sni=args.sni,
+ sslversion=args.sslversion,
+ clientcert=args.clientcert,
+ ciphers=args.ciphers
+ )
+ if args.explain or args.memo:
+ playlist = [
+ i.freeze(p.settings, p.address.host) for i in playlist
+ ]
+ if args.memo:
+ newlist = []
+ for spec in playlist:
+ h = hashlib.sha256(spec.spec()).digest()
+ if h not in memo:
+ memo.add(h)
+ newlist.append(spec)
+ playlist = newlist
+ if not playlist:
+ trycount += 1
+ continue
+
+ trycount = 0
+ try:
+ p.connect(args.connect_to)
+ except (tcp.NetLibError, PathocError), v:
+ print >> sys.stderr, str(v)
+ sys.exit(1)
+ if args.timeout:
+ p.settimeout(args.timeout)
+ for spec in playlist:
+ ret = p.print_request(
+ spec,
+ showreq=args.showreq,
+ showresp=args.showresp,
+ explain=args.explain,
+ showssl=args.showssl,
+ hexdump=args.hexdump,
+ ignorecodes=args.ignorecodes,
+ ignoretimeout=args.ignoretimeout
+ )
+ sys.stdout.flush()
+ if ret and args.oneshot:
+ sys.exit(0)
+ if cnt == args.repeat:
+ break
+ except KeyboardInterrupt:
+ pass
diff --git a/libpathod/pathod.py b/libpathod/pathod.py
index 79fe7ed1..173773cf 100644
--- a/libpathod/pathod.py
+++ b/libpathod/pathod.py
@@ -1,7 +1,12 @@
-import urllib, threading, re, logging, os
+import urllib
+import threading
+import logging
+import os
+import sys
from netlib import tcp, http, wsgi, certutils
import netlib.utils
-import version, app, language, utils
+
+from . import version, app, language, utils
DEFAULT_CERT_DOMAIN = "pathod.net"
@@ -12,7 +17,8 @@ CA_CERT_NAME = "mitmproxy-ca.pem"
logger = logging.getLogger('pathod')
-class PathodError(Exception): pass
+class PathodError(Exception):
+ pass
class SSLOptions:
@@ -64,7 +70,12 @@ class PathodHandler(tcp.BaseHandler):
if self.server.explain and not isinstance(crafted, language.PathodErrorResponse):
crafted = crafted.freeze(self.server.request_settings, None)
self.info(">> Spec: %s" % crafted.spec())
- response_log = language.serve(crafted, self.wfile, self.server.request_settings, None)
+ response_log = language.serve(
+ crafted,
+ self.wfile,
+ self.server.request_settings,
+ None
+ )
if response_log["disconnect"]:
return False, response_log
return True, response_log
@@ -162,15 +173,14 @@ class PathodHandler(tcp.BaseHandler):
for i in self.server.anchors:
if i[0].match(path):
self.info("crafting anchor: %s" % path)
- aresp = language.parse_response(self.server.request_settings, i[1])
- again, retlog["response"] = self.serve_crafted(aresp)
+ again, retlog["response"] = self.serve_crafted(i[1])
return again, retlog
if not self.server.nocraft and path.startswith(self.server.craftanchor):
spec = urllib.unquote(path)[len(self.server.craftanchor):]
self.info("crafting spec: %s" % spec)
try:
- crafted = language.parse_response(self.server.request_settings, spec)
+ crafted = language.parse_response(spec)
except language.ParseException, v:
self.info("Parse error: %s" % v.msg)
crafted = language.make_error_response(
@@ -182,7 +192,10 @@ class PathodHandler(tcp.BaseHandler):
elif self.server.noweb:
crafted = language.make_error_response("Access Denied")
language.serve(crafted, self.wfile, self.server.request_settings)
- return False, dict(type="error", msg="Access denied: web interface disabled")
+ return False, dict(
+ type="error",
+ msg="Access denied: web interface disabled"
+ )
else:
self.info("app: %s %s" % (method, path))
req = wsgi.Request("http", method, path, headers, content)
@@ -252,19 +265,34 @@ class Pathod(tcp.TCPServer):
LOGBUF = 500
def __init__(
- self, addr, confdir=CONFDIR, ssl=False, ssloptions=None,
- craftanchor="/p/", staticdir=None, anchors=None,
- sizelimit=None, noweb=False, nocraft=False, noapi=False,
- nohang=False, timeout=None, logreq=False, logresp=False,
- explain=False, hexdump=False
+ self,
+ addr,
+ confdir=CONFDIR,
+ ssl=False,
+ ssloptions=None,
+ craftanchor="/p/",
+ staticdir=None,
+ anchors=(),
+ sizelimit=None,
+ noweb=False,
+ nocraft=False,
+ noapi=False,
+ nohang=False,
+ timeout=None,
+ logreq=False,
+ logresp=False,
+ explain=False,
+ hexdump=False
):
"""
addr: (address, port) tuple. If port is 0, a free port will be
automatically chosen.
ssloptions: an SSLOptions object.
- craftanchor: string specifying the path under which to anchor response generation.
+ craftanchor: string specifying the path under which to anchor
+ response generation.
staticdir: path to a directory of static resources, or None.
- anchors: A list of (regex, spec) tuples, or None.
+ anchors: List of (regex object, language.Request object) tuples, or
+ None.
sizelimit: Limit size of served data.
nocraft: Disable response crafting.
noapi: Disable the API.
@@ -276,26 +304,17 @@ class Pathod(tcp.TCPServer):
self.staticdir = staticdir
self.craftanchor = craftanchor
self.sizelimit = sizelimit
- self.noweb, self.nocraft, self.noapi, self.nohang = noweb, nocraft, noapi, nohang
- self.timeout, self.logreq, self.logresp, self.hexdump = timeout, logreq, logresp, hexdump
+ self.noweb, self.nocraft = noweb, nocraft
+ self.noapi, self.nohang = noapi, nohang
+ self.timeout, self.logreq = timeout, logreq
+ self.logresp, self.hexdump = logresp, hexdump
self.explain = explain
self.app = app.make_app(noapi)
self.app.config["pathod"] = self
self.log = []
self.logid = 0
- self.anchors = []
- if anchors:
- for i in anchors:
- try:
- arex = re.compile(i[0])
- except re.error:
- raise PathodError("Invalid regex in anchor: %s" % i[0])
- try:
- language.parse_response(self.request_settings, i[1])
- except language.ParseException, v:
- raise PathodError("Invalid page spec in anchor: '%s', %s" % (i[1], str(v)))
- self.anchors.append((arex, i[1]))
+ self.anchors = anchors
def check_policy(self, req, settings):
"""
@@ -363,3 +382,72 @@ class Pathod(tcp.TCPServer):
def get_log(self):
return self.log
+
+
+def main(args):
+ ssloptions = SSLOptions(
+ cn = args.cn,
+ confdir = args.confdir,
+ not_after_connect = args.ssl_not_after_connect,
+ ciphers = args.ciphers,
+ sslversion = utils.SSLVERSIONS[args.sslversion],
+ certs = args.ssl_certs
+ )
+
+ root = logging.getLogger()
+ if root.handlers:
+ for handler in root.handlers:
+ root.removeHandler(handler)
+
+ log = logging.getLogger('pathod')
+ log.setLevel(logging.DEBUG)
+ fmt = logging.Formatter(
+ '%(asctime)s: %(message)s',
+ datefmt='%d-%m-%y %H:%M:%S',
+ )
+ if args.logfile:
+ fh = logging.handlers.WatchedFileHandler(args.logfile)
+ fh.setFormatter(fmt)
+ log.addHandler(fh)
+ if not args.daemonize:
+ sh = logging.StreamHandler()
+ sh.setFormatter(fmt)
+ log.addHandler(sh)
+
+ try:
+ pd = Pathod(
+ (args.address, args.port),
+ craftanchor = args.craftanchor,
+ ssl = args.ssl,
+ ssloptions = ssloptions,
+ staticdir = args.staticdir,
+ anchors = args.anchors,
+ sizelimit = args.sizelimit,
+ noweb = args.noweb,
+ nocraft = args.nocraft,
+ noapi = args.noapi,
+ nohang = args.nohang,
+ timeout = args.timeout,
+ logreq = args.logreq,
+ logresp = args.logresp,
+ hexdump = args.hexdump,
+ explain = args.explain,
+ )
+ except PathodError, v:
+ print >> sys.stderr, "Error: %s"%v
+ sys.exit(1)
+ except language.FileAccessDenied, v:
+ print >> sys.stderr, "Error: %s"%v
+
+ if args.daemonize:
+ utils.daemonize()
+
+ try:
+ print "%s listening on %s:%s"%(
+ version.NAMEVERSION,
+ pd.address.host,
+ pd.address.port
+ )
+ pd.serve_forever()
+ except KeyboardInterrupt:
+ pass
diff --git a/libpathod/templates/docs_lang.html b/libpathod/templates/docs_lang.html
index aef12a8d..7cb3fc5f 100644
--- a/libpathod/templates/docs_lang.html
+++ b/libpathod/templates/docs_lang.html
@@ -183,14 +183,6 @@
</div>
-<section id="specifying_requests">
- <div class="page-header">
- <h1>Executing specs from file</h1>
- </div>
-
- <pre class="example">+./path/to/spec</pre>
-
-</section>
<section id="specifying_requests">
<div class="page-header">
diff --git a/libpathod/test.py b/libpathod/test.py
index 57d40300..6f95a797 100644
--- a/libpathod/test.py
+++ b/libpathod/test.py
@@ -1,4 +1,5 @@
-import threading, Queue
+import threading
+import Queue
import requests
import requests.packages.urllib3
import pathod
@@ -6,7 +7,6 @@ import pathod
requests.packages.urllib3.disable_warnings()
-
class Daemon:
IFACE = "127.0.0.1"
def __init__(self, ssl=None, **daemonargs):
@@ -14,7 +14,11 @@ class Daemon:
self.thread = _PaThread(self.IFACE, self.q, ssl, daemonargs)
self.thread.start()
self.port = self.q.get(True, 5)
- self.urlbase = "%s://%s:%s"%("https" if ssl else "http", self.IFACE, self.port)
+ self.urlbase = "%s://%s:%s"%(
+ "https" if ssl else "http",
+ self.IFACE,
+ self.port
+ )
def __enter__(self):
return self
@@ -80,6 +84,9 @@ class _PaThread(threading.Thread):
ssl = self.ssl,
**self.daemonargs
)
- self.name = "PathodThread (%s:%s)" % (self.server.address.host, self.server.address.port)
+ self.name = "PathodThread (%s:%s)" % (
+ self.server.address.host,
+ self.server.address.port
+ )
self.q.put(self.server.address.port)
self.server.serve_forever()
diff --git a/libpathod/utils.py b/libpathod/utils.py
index 110a7170..d4160a23 100644
--- a/libpathod/utils.py
+++ b/libpathod/utils.py
@@ -1,4 +1,5 @@
import os
+import sys
from netlib import tcp
SSLVERSIONS = {
@@ -110,3 +111,30 @@ class Data:
data = Data(__name__)
+
+def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
+ try:
+ pid = os.fork()
+ if pid > 0:
+ sys.exit(0)
+ except OSError, e:
+ sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror))
+ sys.exit(1)
+ os.chdir("/")
+ os.umask(0)
+ os.setsid()
+ try:
+ pid = os.fork()
+ if pid > 0:
+ sys.exit(0)
+ except OSError, e:
+ sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror))
+ sys.exit(1)
+ si = open(stdin, 'rb')
+ so = open(stdout, 'a+b')
+ se = open(stderr, 'a+b', 0)
+ os.dup2(si.fileno(), sys.stdin.fileno())
+ os.dup2(so.fileno(), sys.stdout.fileno())
+ os.dup2(se.fileno(), sys.stderr.fileno())
+
+
diff --git a/pathoc b/pathoc
new file mode 100755
index 00000000..cbf8f773
--- /dev/null
+++ b/pathoc
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+from libpathod import cmdline
+
+if __name__ == "__main__":
+ cmdline.go_pathoc()
diff --git a/pathod b/pathod
new file mode 100755
index 00000000..ca0baa57
--- /dev/null
+++ b/pathod
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+from libpathod import cmdline
+
+if __name__ == "__main__":
+ cmdline.go_pathod()
diff --git a/setup.py b/setup.py
index 02ec25b2..cb2b7798 100644
--- a/setup.py
+++ b/setup.py
@@ -35,16 +35,12 @@ setup(
packages=find_packages(),
include_package_data=True,
-
- entry_points={
- 'console_scripts': [
- "pathod = libpathod.main:pathod",
- "pathoc = libpathod.main:pathoc"
- ]
- },
-
+ scripts = ["pathod", "pathoc"],
install_requires=[
"netlib>=%s, <%s" % (version.MINORVERSION, version.NEXT_MINORVERSION),
+ # It's INSANE that we have to do this, but...
+ # FIXME: Requirement to be removed at next release
+ "pip>=1.5.6",
"requests>=2.4.1",
"Flask>=0.10.1"
],
diff --git a/test/test_app.py b/test/test_app.py
index f35def00..7c7ac730 100644
--- a/test/test_app.py
+++ b/test/test_app.py
@@ -1,7 +1,9 @@
import tutils
+
class TestApp(tutils.DaemonTests):
SSL = False
+
def test_index(self):
r = self.getpath("/")
assert r.status_code == 200
@@ -78,4 +80,3 @@ class TestApp(tutils.DaemonTests):
r = self.getpath("/request_preview", params=dict(spec=""))
assert r.status_code == 200
assert 'empty spec' in r.content
-
diff --git a/test/test_language.py b/test/test_language.py
index 5e9176ab..e2d5190f 100644
--- a/test/test_language.py
+++ b/test/test_language.py
@@ -1,10 +1,19 @@
-import os, cStringIO
+import os
+import cStringIO
from libpathod import language, utils
import tutils
language.TESTING = True
+def test_quote():
+ assert language.quote("'\\\\'")
+
+
+def parse_request(s):
+ return language.parse_requests(s)[0]
+
+
class TestValueNakedLiteral:
def test_expr(self):
v = language.ValueNakedLiteral("foo")
@@ -24,21 +33,35 @@ class TestValueLiteral:
assert v.expr()
assert v.val == "foo"
- v = language.ValueLiteral(r"foo\n")
+ v = language.ValueLiteral("foo\n")
assert v.expr()
assert v.val == "foo\n"
assert repr(v)
def test_spec(self):
v = language.ValueLiteral("foo")
- assert v.spec() == r'"foo"'
+ assert v.spec() == r"'foo'"
v = language.ValueLiteral("f\x00oo")
- assert v.spec() == repr(v) == r'"f\x00oo"'
+ assert v.spec() == repr(v) == r"'f\x00oo'"
- def test_freeze(self):
- v = language.ValueLiteral("foo")
- assert v.freeze({}).val == v.val
+ v = language.ValueLiteral("\"")
+ assert v.spec() == repr(v) == '\'"\''
+
+ def roundtrip(self, spec):
+ e = language.ValueLiteral.expr()
+ v = language.ValueLiteral(spec)
+ v2 = e.parseString(v.spec())
+ assert v.val == v2[0].val
+ assert v.spec() == v2[0].spec()
+
+ def test_roundtrip(self):
+ self.roundtrip("'")
+ self.roundtrip('\'')
+ self.roundtrip("a")
+ self.roundtrip("\"")
+ self.roundtrip(r"\\")
+ self.roundtrip("200:b'foo':i23,'\\''")
class TestValueGenerate:
@@ -181,7 +204,7 @@ class TestMisc:
assert e.parseString("'get'")[0].value.val == "get"
assert e.parseString("get")[0].spec() == "get"
- assert e.parseString("'foo'")[0].spec() == '"foo"'
+ assert e.parseString("'foo'")[0].spec() == "'foo'"
s = e.parseString("get")[0].spec()
assert s == e.parseString(s)[0].spec()
@@ -218,6 +241,31 @@ class TestMisc:
s = v.spec()
assert s == e.parseString(s)[0].spec()
+ def test_pathodspec(self):
+ e = language.PathodSpec.expr()
+ v = e.parseString("s'200'")[0]
+ assert v.value.val == "200"
+ tutils.raises(
+ language.ParseException,
+ e.parseString,
+ "s'foo'"
+ )
+
+ v = e.parseString('s"200:b@1"')[0]
+ assert "@1" in v.spec()
+ f = v.freeze({})
+ assert "@1" not in f.spec()
+
+ def test_pathodspec_freeze(self):
+ e = language.PathodSpec(
+ language.ValueLiteral(
+ "200:b'foo':i10,'\\''".encode(
+ "string_escape"
+ )
+ )
+ )
+ assert e.freeze({})
+
def test_code(self):
e = language.Code.expr()
v = e.parseString("200")[0]
@@ -298,11 +346,11 @@ class TestHeaders:
assert v2.value.val == v3.value.val
def test_shortcuts(self):
- assert language.parse_response({}, "400:c'foo'").headers[0].key.val == "Content-Type"
- assert language.parse_response({}, "400:l'foo'").headers[0].key.val == "Location"
+ assert language.parse_response("400:c'foo'").headers[0].key.val == "Content-Type"
+ assert language.parse_response("400:l'foo'").headers[0].key.val == "Location"
- assert 'Android' in language.parse_request({}, "get:/:ua").headers[0].value.val
- assert language.parse_request({}, "get:/:ua").headers[0].key.val == "User-Agent"
+ assert 'Android' in parse_request("get:/:ua").headers[0].value.val
+ assert parse_request("get:/:ua").headers[0].key.val == "User-Agent"
class TestShortcutUserAgent:
@@ -336,7 +384,7 @@ class Test_Action:
assert l[0].offset == 0
def test_resolve(self):
- r = language.parse_request({}, 'GET:"/foo"')
+ r = parse_request('GET:"/foo"')
e = language.DisconnectAt("r")
ret = e.resolve(r, {})
assert isinstance(ret.offset, int)
@@ -352,9 +400,9 @@ class Test_Action:
class TestDisconnects:
def test_parse_response(self):
- a = language.parse_response({}, "400:d0").actions[0]
+ a = language.parse_response("400:d0").actions[0]
assert a.spec() == "d0"
- a = language.parse_response({}, "400:dr").actions[0]
+ a = language.parse_response("400:dr").actions[0]
assert a.spec() == "dr"
def test_at(self):
@@ -377,12 +425,12 @@ class TestDisconnects:
class TestInject:
def test_parse_response(self):
- a = language.parse_response({}, "400:ir,@100").actions[0]
+ a = language.parse_response("400:ir,@100").actions[0]
assert a.offset == "r"
assert a.value.datatype == "bytes"
assert a.value.usize == 100
- a = language.parse_response({}, "400:ia,@100").actions[0]
+ a = language.parse_response("400:ia,@100").actions[0]
assert a.offset == "a"
def test_at(self):
@@ -397,7 +445,7 @@ class TestInject:
def test_serve(self):
s = cStringIO.StringIO()
- r = language.parse_response({}, "400:i0,'foo'")
+ r = language.parse_response("400:i0,'foo'")
assert language.serve(r, s, {})
def test_spec(self):
@@ -430,7 +478,7 @@ class TestPauses:
assert v.offset == "a"
def test_request(self):
- r = language.parse_response({}, '400:p10,10')
+ r = language.parse_response('400:p10,10')
assert r.actions[0].spec() == "p10,10"
def test_spec(self):
@@ -444,30 +492,59 @@ class TestPauses:
class TestRequest:
- def test_file(self):
- p = tutils.test_data.path("data")
- d = dict(staticdir=p)
- r = language.parse_request(d, "+request")
- assert r.path.values({})[0][:] == "/foo"
-
def test_nonascii(self):
- tutils.raises("ascii", language.parse_request, {}, "get:\xf0")
+ tutils.raises("ascii", parse_request, "get:\xf0")
def test_err(self):
- tutils.raises(language.ParseException, language.parse_request, {}, 'GET')
+ tutils.raises(language.ParseException, parse_request, 'GET')
def test_simple(self):
- r = language.parse_request({}, 'GET:"/foo"')
+ r = parse_request('GET:"/foo"')
assert r.method.string() == "GET"
assert r.path.string() == "/foo"
- r = language.parse_request({}, 'GET:/foo')
+ r = parse_request('GET:/foo')
assert r.path.string() == "/foo"
- r = language.parse_request({}, 'GET:@1k')
+ r = parse_request('GET:@1k')
assert len(r.path.string()) == 1024
+ def test_multiple(self):
+ r = language.parse_requests("GET:/ PUT:/")
+ assert r[0].method.string() == "GET"
+ assert r[1].method.string() == "PUT"
+ assert len(r) == 2
+
+ l = """
+ GET
+ "/foo"
+ ir,@1
+
+ PUT
+
+ "/foo
+
+
+
+ bar"
+
+ ir,@1
+ """
+ r = language.parse_requests(l)
+ assert len(r) == 2
+ assert r[0].method.string() == "GET"
+ assert r[1].method.string() == "PUT"
+
+ l = """
+ get:"http://localhost:9999/p/200":ir,@1
+ get:"http://localhost:9999/p/200":ir,@2
+ """
+ r = language.parse_requests(l)
+ assert len(r) == 2
+ assert r[0].method.string() == "GET"
+ assert r[1].method.string() == "GET"
+
def test_render(self):
s = cStringIO.StringIO()
- r = language.parse_request({}, "GET:'/foo'")
+ r = parse_request("GET:'/foo'")
assert language.serve(r, s, {}, "foo.com")
def test_multiline(self):
@@ -476,12 +553,11 @@ class TestRequest:
"/foo"
ir,@1
"""
- r = language.parse_request({}, l)
+ r = parse_request(l)
assert r.method.string() == "GET"
assert r.path.string() == "/foo"
assert r.actions
-
l = """
GET
@@ -493,24 +569,24 @@ class TestRequest:
ir,@1
"""
- r = language.parse_request({}, l)
+ r = parse_request(l)
assert r.method.string() == "GET"
assert r.path.string().endswith("bar")
assert r.actions
def test_spec(self):
def rt(s):
- s = language.parse_request({}, s).spec()
- assert language.parse_request({}, s).spec() == s
+ s = parse_request(s).spec()
+ assert parse_request(s).spec() == s
rt("get:/foo")
rt("get:/foo:da")
def test_freeze(self):
- r = language.parse_request({}, "GET:/:b@100").freeze({})
+ r = parse_request("GET:/:b@100").freeze({})
assert len(r.spec()) > 100
def test_path_generator(self):
- r = language.parse_request({}, "GET:@100").freeze({})
+ r = parse_request("GET:@100").freeze({})
assert len(r.spec()) > 100
@@ -580,72 +656,69 @@ class TestWriteValues:
def test_write_values_after(self):
s = cStringIO.StringIO()
- r = language.parse_response({}, "400:da")
+ r = language.parse_response("400:da")
language.serve(r, s, {})
s = cStringIO.StringIO()
- r = language.parse_response({}, "400:pa,0")
+ r = language.parse_response("400:pa,0")
language.serve(r, s, {})
s = cStringIO.StringIO()
- r = language.parse_response({}, "400:ia,'xx'")
+ r = language.parse_response("400:ia,'xx'")
language.serve(r, s, {})
assert s.getvalue().endswith('xx')
class TestResponse:
def dummy_response(self):
- return language.parse_response({}, "400'msg'")
-
- def test_file(self):
- p = tutils.test_data.path("data")
- d = dict(staticdir=p)
- r = language.parse_response(d, "+response")
- assert r.code.string() == "202"
+ return language.parse_response("400'msg'")
def test_response(self):
- r = language.parse_response({}, "400:m'msg'")
+ r = language.parse_response("400:m'msg'")
assert r.code.string() == "400"
assert r.reason.string() == "msg"
- r = language.parse_response({}, "400:m'msg':b@100b")
+ r = language.parse_response("400:m'msg':b@100b")
assert r.reason.string() == "msg"
assert r.body.values({})
assert str(r)
- r = language.parse_response({}, "200")
+ r = language.parse_response("200")
assert r.code.string() == "200"
assert not r.reason
assert "OK" in [i[:] for i in r.preamble({})]
def test_render(self):
s = cStringIO.StringIO()
- r = language.parse_response({}, "400:m'msg'")
+ r = language.parse_response("400:m'msg'")
assert language.serve(r, s, {})
+ r = language.parse_response("400:p0,100:dr")
+ assert "p0" in r.spec()
+ s = r.preview_safe()
+ assert "p0" not in s.spec()
+
def test_raw(self):
s = cStringIO.StringIO()
- r = language.parse_response({}, "400:b'foo'")
+ r = language.parse_response("400:b'foo'")
language.serve(r, s, {})
v = s.getvalue()
assert "Content-Length" in v
- assert "Date" in v
s = cStringIO.StringIO()
- r = language.parse_response({}, "400:b'foo':r")
+ r = language.parse_response("400:b'foo':r")
language.serve(r, s, {})
v = s.getvalue()
assert not "Content-Length" in v
- assert not "Date" in v
def test_length(self):
def testlen(x):
s = cStringIO.StringIO()
language.serve(x, s, {})
assert x.length({}) == len(s.getvalue())
- testlen(language.parse_response({}, "400:m'msg':r"))
- testlen(language.parse_response({}, "400:m'msg':h'foo'='bar':r"))
- testlen(language.parse_response({}, "400:m'msg':h'foo'='bar':b@100b:r"))
+ testlen(language.parse_response("400:m'msg':r"))
+ testlen(language.parse_response("400:m'msg':h'foo'='bar':r"))
+ testlen(language.parse_response("400:m'msg':h'foo'='bar':b@100b:r"))
def test_maximum_length(self):
def testlen(x):
@@ -654,74 +727,84 @@ class TestResponse:
language.serve(x, s, {})
assert m >= len(s.getvalue())
- r = language.parse_response({}, "400:m'msg':b@100:d0")
+ r = language.parse_response("400:m'msg':b@100:d0")
testlen(r)
- r = language.parse_response({}, "400:m'msg':b@100:d0:i0,'foo'")
+ r = language.parse_response("400:m'msg':b@100:d0:i0,'foo'")
testlen(r)
- r = language.parse_response({}, "400:m'msg':b@100:d0:i0,'foo'")
+ r = language.parse_response("400:m'msg':b@100:d0:i0,'foo'")
testlen(r)
- def test_render(self):
- r = language.parse_response({}, "400:p0,100:dr")
- assert "p0" in r.spec()
- s = r.preview_safe()
- assert not "p0" in s.spec()
-
def test_parse_err(self):
- tutils.raises(language.ParseException, language.parse_response, {}, "400:msg,b:")
+ tutils.raises(
+ language.ParseException, language.parse_response, "400:msg,b:"
+ )
try:
- language.parse_response({}, "400'msg':b:")
+ language.parse_response("400'msg':b:")
except language.ParseException, v:
assert v.marked()
assert str(v)
def test_nonascii(self):
- tutils.raises("ascii", language.parse_response, {}, "foo:b\xf0")
+ tutils.raises("ascii", language.parse_response, "foo:b\xf0")
def test_parse_header(self):
- r = language.parse_response({}, '400:h"foo"="bar"')
+ r = language.parse_response('400:h"foo"="bar"')
assert utils.get_header("foo", r.headers)
def test_parse_pause_before(self):
- r = language.parse_response({}, "400:p0,10")
+ r = language.parse_response("400:p0,10")
assert r.actions[0].spec() == "p0,10"
def test_parse_pause_after(self):
- r = language.parse_response({}, "400:pa,10")
+ r = language.parse_response("400:pa,10")
assert r.actions[0].spec() == "pa,10"
def test_parse_pause_random(self):
- r = language.parse_response({}, "400:pr,10")
+ r = language.parse_response("400:pr,10")
assert r.actions[0].spec() == "pr,10"
def test_parse_stress(self):
- # While larger values are known to work on linux,
- # len() technically returns an int and a python 2.7 int on windows has 32bit precision.
- # Therefore, we should keep the body length < 2147483647 bytes in our tests.
- r = language.parse_response({}, "400:b@1g")
+ # While larger values are known to work on linux, len() technically
+ # returns an int and a python 2.7 int on windows has 32bit precision.
+ # Therefore, we should keep the body length < 2147483647 bytes in our
+ # tests.
+ r = language.parse_response("400:b@1g")
assert r.length({})
def test_spec(self):
def rt(s):
- s = language.parse_response({}, s).spec()
- assert language.parse_response({}, s).spec() == s
+ s = language.parse_response(s).spec()
+ assert language.parse_response(s).spec() == s
rt("400:b@100g")
rt("400")
rt("400:da")
-
def test_read_file():
tutils.raises(language.FileAccessDenied, language.read_file, {}, "=/foo")
p = tutils.test_data.path("data")
d = dict(staticdir=p)
assert language.read_file(d, "+./file").strip() == "testfile"
assert language.read_file(d, "+file").strip() == "testfile"
- tutils.raises(language.FileAccessDenied, language.read_file, d, "+./nonexistent")
- tutils.raises(language.FileAccessDenied, language.read_file, d, "+/nonexistent")
-
- tutils.raises(language.FileAccessDenied, language.read_file, d, "+../test_language.py")
+ tutils.raises(
+ language.FileAccessDenied,
+ language.read_file,
+ d,
+ "+./nonexistent"
+ )
+ tutils.raises(
+ language.FileAccessDenied,
+ language.read_file,
+ d,
+ "+/nonexistent"
+ )
+ tutils.raises(
+ language.FileAccessDenied,
+ language.read_file,
+ d,
+ "+../test_language.py"
+ )
d["unconstrained_file_access"] = True
assert language.read_file(d, "+../test_language.py")
diff --git a/test/test_pathoc.py b/test/test_pathoc.py
index fe7adc4d..23b42994 100644
--- a/test/test_pathoc.py
+++ b/test/test_pathoc.py
@@ -1,6 +1,8 @@
import json
import cStringIO
-from libpathod import pathoc, test, version, pathod
+import re
+
+from libpathod import pathoc, test, version, pathod, language
import tutils
@@ -18,7 +20,9 @@ class _TestDaemon:
ssl=self.ssl,
ssloptions=self.ssloptions,
staticdir=tutils.test_data.path("data"),
- anchors=[("/anchor/.*", "202")]
+ anchors=[
+ (re.compile("/anchor/.*"), language.parse_response("202"))
+ ]
)
@classmethod
@@ -37,17 +41,29 @@ class _TestDaemon:
r = c.request("get:/api/info")
assert tuple(json.loads(r.content)["version"]) == version.IVERSION
- def tval(self, requests, showreq=False, showresp=False, explain=False,
- showssl=False, hexdump=False, timeout=None, ignorecodes=None,
- ignoretimeout=None):
+ def tval(
+ self,
+ requests,
+ showreq=False,
+ showresp=False,
+ explain=False,
+ showssl=False,
+ hexdump=False,
+ timeout=None,
+ ignorecodes=None,
+ ignoretimeout=None
+ ):
c = pathoc.Pathoc(("127.0.0.1", self.d.port), ssl=self.ssl)
c.connect()
if timeout:
c.settimeout(timeout)
s = cStringIO.StringIO()
for i in requests:
+ r = language.parse_requests(i)[0]
+ if explain:
+ r = r.freeze({})
c.print_request(
- i,
+ r,
showreq = showreq,
showresp = showresp,
explain = explain,
@@ -125,25 +141,17 @@ class TestDaemon(_TestDaemon):
assert "HTTP/" in v
def test_explain(self):
- reqs = [ "get:/p/200:b@100" ]
- assert not "b@100" in self.tval(reqs, explain=True)
+ reqs = ["get:/p/200:b@100"]
+ assert "b@100" not in self.tval(reqs, explain=True)
def test_showreq(self):
- reqs = [ "get:/api/info:p0,0", "get:/api/info:p0,0" ]
+ reqs = ["get:/api/info:p0,0", "get:/api/info:p0,0"]
assert self.tval(reqs, showreq=True).count("unprintables escaped") == 2
assert self.tval(reqs, showreq=True, hexdump=True).count("hex dump") == 2
- def test_parse_err(self):
- assert "Error parsing" in self.tval(["foo"])
-
def test_conn_err(self):
assert "Invalid server response" in self.tval(["get:'/p/200:d2'"])
- def test_fileread(self):
- d = tutils.test_data.path("data/request")
- assert "foo" in self.tval(["+%s"%d], showreq=True)
- assert "File" in self.tval(["+/nonexistent"])
-
def test_connect_fail(self):
to = ("foobar", 80)
c = pathoc.Pathoc(("127.0.0.1", self.d.port))
@@ -157,6 +165,3 @@ class TestDaemon(_TestDaemon):
"HTTP/1.1 200 OK\r\n"
)
c.http_connect(to)
-
-
-
diff --git a/test/test_pathod.py b/test/test_pathod.py
index a98ce2de..8b37b545 100644
--- a/test/test_pathod.py
+++ b/test/test_pathod.py
@@ -1,19 +1,9 @@
-import pprint
from libpathod import pathod, version
-from netlib import tcp, http, certutils
-import requests
+from netlib import tcp, http
import tutils
-class TestPathod:
- def test_instantiation(self):
- p = pathod.Pathod(
- ("127.0.0.1", 0),
- anchors = [(".*", "200:da")]
- )
- assert p.anchors
- tutils.raises("invalid regex", pathod.Pathod, ("127.0.0.1", 0), anchors=[("*", "200:da")])
- tutils.raises("invalid page spec", pathod.Pathod, ("127.0.0.1", 0), anchors=[("foo", "bar")])
+class TestPathod:
def test_logging(self):
p = pathod.Pathod(("127.0.0.1", 0))
assert len(p.get_log()) == 0
@@ -30,6 +20,7 @@ class TestPathod:
class TestNoWeb(tutils.DaemonTests):
noweb = True
+
def test_noweb(self):
assert self.get("200:da").status_code == 200
assert self.getpath("/").status_code == 800
@@ -37,6 +28,7 @@ class TestNoWeb(tutils.DaemonTests):
class TestTimeout(tutils.DaemonTests):
timeout = 0.01
+
def test_noweb(self):
# FIXME: Add float values to spec language, reduce test timeout to
# increase test performance
@@ -46,6 +38,7 @@ class TestTimeout(tutils.DaemonTests):
class TestNoApi(tutils.DaemonTests):
noapi = True
+
def test_noapi(self):
assert self.getpath("/log").status_code == 404
r = self.getpath("/")
@@ -59,7 +52,10 @@ class TestNotAfterConnect(tutils.DaemonTests):
not_after_connect = True
)
def test_connect(self):
- r = self.pathoc(r"get:'http://foo.com/p/202':da", connect_to=("localhost", self.d.port))
+ r = self.pathoc(
+ r"get:'http://foo.com/p/202':da",
+ connect_to=("localhost", self.d.port)
+ )
assert r.status_code == 202
@@ -157,11 +153,15 @@ class CommonTests(tutils.DaemonTests):
assert l["type"] == "error"
assert "foo" in l["msg"]
- def test_invalid_body(self):
- tutils.raises(http.HttpError, self.pathoc, "get:/:h'content-length'='foo'")
+ def test_invalid_content_length(self):
+ tutils.raises(
+ http.HttpError,
+ self.pathoc,
+ "get:/:h'content-length'='foo'"
+ )
l = self.d.last_log()
assert l["type"] == "error"
- assert "Invalid" in l["msg"]
+ assert "Content-Length unknown" in l["msg"]
def test_invalid_headers(self):
tutils.raises(http.HttpError, self.pathoc, "get:/:h'\t'='foo'")
@@ -204,7 +204,7 @@ class TestDaemon(CommonTests):
class TestDaemonSSL(CommonTests):
ssl = True
- def test_ssl_conn_failure(self):
+ def _test_ssl_conn_failure(self):
c = tcp.TCPClient(("localhost", self.d.port))
c.rbufsize = 0
c.wbufsize = 0
@@ -222,4 +222,3 @@ class TestDaemonSSL(CommonTests):
r = self.pathoc(r"get:/p/202")
assert r.status_code == 202
assert self.d.last_log()["cipher"][1] > 0
-
diff --git a/test/test_test.py b/test/test_test.py
index 0a05f1c1..943fb270 100644
--- a/test/test_test.py
+++ b/test/test_test.py
@@ -4,13 +4,18 @@ from libpathod import test
import tutils
logging.disable(logging.CRITICAL)
+
class TestDaemonManual:
def test_simple(self):
with test.Daemon() as d:
rsp = requests.get("http://localhost:%s/p/202:da"%d.port)
assert rsp.ok
assert rsp.status_code == 202
- tutils.raises(requests.ConnectionError, requests.get, "http://localhost:%s/p/202:da"%d.port)
+ tutils.raises(
+ "Connection aborted",
+ requests.get,
+ "http://localhost:%s/p/202:da"%d.port
+ )
def test_startstop_ssl(self):
d = test.Daemon(ssl=True)
@@ -18,7 +23,11 @@ class TestDaemonManual:
assert rsp.ok
assert rsp.status_code == 202
d.shutdown()
- tutils.raises(requests.ConnectionError, requests.get, "http://localhost:%s/p/202:da"%d.port)
+ tutils.raises(
+ "Connection aborted",
+ requests.get,
+ "http://localhost:%s/p/202:da"%d.port
+ )
def test_startstop_ssl_explicit(self):
ssloptions = dict(
@@ -31,6 +40,9 @@ class TestDaemonManual:
assert rsp.ok
assert rsp.status_code == 202
d.shutdown()
- tutils.raises(requests.ConnectionError, requests.get, "http://localhost:%s/p/202:da"%d.port)
-
+ tutils.raises(
+ "Connection aborted",
+ requests.get,
+ "http://localhost:%s/p/202:da"%d.port
+ )
diff --git a/test/tutils.py b/test/tutils.py
index 2c3a2c9d..5876e5e6 100644
--- a/test/tutils.py
+++ b/test/tutils.py
@@ -1,8 +1,12 @@
-import tempfile, os, shutil
+import tempfile
+import os
+import re
+import shutil
from contextlib import contextmanager
-from libpathod import utils, test, pathoc, pathod
+from libpathod import utils, test, pathoc, pathod, language
import requests
+
class DaemonTests:
noweb = False
noapi = False
@@ -11,6 +15,7 @@ class DaemonTests:
timeout = None
hexdump = False
ssloptions = None
+
@classmethod
def setUpAll(self):
opts = self.ssloptions or {}
@@ -19,7 +24,9 @@ class DaemonTests:
so = pathod.SSLOptions(**opts)
self.d = test.Daemon(
staticdir=test_data.path("data"),
- anchors=[("/anchor/.*", "202:da")],
+ anchors=[
+ (re.compile("/anchor/.*"), language.parse_response("202:da"))
+ ],
ssl = self.ssl,
ssloptions = so,
sizelimit=1*1024*1024,
@@ -45,7 +52,13 @@ class DaemonTests:
def getpath(self, path, params=None):
scheme = "https" if self.ssl else "http"
return requests.get(
- "%s://localhost:%s/%s"%(scheme, self.d.port, path), verify=False, params=params
+ "%s://localhost:%s/%s"%(
+ scheme,
+ self.d.port,
+ path
+ ),
+ verify=False,
+ params=params
)
def get(self, spec):