pax_global_header 0000666 0000000 0000000 00000000064 15206666350 0014523 g ustar 00root root 0000000 0000000 52 comment=99e5eed8e3a210b4951ca1ed5cb5575479a25c99
webpy-webpy-99e5eed/ 0000775 0000000 0000000 00000000000 15206666350 0014510 5 ustar 00root root 0000000 0000000 webpy-webpy-99e5eed/.coveragerc 0000664 0000000 0000000 00000000227 15206666350 0016632 0 ustar 00root root 0000000 0000000 [run]
branch = 1
# NOTE: cannot use package easily, without chdir (https://github.com/nedbat/coveragepy/issues/268).
source = web/,tests/
parallel = 1
webpy-webpy-99e5eed/.github/ 0000775 0000000 0000000 00000000000 15206666350 0016050 5 ustar 00root root 0000000 0000000 webpy-webpy-99e5eed/.github/dependabot.yml 0000664 0000000 0000000 00000000447 15206666350 0020705 0 ustar 00root root 0000000 0000000 # Keep GitHub Actions up to date with Dependabot...
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
webpy-webpy-99e5eed/.github/workflows/ 0000775 0000000 0000000 00000000000 15206666350 0020105 5 ustar 00root root 0000000 0000000 webpy-webpy-99e5eed/.github/workflows/lint_python.yml 0000664 0000000 0000000 00000004767 15206666350 0023215 0 ustar 00root root 0000000 0000000 name: ci
on:
pull_request:
push:
branches: [master]
workflow_dispatch:
jobs:
ci:
runs-on: ubuntu-latest
services:
mariadb:
image: mysql:8.0
ports:
- 3306:3306
env:
MYSQL_ROOT_PASSWORD: root
options: --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 5
postgres:
image: postgres:latest
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.x", "3.15", "pypy-3.11"]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- run: pip install "codespell[toml]" pytest ruff
- run: codespell
- run: ruff check --output-format=github
- name: "Install dependent Python modules for testing."
run: pip install -r requirements.txt -r test_requirements.txt
# Use the default MySQL server offered by GitHub Actions.
- name: "Generate /tmp/my.cnf"
run: echo -e "[client]\nhost=127.0.0.1\nport=3306\nuser=root\npassword='root'" > /tmp/my.cnf
- name: "Create MySQL user and database used for testing."
run: mysql --defaults-file=/tmp/my.cnf -e "create user 'scott'@'%' identified by 'tiger'; create database webpy; grant all privileges on webpy.* to 'scott'@'%' with grant option;"
- name: "Create PostgreSQL user and database."
run: |
createdb -h localhost -U postgres webpy
createuser -h localhost -U postgres -d scott
psql -h localhost -U postgres -d postgres -c "ALTER USER scott WITH ENCRYPTED PASSWORD 'tiger'"
psql -h localhost -U postgres -d postgres -c "ALTER DATABASE webpy OWNER TO scott"
env:
PGPASSWORD: postgres
# Run pytest and get detailed output for easy debugging if test failed.
# Env variables `WEBPY_DB_` are required for sql db connections.
- run: pytest --capture=no --exitfirst --verbose .
env:
WEBPY_DB_HOST: 127.0.0.1
WEBPY_DB_MYSQL_PORT: 3306
WEBPY_DB_PG_PORT: 5432
WEBPY_DB_NAME: webpy
WEBPY_DB_USER: scott
WEBPY_DB_PASSWORD: tiger
webpy-webpy-99e5eed/.github/workflows/release.yml 0000664 0000000 0000000 00000001314 15206666350 0022247 0 ustar 00root root 0000000 0000000 name: Release
on:
release:
types: [created] # Only publish on tagged releases
jobs:
pypi-publish:
name: Upload release to PyPI
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/web.py
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Build
run: |
python -m pip install setuptools build
python -m build .
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
webpy-webpy-99e5eed/.gitignore 0000664 0000000 0000000 00000000141 15206666350 0016474 0 ustar 00root root 0000000 0000000 *.egg-info
*.py[cod]
*$py.class
.coverage
.DS_Store
.idea
__pycache__/
build/
dist/
docs/_build/
webpy-webpy-99e5eed/.pre-commit-config.yaml 0000664 0000000 0000000 00000001227 15206666350 0020773 0 ustar 00root root 0000000 0000000 # default_language_version:
# python: python3.7
repos:
- repo: https://github.com/python/black
rev: 24.4.2
hooks:
- id: black
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
hooks:
- id: codespell
additional_dependencies:
- tomli
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.4.2
hooks:
- id: ruff
# args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.23
hooks:
- id: validate-pyproject
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.5.0
hooks:
- id: pyproject-fmt
webpy-webpy-99e5eed/ChangeLog.txt 0000664 0000000 0000000 00000055123 15206666350 0017106 0 ustar 00root root 0000000 0000000 # web.py changelog
## 2023-10-02 0.70
* Remove the cgi module which will be removed in Python 3.13 #773
* Add support for current versions of CPython
* Upgrade testing and linting tools
## 2020-11-09 0.62
* Fixed: application.load() assumes ctx.path will be a latin1 string #687
* Fixed: can not reset session data to same value as initialized. #683
* Fixed: can not set session expire time. #655
* Fixed: not export session store `MemoryStore`.
## 2020-06-23 0.61
* setup.py: Add python_requires='>=3.5' #662
## 2020-06-23 0.60
* Python-2 support has been completely dropped. Welcome to Python 3.
* Fixed: session store `DiskStore` doesn't return correctly if session
directory doesn't exist. #652
* Fixed: incorrect str/bytes type of session data. #644 #645
* Fixed: `db.query("insert... returning")` fails to commit. #648 #649
## 2020-03-20 0.50
* New session store `MemoryStore`, used to save a session in memory.
Should be useful where there are limited fs writes to the disk, like
flash memories. #174
* Fixed: not support `samesite=none`. #592
* Fixed Python-3 compatibility issues: #574, #576.
* Support tuple and set in `sqlquote()`.
* Drop support for SQL driver `pgdb`. It was dead, you cannot even find its
website or download link.
* Drop support for SQL driver `psycopg`. The latest version was released in
2006 (14 years ago), please use `psycopg2` instead.
* Removed function `web.safemarkdown`. if it's used in your application, you
can install the `Markdown` module from pypi
(https://pypi.org/project/Markdown/), then replace `web.safemarkdown()` by
`markdown.markdown()`.
## 2019-09-25 0.40
Note: `0.40` is the last release which supports Python 2. Future releases will
drop support for Python 2.
Broken backward compatibilities:
- `web.utils.utf8` and `web.utf8` (it's an alias of `web.utils.utf8`) were
removed. Please replace them by `web.safestr` instead.
- `db.select()` doesn't support specifying offset in `limit` like this:
`db.select(..., limit="2, 10", ...)` (equals to raw SQL statement
`SELECT ... LIMIT 2, 10`). Please replace them by moving the offset to
`offset` keyword like this: `db.select(..., offset=2, limit=10)`.
Major changes since 0.39:
* Fixed lots of Python-3 compatibility issues.
* Drop support for Python < 2.7.
* Allow to get form data from http PATCH request (fixes #259, tx @kufd)
* Only store new session data if the data is non-default (fixes #161, tx @shish)
* Supports `SameSite` cookie attribute (fixes #61 #99 #337)
* Cookie expire time is now set to same as session `timeout` (fixes #409 #410)
* Supports url for SQLite database like `sqlite:///mydb.sqlite`,
`sqlite:////absolute/path/mydb.sqlite` (fixes #209, tx @iamFIREcracker)
* Allow HTML5 form input elements in `web.form.Input()` (fixes #440, tx @jimgregory)
* Add more form classes for different types: `Email`, `Url`, `Number`, `Range`,
`Color`, `Search`, `Telephone` and `Datalist` (fixes #98 #497, tx @faruken @gjdv)
* Return body for `NoMethod` error handler (fixes #240, tx @waldhol)
* Directory `experimental/` has been removed, it's not used and out of date.
* Module `web/webopenid.py` has been removed, it uses old `python-openid`
module which was released 9 years ago. If you need openid support, consider
`python-openid2` or other packages available on https://pypi.org/.
* Fixed unicode in request url (fixes #461, tx @schneidersoft)
* Fixed inline comment in Templator which leads to unexpected behavior (fixes #432, tx @lucylqe)
* Fixed missing exception (ValueError) for socket.inet_pton to be compatible
with twisted patched `socket.inet_pton` (fixes #464, tx @tclh123)
* Fixed incorrect order of arguments for sending email with boto (fixes #204, tx @asldevi)
* Fixed notfound message is not utf-8 charset (fixes #500, tx @by-z)
* Fixed error in creating pooled PostgresDB with pgdb driver (fixes #255, tx @PriceChild)
* Fixed IP address which contains space should not pass validation (fixes #140, tx @chuangbo)
* Fixed incorrect returned row ids with `multiple_insert()` (fixes #263 #447)
* Fixed not correctly render the `id` attribute after changed (fixes #339, tx @jimgregory)
* Fixed DiskStore concurrency issue (fixes Fixes #83 #182 #191 #289 #470, tx @skawouter)
* Fixed app module isn't picked up by `Reloader` for first code change (fixes #438, tx @jzellman)
## 2018-02-28 0.39
* Fixed a security issue with the form module (tx Orange Tsai)
* Fixed a security issue with the db module (tx Adrián Brav and Orange Tsai)
## 2016-07-08 0.38
* Fixed failing tests in test/session.py when postgres is not installed. (tx Michael Diamond)
* Fixed an error with Python 2.3 (tx Michael Diamond)
* web.database now accepts a URL, $DATABASE_URL (fixes #171) (tx Aaron Swartz, we miss you)
* support port use 'port' as keyword for postgres database with used with pgdb (tx Sandesh Singh)
* Fixes to FirebirdDB database (tx Ben Hanna)
* Added a gaerun method to start application for google app engine (tx Matt Habel)
* Better error message from `db.multiple_insert` when not all rows have the same keys (tx Ben Hoyt)
* Allow custom messages for most errors (tx Shaun Sharples)
* IPv6 support (tx Matthew of Boswell and zamabe)
* Fixed sending email using Amazon SES (tx asldevi)
* Fixed handling of long numbers in sqlify. closes #213. (tx cjrolo)
* Escape HTML characters when emitting API docs. (tx Jeff Zellman)
* Fixed an inconsistency in form.Dropdown when numbers are used for args and value. (tx Noprianto)
* Fixed a potential remote execution risk in `reparam` (tx Adrián Brav)
* The where clause in db queries can be a dict now
* Added `first` method to iterbetter
* Fix to unexpected session when used with MySQL (tx suhashpatil)
* Change dburl2dict to use urlparse and to support the simple case of just a database name. (tx Jeff Zellman)
* Support '204 No Content' status code (tx Matteo Landi)
* Support `451 Unavailable For Legal Reasons` status code(tx Yannik Robin Kettenbach)
* Updates to documentation (tx goodrone, asldevi)
## 2012-06-26 0.37
* Fixed datestr issue on Windows -- #155
* Fixed Python 2.4 compatibility issues (tx fredludlow)
* Fixed error in utils.safewrite (tx shuge) -- #95
* Allow use of web.data() with app.request() -- #105
* Fixed an issue with session initialization (tx beardedprojamz) -- #109
* Allow custom message on 400 Bad Request (tx patryk) -- #121
* Made djangoerror work on GAE. -- #80
* Handle malformatted data in the urls. -- #117
* Made it easier to stop the dev server -- #100, #122
* Added support for customizing cookie_path in session (tx larsga) -- #89
* Added exception for "415 Unsupported Media" (tx JirkaChadima) -- #145
* Added GroupedDropdown to support `
\n')
expression_node = got[0]
assert isinstance(expression_node, ExpressionNode)
assert repr(expression_node) == "$back"
assert got[1] == '">← Back to Index\n'
got = Parser().read_expr('path">$title\n')
expression_node = got[0]
assert isinstance(expression_node, ExpressionNode)
assert repr(expression_node) == "$path"
assert got[1] == '">$title\n'
got = Parser().read_expr("title\n")
expression_node = got[0]
assert isinstance(expression_node, ExpressionNode)
assert repr(expression_node) == "$title"
assert got[1] == "\n"
class TestRender:
def test_template_without_ext(self, tmpdir):
tmpdir.join("foobar").write("hello")
render = web.template.render(str(tmpdir))
assert str(render.foobar()).strip() == "hello"
webpy-webpy-99e5eed/tests/test_utils.py 0000664 0000000 0000000 00000004550 15206666350 0020427 0 ustar 00root root 0000000 0000000 from io import BytesIO
from multipart import parse_form_data
from web import utils
def test_group():
assert list(utils.group([], 2)) == []
assert list(utils.group([1, 2, 3, 4, 5, 6, 7, 8, 9], 3)) == [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
assert list(utils.group([1, 2, 3, 4, 5, 6, 7, 8, 9], 4)) == [
[1, 2, 3, 4],
[5, 6, 7, 8],
[9],
]
class TestIterBetter:
def test_iter(self):
assert list(utils.IterBetter(iter([]))) == []
assert list(utils.IterBetter(iter([1, 2, 3]))) == [1, 2, 3]
class TestStorify:
def test_storify_with_non_files(self):
assert utils.storify({"a": [1, 2]}).a == 2
assert utils.storify({"a": [1, 2]}, a=[]).a == [1, 2]
assert utils.storify({"a": 1}, a=[]).a == [1]
assert utils.storify({}, a=[]).a == []
def test_storify_with_a_unicode_file_value(self):
assert utils.storify({"a": utils.storage(value=1)}).a == 1
assert utils.storify({}, a={}).a == {}
result = utils.storify({"a": utils.storage(value=1)}, a={}).a
assert type(result) is utils.storage
assert result.value == 1
def test_storify_with_a_binary_file_value(self):
# Prepare some raw multipart data with a binary file attachment
binary_file_content = b"\x01\x02\x03\x04\x05"
raw_data = b"""--boundary\r
Content-Disposition: form-data; name="field1"\r
\r
value1\r
--boundary\r
Content-Disposition: form-data; name="field2"\r
\r
value2\r
--boundary\r
Content-Disposition: form-data; name="file"; filename="example.bin"\r
Content-Type: application/octet-stream\r
\r
""" + binary_file_content + b"\r\n--boundary--\r\n"
# Create a BytesIO object from the raw data
buffer = BytesIO(raw_data)
# Parse the multipart data
environ = {
"REQUEST_METHOD": "POST",
"CONTENT_TYPE": "multipart/form-data; boundary=boundary",
"CONTENT_LENGTH": str(len(raw_data)),
"wsgi.input": buffer,
}
_, files = parse_form_data(environ)
# Check if 'file' and 'raw' attributes exist
file_obj = files.get("file")
assert hasattr(file_obj, "file")
assert hasattr(file_obj, "raw")
# Ensure binary content matches.
result = utils.storify({"files": file_obj})
assert result.files == binary_file_content
webpy-webpy-99e5eed/tests/test_wsgi.py 0000664 0000000 0000000 00000003050 15206666350 0020232 0 ustar 00root root 0000000 0000000 import threading
import time
import unittest
from urllib.parse import unquote_to_bytes
import web
class WSGITest(unittest.TestCase):
def test_layers_unicode(self):
urls = ("/", "uni")
class uni:
def GET(self):
return "\u0c05\u0c06"
app = web.application(urls, locals())
thread = threading.Thread(target=app.run)
thread.start()
time.sleep(0.5)
b = web.browser.AppBrowser(app)
r = b.open("/").read()
s = r.decode("utf8")
self.assertEqual(s, "\u0c05\u0c06")
app.stop()
thread.join()
def test_layers_bytes(self):
urls = ("/", "bytes")
class bytes:
def GET(self):
return b"abcdef"
app = web.application(urls, locals())
thread = threading.Thread(target=app.run)
thread.start()
time.sleep(0.5)
b = web.browser.AppBrowser(app)
r = b.open("/")
self.assertEqual(r.read(), b"abcdef")
app.stop()
thread.join()
def test_unicode_url(self):
urls = ("/([^/]+)", "url_passthrough")
class url_passthrough:
def GET(self, arg):
return arg
app = web.application(urls, locals())
thread = threading.Thread(target=app.run)
thread.start()
time.sleep(0.5)
b = web.browser.AppBrowser(app)
r = b.open("/%E2%84%A6")
s = unquote_to_bytes(r.read())
self.assertEqual(s, b"\xe2\x84\xa6")
app.stop()
thread.join()
webpy-webpy-99e5eed/tools/ 0000775 0000000 0000000 00000000000 15206666350 0015650 5 ustar 00root root 0000000 0000000 webpy-webpy-99e5eed/tools/_makedoc.py 0000664 0000000 0000000 00000005013 15206666350 0017763 0 ustar 00root root 0000000 0000000 import os
import web
class Parser:
def __init__(self):
self.mode = "normal"
self.text = ""
def go(self, pyfile):
for line in open(pyfile):
if self.mode == "in def":
self.text += " " + line.strip()
if line.strip().endswith(":"):
if self.definition(self.text):
self.text = ""
self.mode = "in func"
else:
self.text = ""
self.mode = "normal"
elif self.mode == "in func":
if '"""' in line:
self.text += line.strip().strip('"')
self.mode = "in doc"
if line.count('"""') == 2:
self.mode = "normal"
self.docstring(self.text)
self.text = ""
else:
self.mode = "normal"
elif self.mode == "in doc":
self.text += " " + line
if '"""' in line:
self.mode = "normal"
self.docstring(self.text.strip().strip('"'))
self.text = ""
elif line.startswith("## "):
self.header(line.strip().strip("#"))
elif line.startswith("def ") or line.startswith("class "):
self.text += line.strip().strip(":")
if line.strip().endswith(":"):
if self.definition(self.text):
self.text = ""
self.mode = "in func"
else:
self.text = ""
self.mode = "normal"
else:
self.mode = "in def"
def clean(self, text):
text = text.strip()
text = text.replace("*", r"\*")
return text
def definition(self, text):
text = web.lstrips(text, "def ")
if text.startswith("_") or text.startswith("class _"):
return False
print("`" + text.strip() + "`")
return True
def docstring(self, text):
print(" :", text.strip())
print()
def header(self, text):
print("##", text.strip())
print()
for pyfile in os.listdir("web"):
if pyfile[-2:] == "py":
print()
print("## " + pyfile)
print()
Parser().go("web/" + pyfile)
print("`ctx`\n :", end=" ")
print("\n".join(" " + x for x in web.ctx.__doc__.strip().split("\n")))
webpy-webpy-99e5eed/tools/makedoc.py 0000664 0000000 0000000 00000011111 15206666350 0017620 0 ustar 00root root 0000000 0000000 """
Outputs web.py docs as html
version 2.0: documents all code, and indents nicely.
By Colin Rothwell (TheBoff)
"""
import inspect
import sys
import markdown
from web.net import websafe
sys.path.insert(0, "..")
ALL_MODULES = [
"web.application",
"web.contrib.template",
"web.db",
"web.debugerror",
"web.form",
"web.http",
"web.httpserver",
"web.net",
"web.session",
"web.template",
"web.utils",
"web.webapi",
"web.wsgi",
]
item_start = ''
item_end = ""
indent_amount = 30
doc_these = ( # These are the types of object that should be docced
"module",
"classobj",
"instancemethod",
"function",
"type",
"property",
)
not_these_names = ( # Any particular object names that shouldn't be doced
"fget",
"fset",
"fdel",
"storage", # These stop the lower case versions getting docced
"memoize",
"iterbetter",
"capturesstdout",
"profile",
"threadeddict",
"d", # Don't know what this is, but only only conclude it shouldn't be doc'd
)
css = """
"""
indent_start = '
")))
# Although ''.join looks weird, it's a lot faster is string addition
members = ""
if hasattr(ob, "__all__"):
members = ob.__all__
else:
members = [item for item in dir(ob) if not item.startswith("_")]
if "im_class" not in members:
for member_name in members:
recurse_over(getattr(ob, member_name), member_name, indent_level + 1)
if indent_level > 0:
print(indent_end)
def main(modules=None):
modules = modules or ALL_MODULES
print("
") # Stops markdown vandalising my html.
print(css)
print(header)
print("
")
for name in modules:
try:
mod = __import__(name, {}, {}, "x")
recurse_over(mod, name)
except ImportError as e:
print(f"Unable to import module {name} (Error: {e})", file=sys.stderr)
pass
print("
")
if __name__ == "__main__":
main(sys.argv[1:])
webpy-webpy-99e5eed/web/ 0000775 0000000 0000000 00000000000 15206666350 0015265 5 ustar 00root root 0000000 0000000 webpy-webpy-99e5eed/web/__init__.py 0000664 0000000 0000000 00000001206 15206666350 0017375 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
"""web.py: makes web apps (http://webpy.org)"""
# ruff: noqa: F401,F403
from . import (
db,
debugerror,
form,
http,
httpserver,
net,
session,
template,
utils,
webapi,
wsgi,
)
from .application import *
from .db import *
from .debugerror import *
from .http import *
from .httpserver import *
from .net import *
from .utils import *
from .webapi import *
from .wsgi import *
__version__ = "0.76"
__author__ = [
"Aaron Swartz ",
"Anand Chitipothu ",
]
__license__ = "public domain"
__contributors__ = "see http://webpy.org/changes"
webpy-webpy-99e5eed/web/application.py 0000664 0000000 0000000 00000062235 15206666350 0020152 0 ustar 00root root 0000000 0000000 """
Web application
(from web.py)
"""
import itertools
import os
import sys
import traceback
import wsgiref.handlers
from importlib import reload
from inspect import isclass
from io import BytesIO
from urllib.parse import unquote, urlencode, urlparse
from . import browser, httpserver, utils, wsgi
from . import webapi as web
from .debugerror import debugerror
from .py3helpers import iteritems
from .utils import lstrips
__all__ = [
"application",
"auto_application",
"subdir_application",
"subdomain_application",
"loadhook",
"unloadhook",
"autodelegate",
]
class application:
"""
Application to delegate requests based on path.
>>> urls = ("/hello", "hello")
>>> app = application(urls, globals())
>>> class hello:
... def GET(self): return "hello"
>>>
>>> app.request("/hello").data
'hello'
"""
# PY3DOCTEST: b'hello'
def __init__(self, mapping=(), fvars={}, autoreload=None):
if autoreload is None:
autoreload = web.config.get("debug", False)
self.init_mapping(mapping)
self.fvars = fvars
self.processors = []
self.add_processor(loadhook(self._load))
self.add_processor(unloadhook(self._unload))
if autoreload:
def main_module_name():
mod = sys.modules["__main__"]
file = getattr(
mod, "__file__", None
) # make sure this works even from python interpreter
return file and os.path.splitext(os.path.basename(file))[0]
def modname(fvars):
"""find name of the module name from fvars."""
file, name = fvars.get("__file__"), fvars.get("__name__")
if file is None or name is None:
return None
if name == "__main__":
# Since the __main__ module can't be reloaded, the module has
# to be imported using its file name.
name = main_module_name()
return name
mapping_name = utils.dictfind(fvars, mapping)
module_name = modname(fvars)
def reload_mapping():
"""loadhook to reload mapping and fvars."""
mod = __import__(module_name, None, None, [""])
mapping = getattr(mod, mapping_name, None)
if mapping:
self.fvars = mod.__dict__
self.init_mapping(mapping)
self.add_processor(loadhook(Reloader()))
if mapping_name and module_name:
# when app is ran as part of a package, this puts the app into
# `sys.modules` correctly, otherwise the first change to the
# app module will not be picked up by Reloader
reload_mapping()
self.add_processor(loadhook(reload_mapping))
# load __main__ module usings its filename, so that it can be reloaded.
if main_module_name() and "__main__" in sys.argv:
try:
__import__(main_module_name())
except ImportError:
pass
def _load(self):
web.ctx.app_stack.append(self)
def _unload(self):
web.ctx.app_stack = web.ctx.app_stack[:-1]
if web.ctx.app_stack:
# this is a sub-application, revert ctx to earlier state.
oldctx = web.ctx.get("_oldctx")
if oldctx:
web.ctx.home = oldctx.home
web.ctx.homepath = oldctx.homepath
web.ctx.path = oldctx.path
web.ctx.fullpath = oldctx.fullpath
def _cleanup(self):
# Threads can be recycled by WSGI servers.
# Clearing up all thread-local state to avoid interefereing with subsequent requests.
utils.ThreadedDict.clear_all()
def init_mapping(self, mapping):
self.mapping = list(utils.group(mapping, 2))
def add_mapping(self, pattern, classname):
self.mapping.append((pattern, classname))
def add_processor(self, processor):
"""
Adds a processor to the application.
>>> urls = ("/(.*)", "echo")
>>> app = application(urls, globals())
>>> class echo:
... def GET(self, name): return name
...
>>>
>>> def hello(handler): return "hello, " + handler()
...
>>> app.add_processor(hello)
>>> app.request("/web.py").data
'hello, web.py'
"""
# PY3DOCTEST: b'hello, web.py'
self.processors.append(processor)
def request(
self,
localpart="/",
method="GET",
data=None,
host="0.0.0.0:8080",
headers=None,
https=False,
**kw,
):
"""Makes request to this application for the specified path and method.
Response will be a storage object with data, status and headers.
>>> urls = ("/hello", "hello")
>>> app = application(urls, globals())
>>> class hello:
... def GET(self):
... web.header('Content-Type', 'text/plain')
... return "hello"
...
>>> response = app.request("/hello")
>>> response.data
'hello'
>>> response.status
'200 OK'
>>> response.headers['Content-Type']
'text/plain'
To use https, use https=True.
>>> urls = ("/redirect", "redirect")
>>> app = application(urls, globals())
>>> class redirect:
... def GET(self): raise web.seeother("/foo")
...
>>> response = app.request("/redirect")
>>> response.headers['Location']
'http://0.0.0.0:8080/foo'
>>> response = app.request("/redirect", https=True)
>>> response.headers['Location']
'https://0.0.0.0:8080/foo'
The headers argument specifies HTTP headers as a mapping object
such as a dict.
>>> urls = ('/ua', 'uaprinter')
>>> class uaprinter:
... def GET(self):
... return 'your user-agent is ' + web.ctx.env['HTTP_USER_AGENT']
...
>>> app = application(urls, globals())
>>> app.request('/ua', headers = {
... 'User-Agent': 'a small jumping bean/1.0 (compatible)'
... }).data
'your user-agent is a small jumping bean/1.0 (compatible)'
"""
# PY3DOCTEST: b'hello'
# PY3DOCTEST: b'your user-agent is a small jumping bean/1.0 (compatible)'
_p = urlparse(localpart)
path = _p.path
maybe_query = _p.query
query = maybe_query or ""
if "env" in kw:
env = kw["env"]
else:
env = {}
env = dict(
env,
HTTP_HOST=host,
REQUEST_METHOD=method,
PATH_INFO=path,
QUERY_STRING=query,
HTTPS=str(https),
)
headers = headers or {}
for k, v in headers.items():
env["HTTP_" + k.upper().replace("-", "_")] = v
if "HTTP_CONTENT_LENGTH" in env:
env["CONTENT_LENGTH"] = env.pop("HTTP_CONTENT_LENGTH")
if "HTTP_CONTENT_TYPE" in env:
env["CONTENT_TYPE"] = env.pop("HTTP_CONTENT_TYPE")
if method not in ["HEAD", "GET"]:
data = data or ""
if isinstance(data, dict):
q = urlencode(data)
else:
q = data
env["wsgi.input"] = BytesIO(q.encode("utf-8"))
# if not env.get('CONTENT_TYPE', '').lower().startswith('multipart/') and 'CONTENT_LENGTH' not in env:
if "CONTENT_LENGTH" not in env:
env["CONTENT_LENGTH"] = len(q)
response = web.storage()
def start_response(status, headers):
response.status = status
response.headers = dict(headers)
response.header_items = headers
data = self.wsgifunc()(env, start_response)
response.data = b"".join(data)
return response
def browser(self):
return browser.AppBrowser(self)
def handle(self):
fn, args = self._match(self.mapping, web.ctx.path)
return self._delegate(fn, self.fvars, args)
def handle_with_processors(self):
def process(processors):
try:
if processors:
p, processors = processors[0], processors[1:]
return p(lambda: process(processors))
else:
return self.handle()
except web.HTTPError:
raise
except (KeyboardInterrupt, SystemExit):
raise
except:
print(traceback.format_exc(), file=web.debug)
raise self.internalerror()
# processors must be applied in the reverse order. (??)
return process(self.processors)
def wsgifunc(self, *middleware):
"""Returns a WSGI-compatible function for this application."""
def peep(iterator):
"""Peeps into an iterator by doing an iteration
and returns an equivalent iterator.
"""
# wsgi requires the headers first
# so we need to do an iteration
# and save the result for later
try:
firstchunk = next(iterator)
except StopIteration:
firstchunk = ""
return itertools.chain([firstchunk], iterator)
def wsgi(env, start_resp):
# clear threadlocal to avoid interference of previous requests
self._cleanup()
self.load(env)
try:
# allow uppercase methods only
if web.ctx.method.upper() != web.ctx.method:
raise web.nomethod()
result = self.handle_with_processors()
if result and hasattr(result, "__next__"):
result = peep(result)
else:
result = [result]
except web.HTTPError as e:
result = [e.data]
def build_result(result):
for r in result:
if isinstance(r, bytes):
yield r
else:
yield str(r).encode("utf-8")
result = build_result(result)
status, headers = web.ctx.status, web.ctx.headers
start_resp(status, headers)
def cleanup():
self._cleanup()
yield b"" # force this function to be a generator
return itertools.chain(result, cleanup())
for m in middleware:
wsgi = m(wsgi)
return wsgi
def run(self, *middleware):
"""
Starts handling requests. If called in a CGI or FastCGI context, it will follow
that protocol. If called from the command line, it will start an HTTP
server on the port named in the first command line argument, or, if there
is no argument, on port 8080.
`middleware` is a list of WSGI middleware which is applied to the resulting WSGI
function.
"""
return wsgi.runwsgi(self.wsgifunc(*middleware))
def stop(self):
"""Stops the http server started by run."""
if httpserver.server:
httpserver.server.stop()
httpserver.server = None
def cgirun(self, *middleware):
"""
Return a CGI handler. This is mostly useful with Google App Engine.
There you can just do:
main = app.cgirun()
"""
wsgiapp = self.wsgifunc(*middleware)
try:
from google.appengine.ext.webapp.util import run_wsgi_app
return run_wsgi_app(wsgiapp)
except ImportError:
# we're not running from within Google App Engine
return wsgiref.handlers.CGIHandler().run(wsgiapp)
def gaerun(self, *middleware):
"""
Starts the program in a way that will work with Google app engine,
no matter which version you are using (2.5 / 2.7)
If it is 2.5, just normally start it with app.gaerun()
If it is 2.7, make sure to change the app.yaml handler to point to the
global variable that contains the result of app.gaerun()
For example:
in app.yaml (where code.py is where the main code is located)
handlers:
- url: /.*
script: code.app
Make sure that the app variable is globally accessible
"""
wsgiapp = self.wsgifunc(*middleware)
try:
# check what version of python is running
version = sys.version_info[:2]
major = version[0]
minor = version[1]
if major != 2:
raise OSError("Google App Engine only supports python 2.5 and 2.7")
# if 2.7, return a function that can be run by gae
if minor == 7:
return wsgiapp
# if 2.5, use run_wsgi_app
elif minor == 5:
from google.appengine.ext.webapp.util import run_wsgi_app
return run_wsgi_app(wsgiapp)
else:
raise OSError("Not a supported platform, use python 2.5 or 2.7")
except ImportError:
return wsgiref.handlers.CGIHandler().run(wsgiapp)
def load(self, env):
"""Initializes ctx using env."""
ctx = web.ctx
ctx.clear()
ctx.status = "200 OK"
ctx.headers = []
ctx.output = ""
ctx.environ = ctx.env = env
ctx.host = env.get("HTTP_HOST")
if env.get("wsgi.url_scheme") in ["http", "https"]:
ctx.protocol = env["wsgi.url_scheme"]
elif env.get("HTTPS", "").lower() in ["on", "true", "1"]:
ctx.protocol = "https"
else:
ctx.protocol = "http"
ctx.homedomain = ctx.protocol + "://" + env.get("HTTP_HOST", "[unknown]")
ctx.homepath = os.environ.get("REAL_SCRIPT_NAME", env.get("SCRIPT_NAME", ""))
ctx.home = ctx.homedomain + ctx.homepath
# @@ home is changed when the request is handled to a sub-application.
# @@ but the real home is required for doing absolute redirects.
ctx.realhome = ctx.home
ctx.ip = env.get("REMOTE_ADDR")
ctx.method = env.get("REQUEST_METHOD")
try:
ctx.path = bytes(env.get("PATH_INFO"), "latin1").decode("utf8")
except UnicodeDecodeError: # If there are Unicode characters...
ctx.path = env.get("PATH_INFO")
# http://trac.lighttpd.net/trac/ticket/406 requires:
if env.get("SERVER_SOFTWARE", "").startswith(("lighttpd/", "nginx/")):
ctx.path = lstrips(env.get("REQUEST_URI").split("?")[0], ctx.homepath)
# Apache and CherryPy webservers unquote urls but lighttpd and nginx do not.
# Unquote explicitly for lighttpd and nginx to make ctx.path uniform across
# all servers.
ctx.path = unquote(ctx.path)
if env.get("QUERY_STRING"):
ctx.query = "?" + env.get("QUERY_STRING", "")
else:
ctx.query = ""
ctx.fullpath = ctx.path + ctx.query
for k, v in iteritems(ctx):
# convert all string values to unicode values and replace
# malformed data with a suitable replacement marker.
if isinstance(v, bytes):
ctx[k] = v.decode("utf-8", "replace")
# status must always be str
ctx.status = "200 OK"
ctx.app_stack = []
def _delegate(self, f, fvars, args=[]):
def handle_class(cls):
meth = web.ctx.method
if meth == "HEAD" and not hasattr(cls, meth):
meth = "GET"
if not hasattr(cls, meth):
raise web.nomethod(cls)
tocall = getattr(cls(), meth)
return tocall(*args)
if f is None:
raise web.notfound()
elif isinstance(f, application):
return f.handle_with_processors()
elif isclass(f):
return handle_class(f)
elif isinstance(f, str):
if f.startswith("redirect "):
url = f.split(" ", 1)[1]
if web.ctx.method == "GET":
x = web.ctx.env.get("QUERY_STRING", "")
if x:
url += "?" + x
raise web.redirect(url)
elif "." in f:
mod, cls = f.rsplit(".", 1)
mod = __import__(mod, None, None, [""])
cls = getattr(mod, cls)
else:
cls = fvars[f]
return handle_class(cls)
elif hasattr(f, "__call__"):
return f()
else:
return web.notfound()
def _match(self, mapping, value):
for pat, what in mapping:
if isinstance(what, application):
if value.startswith(pat):
f = lambda: self._delegate_sub_application(pat, what)
return f, None
else:
continue
elif isinstance(what, str):
what, result = utils.re_subm(rf"^{pat}\Z", what, value)
else:
result = utils.re_compile(rf"^{pat}\Z").match(value)
if result: # it's a match
return what, [x for x in result.groups()]
return None, None
def _delegate_sub_application(self, dir, app):
"""Deletes request to sub application `app` rooted at the directory `dir`.
The home, homepath, path and fullpath values in web.ctx are updated to mimic request
to the subapp and are restored after it is handled.
@@Any issues with when used with yield?
"""
web.ctx._oldctx = web.storage(web.ctx)
web.ctx.home += dir
web.ctx.homepath += dir
web.ctx.path = web.ctx.path[len(dir) :]
web.ctx.fullpath = web.ctx.fullpath[len(dir) :]
return app.handle_with_processors()
def get_parent_app(self):
if self in web.ctx.app_stack:
index = web.ctx.app_stack.index(self)
if index > 0:
return web.ctx.app_stack[index - 1]
def notfound(self):
"""Returns HTTPError with '404 not found' message"""
parent = self.get_parent_app()
if parent:
return parent.notfound()
else:
return web._NotFound()
def internalerror(self):
"""Returns HTTPError with '500 internal error' message"""
parent = self.get_parent_app()
if parent:
return parent.internalerror()
elif web.config.get("debug"):
return debugerror()
else:
return web._InternalError()
def with_metaclass(mcls):
def decorator(cls):
body = vars(cls).copy()
# clean out class body
body.pop("__dict__", None)
body.pop("__weakref__", None)
return mcls(cls.__name__, cls.__bases__, body)
return decorator
class auto_application(application):
"""Application similar to `application` but urls are constructed
automatically using metaclass.
>>> app = auto_application()
>>> class hello(app.page):
... def GET(self): return "hello, world"
...
>>> class foo(app.page):
... path = '/foo/.*'
... def GET(self): return "foo"
>>> app.request("/hello").data
'hello, world'
>>> app.request('/foo/bar').data
'foo'
"""
# PY3DOCTEST: b'hello, world'
# PY3DOCTEST: b'foo'
def __init__(self):
application.__init__(self)
class metapage(type):
def __init__(klass, name, bases, attrs):
type.__init__(klass, name, bases, attrs)
path = attrs.get("path", "/" + name)
# path can be specified as None to ignore that class
# typically required to create a abstract base class.
if path is not None:
self.add_mapping(path, klass)
@with_metaclass(metapage) # little hack needed for Py2 and Py3 compatibility
class page:
path = None
self.page = page
# The application class already has the required functionality of subdir_application
subdir_application = application
class subdomain_application(application):
r"""
Application to delegate requests based on the host.
>>> urls = ("/hello", "hello")
>>> app = application(urls, globals())
>>> class hello:
... def GET(self): return "hello"
>>>
>>> mapping = (r"hello\.example\.com", app)
>>> app2 = subdomain_application(mapping)
>>> app2.request("/hello", host="hello.example.com").data
'hello'
>>> response = app2.request("/hello", host="something.example.com")
>>> response.status
'404 Not Found'
>>> response.data
'not found'
"""
# PY3DOCTEST: b'hello'
# PY3DOCTEST: b'not found'
def handle(self):
host = web.ctx.host.split(":")[0] # strip port
fn, args = self._match(self.mapping, host)
return self._delegate(fn, self.fvars, args)
def _match(self, mapping, value):
for pat, what in mapping:
if isinstance(what, str):
what, result = utils.re_subm("^" + pat + "$", what, value)
else:
result = utils.re_compile("^" + pat + "$").match(value)
if result: # it's a match
return what, [x for x in result.groups()]
return None, None
def loadhook(h):
"""
Converts a load hook into an application processor.
>>> app = auto_application()
>>> def f(): "something done before handling request"
...
>>> app.add_processor(loadhook(f))
"""
def processor(handler):
h()
return handler()
return processor
def unloadhook(h):
"""
Converts an unload hook into an application processor.
>>> app = auto_application()
>>> def f(): "something done after handling request"
...
>>> app.add_processor(unloadhook(f))
"""
def processor(handler):
try:
result = handler()
except:
# run the hook even when handler raises some exception
h()
raise
if result and hasattr(result, "__next__"):
return wrap(result)
else:
h()
return result
def wrap(result):
def next_hook():
try:
return next(result)
except:
# call the hook at the and of iterator
h()
raise
result = iter(result)
while True:
try:
yield next_hook()
except StopIteration:
return
return processor
def autodelegate(prefix=""):
"""
Returns a method that takes one argument and calls the method named prefix+arg,
calling `notfound()` if there isn't one. Example:
urls = ('/prefs/(.*)', 'prefs')
class prefs:
GET = autodelegate('GET_')
def GET_password(self): pass
def GET_privacy(self): pass
`GET_password` would get called for `/prefs/password` while `GET_privacy` for
`GET_privacy` gets called for `/prefs/privacy`.
If a user visits `/prefs/password/change` then `GET_password(self, '/change')`
is called.
"""
def internal(self, arg):
if "/" in arg:
first, rest = arg.split("/", 1)
func = prefix + first
args = ["/" + rest]
else:
func = prefix + arg
args = []
if hasattr(self, func):
try:
return getattr(self, func)(*args)
except TypeError:
raise web.notfound()
else:
raise web.notfound()
return internal
class Reloader:
"""Checks to see if any loaded modules have changed on disk and,
if so, reloads them.
"""
"""File suffix of compiled modules."""
if sys.platform.startswith("java"):
SUFFIX = "$py.class"
else:
SUFFIX = ".pyc"
def __init__(self):
self.mtimes = {}
def __call__(self):
sys_modules = list(sys.modules.values())
for mod in sys_modules:
self.check(mod)
def check(self, mod):
# jython registers java packages as modules but they either
# don't have a __file__ attribute or its value is None
if not (mod and hasattr(mod, "__file__") and mod.__file__):
return
try:
mtime = os.stat(mod.__file__).st_mtime
except OSError:
return
if mod.__file__.endswith(self.__class__.SUFFIX) and os.path.exists(
mod.__file__[:-1]
):
mtime = max(os.stat(mod.__file__[:-1]).st_mtime, mtime)
if mod not in self.mtimes:
self.mtimes[mod] = mtime
elif self.mtimes[mod] < mtime:
try:
reload(mod)
self.mtimes[mod] = mtime
except ImportError:
pass
if __name__ == "__main__":
import doctest
doctest.testmod()
webpy-webpy-99e5eed/web/browser.py 0000664 0000000 0000000 00000017703 15206666350 0017332 0 ustar 00root root 0000000 0000000 """Browser to test web applications.
(from web.py)
"""
import os
import webbrowser
from http.cookiejar import CookieJar
from io import BytesIO
from urllib.parse import urljoin
from urllib.request import HTTPCookieProcessor, HTTPError, HTTPHandler, Request
from urllib.request import build_opener as urllib_build_opener
from urllib.response import addinfourl
from .net import htmlunquote
from .utils import re_compile
DEBUG = False
__all__ = ["BrowserError", "Browser", "AppBrowser", "AppHandler"]
class BrowserError(Exception):
pass
class Browser:
def __init__(self):
self.cookiejar = CookieJar()
self._cookie_processor = HTTPCookieProcessor(self.cookiejar)
self.form = None
self.url = "http://0.0.0.0:8080/"
self.path = "/"
self.status = None
self.data = None
self._response = None
self._forms = None
@property
def text(self):
return self.data.decode("utf-8")
def reset(self):
"""Clears all cookies and history."""
self.cookiejar.clear()
def build_opener(self):
"""Builds the opener using (urllib2/urllib.request).build_opener.
Subclasses can override this function to provide custom openers.
"""
return urllib_build_opener()
def do_request(self, req):
if DEBUG:
print("requesting", req.get_method(), req.get_full_url())
opener = self.build_opener()
opener.add_handler(self._cookie_processor)
try:
self._response = opener.open(req)
except HTTPError as e:
self._response = e
self.url = self._response.geturl()
self.path = Request(self.url).selector
self.data = self._response.read()
self.status = self._response.code
self._forms = None
self.form = None
return self.get_response()
def open(self, url, data=None, headers={}):
"""Opens the specified url."""
url = urljoin(self.url, url)
req = Request(url, data, headers)
return self.do_request(req)
def show(self):
"""Opens the current page in real web browser."""
f = open("page.html", "w")
f.write(self.data)
f.close()
url = "file://" + os.path.abspath("page.html")
webbrowser.open(url)
def get_response(self):
"""Returns a copy of the current response."""
return addinfourl(
BytesIO(self.data), self._response.info(), self._response.geturl()
)
def get_soup(self):
"""Returns beautiful soup of the current document."""
import BeautifulSoup
return BeautifulSoup.BeautifulSoup(self.data)
def get_text(self, e=None):
"""Returns content of e or the current document as plain text."""
e = e or self.get_soup()
return "".join(
[htmlunquote(c) for c in e.recursiveChildGenerator() if isinstance(c, str)]
)
def _get_links(self):
soup = self.get_soup()
return [a for a in soup.findAll(name="a")]
def get_links(
self, text=None, text_regex=None, url=None, url_regex=None, predicate=None
):
"""Returns all links in the document."""
return self._filter_links(
self._get_links(),
text=text,
text_regex=text_regex,
url=url,
url_regex=url_regex,
predicate=predicate,
)
def follow_link(
self,
link=None,
text=None,
text_regex=None,
url=None,
url_regex=None,
predicate=None,
):
if link is None:
links = self._filter_links(
self.get_links(),
text=text,
text_regex=text_regex,
url=url,
url_regex=url_regex,
predicate=predicate,
)
link = links and links[0]
if link:
return self.open(link["href"])
else:
raise BrowserError("No link found")
def find_link(
self, text=None, text_regex=None, url=None, url_regex=None, predicate=None
):
links = self._filter_links(
self.get_links(),
text=text,
text_regex=text_regex,
url=url,
url_regex=url_regex,
predicate=predicate,
)
return links and links[0] or None
def _filter_links(
self,
links,
text=None,
text_regex=None,
url=None,
url_regex=None,
predicate=None,
):
predicates = []
if text is not None:
predicates.append(lambda link: link.string == text)
if text_regex is not None:
predicates.append(
lambda link: re_compile(text_regex).search(link.string or "")
)
if url is not None:
predicates.append(lambda link: link.get("href") == url)
if url_regex is not None:
predicates.append(
lambda link: re_compile(url_regex).search(link.get("href", ""))
)
if predicate:
predicate.append(predicate)
def f(link):
for p in predicates:
if not p(link):
return False
return True
return [link for link in links if f(link)]
def get_forms(self):
"""Returns all forms in the current document.
The returned form objects implement the ClientForm.HTMLForm interface.
"""
if self._forms is None:
import ClientForm
self._forms = ClientForm.ParseResponse(
self.get_response(), backwards_compat=False
)
return self._forms
def select_form(self, name=None, predicate=None, index=0):
"""Selects the specified form."""
forms = self.get_forms()
if name is not None:
forms = [f for f in forms if f.name == name]
if predicate:
forms = [f for f in forms if predicate(f)]
if forms:
self.form = forms[index]
return self.form
else:
raise BrowserError("No form selected.")
def submit(self, **kw):
"""submits the currently selected form."""
if self.form is None:
raise BrowserError("No form selected.")
req = self.form.click(**kw)
return self.do_request(req)
def __getitem__(self, key):
return self.form[key]
def __setitem__(self, key, value):
self.form[key] = value
class AppBrowser(Browser):
"""Browser interface to test web.py apps.
b = AppBrowser(app)
b.open('/')
b.follow_link(text='Login')
b.select_form(name='login')
b['username'] = 'joe'
b['password'] = 'secret'
b.submit()
assert b.path == '/'
assert 'Welcome joe' in b.get_text()
"""
def __init__(self, app):
Browser.__init__(self)
self.app = app
def build_opener(self):
return urllib_build_opener(AppHandler(self.app))
class AppHandler(HTTPHandler):
"""urllib2 handler to handle requests using web.py application."""
handler_order = 100
https_request = HTTPHandler.do_request_
def __init__(self, app):
self.app = app
def http_open(self, req):
result = self.app.request(
localpart=req.selector,
method=req.get_method(),
host=req.host,
data=req.data,
headers=dict(req.header_items()),
https=(req.type == "https"),
)
return self._make_response(result, req.get_full_url())
def https_open(self, req):
return self.http_open(req)
def _make_response(self, result, url):
data = "\r\n".join([f"{k}: {v}" for k, v in result.header_items])
import email
headers = email.message_from_string(data)
response = addinfourl(BytesIO(result.data), headers, url)
code, msg = result.status.split(None, 1)
response.code, response.msg = int(code), msg
return response
webpy-webpy-99e5eed/web/contrib/ 0000775 0000000 0000000 00000000000 15206666350 0016725 5 ustar 00root root 0000000 0000000 webpy-webpy-99e5eed/web/contrib/__init__.py 0000664 0000000 0000000 00000000000 15206666350 0021024 0 ustar 00root root 0000000 0000000 webpy-webpy-99e5eed/web/contrib/template.py 0000664 0000000 0000000 00000006605 15206666350 0021121 0 ustar 00root root 0000000 0000000 """
Interface to various templating engines.
"""
import os.path
__all__ = ["render_cheetah", "render_genshi", "render_mako", "cache"]
class render_cheetah:
"""Rendering interface to Cheetah Templates.
Example:
render = render_cheetah('templates')
render.hello(name="cheetah")
"""
def __init__(self, path):
# give error if Chetah is not installed
from Cheetah.Template import Template # noqa: F401
self.path = path
def __getattr__(self, name):
from Cheetah.Template import Template
path = os.path.join(self.path, name + ".html")
def template(**kw):
t = Template(file=path, searchList=[kw])
return t.respond()
return template
class render_genshi:
"""Rendering interface genshi templates.
Example:
for xml/html templates.
render = render_genshi(['templates/'])
render.hello(name='genshi')
For text templates:
render = render_genshi(['templates/'], type='text')
render.hello(name='genshi')
"""
def __init__(self, *a, **kwargs):
from genshi.template import TemplateLoader
self._type = kwargs.pop("type", None)
self._loader = TemplateLoader(*a, **kwargs)
def __getattr__(self, name):
# Assuming all templates are html
path = name + ".html"
if self._type == "text":
from genshi.template import TextTemplate
cls = TextTemplate
type = "text"
else:
cls = None
type = self._type
t = self._loader.load(path, cls=cls)
def template(**kw):
stream = t.generate(**kw)
if type:
return stream.render(type)
else:
return stream.render()
return template
class render_jinja:
"""Rendering interface to Jinja2 Templates
Example:
render= render_jinja('templates')
render.hello(name='jinja2')
"""
def __init__(self, *a, **kwargs):
extensions = kwargs.pop("extensions", [])
globals = kwargs.pop("globals", {})
from jinja2 import Environment, FileSystemLoader
self._lookup = Environment(
loader=FileSystemLoader(*a, **kwargs), extensions=extensions
)
self._lookup.globals.update(globals)
def __getattr__(self, name):
# Assuming all templates end with .html
path = name + ".html"
t = self._lookup.get_template(path)
return t.render
class render_mako:
"""Rendering interface to Mako Templates.
Example:
render = render_mako(directories=['templates'])
render.hello(name="mako")
"""
def __init__(self, *a, **kwargs):
from mako.lookup import TemplateLookup
self._lookup = TemplateLookup(*a, **kwargs)
def __getattr__(self, name):
# Assuming all templates are html
path = name + ".html"
t = self._lookup.get_template(path)
return t.render
class cache:
"""Cache for any rendering interface.
Example:
render = cache(render_cheetah("templates/"))
render.hello(name='cache')
"""
def __init__(self, render):
self._render = render
self._cache = {}
def __getattr__(self, name):
if name not in self._cache:
self._cache[name] = getattr(self._render, name)
return self._cache[name]
webpy-webpy-99e5eed/web/db.py 0000664 0000000 0000000 00000150621 15206666350 0016231 0 ustar 00root root 0000000 0000000 """
Database API
(part of web.py)
"""
import ast
import datetime
import os
import re
import time
from urllib.parse import unquote, urlparse
from .py3helpers import iteritems
from .utils import iters, safestr, safeunicode, storage, threadeddict
try:
# db module can work independent of web.py
from .webapi import config, debug
except ImportError:
import sys
debug = sys.stderr
config = storage()
__all__ = [
"UnknownParamstyle",
"UnknownDB",
"TransactionError",
"sqllist",
"sqlors",
"reparam",
"sqlquote",
"SQLQuery",
"SQLParam",
"sqlparam",
"SQLLiteral",
"sqlliteral",
"database",
"DB",
]
TOKEN = "[ \\f\\t]*(\\\\\\r?\\n[ \\f\\t]*)*(#[^\\r\\n]*)?(((\\d+[jJ]|((\\d+\\.\\d*|\\.\\d+)([eE][-+]?\\d+)?|\\d+[eE][-+]?\\d+)[jJ])|((\\d+\\.\\d*|\\.\\d+)([eE][-+]?\\d+)?|\\d+[eE][-+]?\\d+)|(0[xX][\\da-fA-F]+[lL]?|0[bB][01]+[lL]?|(0[oO][0-7]+)|(0[0-7]*)[lL]?|[1-9]\\d*[lL]?))|((\\*\\*=?|>>=?|<<=?|<>|!=|//=?|[+\\-*/%&|^=<>]=?|~)|[][(){}]|(\\r?\\n|[:;.,`@]))|([uUbB]?[rR]?'[^\\n'\\\\]*(?:\\\\.[^\\n'\\\\]*)*'|[uUbB]?[rR]?\"[^\\n\"\\\\]*(?:\\\\.[^\\n\"\\\\]*)*\")|[a-zA-Z_]\\w*)" # noqa: E501
tokenprog = re.compile(TOKEN)
# Supported db drivers.
pg_drivers = ("psycopg2",)
mysql_drivers = ("pymysql", "MySQLdb", "mysql.connector")
sqlite_drivers = ("sqlite3", "pysqlite2.dbapi2", "sqlite")
class UnknownDB(Exception):
"""raised for unsupported dbms"""
pass
class _ItplError(ValueError):
def __init__(self, text, pos):
ValueError.__init__(self)
self.text = text
self.pos = pos
def __str__(self):
return "unfinished expression in %s at char %d" % (repr(self.text), self.pos)
class TransactionError(Exception):
pass
class UnknownParamstyle(Exception):
"""
raised for unsupported db paramstyles
(currently supported: qmark, numeric, format, pyformat)
"""
pass
class SQLParam:
"""
Parameter in SQLQuery.
>>> q = SQLQuery(["SELECT * FROM test WHERE name=", SQLParam("joe")])
>>> q
>>> q.query()
'SELECT * FROM test WHERE name=%s'
>>> q.values()
['joe']
"""
__slots__ = ["value"]
def __init__(self, value):
self.value = value
def get_marker(self, paramstyle="pyformat"):
if paramstyle == "qmark":
return "?"
elif paramstyle == "numeric":
return ":1"
elif paramstyle is None or paramstyle in ["format", "pyformat"]:
return "%s"
raise UnknownParamstyle(paramstyle)
def sqlquery(self):
return SQLQuery([self])
def __add__(self, other):
return self.sqlquery() + other
def __radd__(self, other):
return other + self.sqlquery()
def __str__(self):
return str(self.value)
def __eq__(self, other):
return isinstance(other, SQLParam) and other.value == self.value
def __repr__(self):
return "" % repr(self.value)
sqlparam = SQLParam
class SQLQuery:
"""
You can pass this sort of thing as a clause in any db function.
Otherwise, you can pass a dictionary to the keyword argument `vars`
and the function will call reparam for you.
Internally, consists of `items`, which is a list of strings and
SQLParams, which get concatenated to produce the actual query.
"""
__slots__ = ["items"]
# tested in sqlquote's docstring
def __init__(self, items=None):
r"""Creates a new SQLQuery.
>>> SQLQuery("x")
>>> q = SQLQuery(['SELECT * FROM ', 'test', ' WHERE x=', SQLParam(1)])
>>> q
>>> q.query(), q.values()
('SELECT * FROM test WHERE x=%s', [1])
>>> SQLQuery(SQLParam(1))
"""
if items is None:
self.items = []
elif isinstance(items, list):
self.items = items
elif isinstance(items, SQLParam):
self.items = [items]
elif isinstance(items, SQLQuery):
self.items = list(items.items)
else:
self.items = [items]
# Take care of SQLLiterals
for i, item in enumerate(self.items):
if isinstance(item, SQLParam) and isinstance(item.value, SQLLiteral):
self.items[i] = item.value.v
def append(self, value):
self.items.append(value)
def __add__(self, other):
if isinstance(other, str):
items = [other]
elif isinstance(other, SQLQuery):
items = other.items
else:
return NotImplemented
return SQLQuery(self.items + items)
def __radd__(self, other):
if isinstance(other, str):
items = [other]
elif isinstance(other, SQLQuery):
items = other.items
else:
return NotImplemented
return SQLQuery(items + self.items)
def __iadd__(self, other):
if isinstance(other, (str, SQLParam)):
self.items.append(other)
elif isinstance(other, SQLQuery):
self.items.extend(other.items)
else:
return NotImplemented
return self
def __len__(self):
return len(self.query())
def __eq__(self, other):
return isinstance(other, SQLQuery) and other.items == self.items
def query(self, paramstyle=None):
"""
Returns the query part of the sql query.
>>> q = SQLQuery(["SELECT * FROM test WHERE name=", SQLParam('joe')])
>>> q.query()
'SELECT * FROM test WHERE name=%s'
>>> q.query(paramstyle='qmark')
'SELECT * FROM test WHERE name=?'
"""
s = []
for x in self.items:
if isinstance(x, SQLParam):
x = x.get_marker(paramstyle)
s.append(safestr(x))
else:
x = safestr(x)
# automatically escape % characters in the query
# For backward compatibility, ignore escaping when the query
# looks already escaped
if paramstyle in ["format", "pyformat"]:
if "%" in x and "%%" not in x:
x = x.replace("%", "%%")
s.append(x)
return "".join(s)
def values(self):
"""
Returns the values of the parameters used in the sql query.
>>> q = SQLQuery(["SELECT * FROM test WHERE name=", SQLParam('joe')])
>>> q.values()
['joe']
"""
return [i.value for i in self.items if isinstance(i, SQLParam)]
def join(items, sep=" ", prefix=None, suffix=None, target=None):
"""
Joins multiple queries.
>>> SQLQuery.join(['a', 'b'], ', ')
Optionally, prefix and suffix arguments can be provided.
>>> SQLQuery.join(['a', 'b'], ', ', prefix='(', suffix=')')
If target argument is provided, the items are appended to target
instead of creating a new SQLQuery.
"""
if target is None:
target = SQLQuery()
target_items = target.items
if prefix:
target_items.append(prefix)
for i, item in enumerate(items):
if i != 0 and sep != "":
target_items.append(sep)
if isinstance(item, SQLQuery):
target_items.extend(item.items)
elif item == "": # joins with empty strings
continue
else:
target_items.append(item)
if suffix:
target_items.append(suffix)
return target
join = staticmethod(join)
def _str(self):
try:
return self.query() % tuple(sqlify(x) for x in self.values())
except (ValueError, TypeError):
return self.query()
def __str__(self):
return safestr(self._str())
def __unicode__(self):
return safeunicode(self._str())
def __repr__(self):
return "" % repr(str(self))
class SQLLiteral:
"""
Protects a string from `sqlquote`.
>>> sqlquote('NOW()')
>>> sqlquote(SQLLiteral('NOW()'))
"""
def __init__(self, v):
self.v = v
def __repr__(self):
return "" % self.v
sqlliteral = SQLLiteral
def _sqllist(values):
"""
>>> _sqllist([1, 2, 3])
>>> _sqllist(set([5, 1, 3, 2]))
>>> _sqllist((5, 1, 3, 2, 2, 5))
"""
items = []
items.append("(")
if isinstance(values, set):
values = list(values)
elif isinstance(values, tuple):
values = list(set(values))
for i, v in enumerate(values):
if i != 0:
items.append(", ")
items.append(sqlparam(v))
items.append(")")
return SQLQuery(items)
def reparam(string_, dictionary):
"""
Takes a string and a dictionary and interpolates the string
using values from the dictionary. Returns an `SQLQuery` for the result.
>>> reparam("s = $s", dict(s=True))
>>> reparam("s IN $s", dict(s=[1, 2]))
"""
return SafeEval().safeeval(string_, dictionary)
def sqlify(obj):
"""
converts `obj` to its proper SQL version
>>> sqlify(None)
'NULL'
>>> sqlify(True)
"'t'"
>>> sqlify(3)
'3'
"""
# because `1 == True and hash(1) == hash(True)`
# we have to do this the hard way...
if obj is None:
return "NULL"
elif obj is True:
return "'t'"
elif obj is False:
return "'f'"
elif isinstance(obj, int):
return str(obj)
elif isinstance(obj, datetime.datetime):
return repr(obj.isoformat())
else:
return repr(obj)
def sqllist(lst):
"""
Converts the arguments for use in something like a WHERE clause.
>>> sqllist(['a', 'b'])
'a, b'
>>> sqllist('a')
'a'
"""
if isinstance(lst, str):
return lst
else:
return ", ".join(lst)
def sqlors(left, lst):
"""
`left is a SQL clause like `tablename.arg = `
and `lst` is a list of values. Returns a reparam-style
pair featuring the SQL that ORs together the clause
for each item in the lst.
>>> sqlors('foo = ', [])
>>> sqlors('foo = ', [1])
>>> sqlors('foo = ', 1)
>>> sqlors('foo = ', [1,2,3])
"""
if isinstance(lst, iters):
lst = list(lst)
ln = len(lst)
if ln == 0:
return SQLQuery("1=2")
if ln == 1:
lst = lst[0]
if isinstance(lst, iters):
return SQLQuery(
["("] + sum(([left, sqlparam(x), " OR "] for x in lst), []) + ["1=2)"]
)
else:
return left + sqlparam(lst)
def sqlwhere(data, grouping=" AND "):
"""
Converts a two-tuple (key, value) iterable `data` to an SQL WHERE clause
`SQLQuery`.
>>> sqlwhere((('cust_id', 2), ('order_id',3)))
>>> sqlwhere((('order_id', 3), ('cust_id', 2)), grouping=', ')
>>> sqlwhere((('a', 'a'), ('b', 'b'))).query()
'a = %s AND b = %s'
"""
return SQLQuery.join([k + " = " + sqlparam(v) for k, v in data], grouping)
def sqlquote(a):
"""
Ensures `a` is quoted properly for use in a SQL query.
>>> 'WHERE x = ' + sqlquote(True) + ' AND y = ' + sqlquote(3)
>>> 'WHERE x = ' + sqlquote(True) + ' AND y IN ' + sqlquote([2, 3])
>>> 'WHERE x = ' + sqlquote(True) + ' AND y IN ' + sqlquote(set([3, 2, 3, 4]))
>>> 'WHERE x = ' + sqlquote(True) + ' AND y IN ' + sqlquote((3, 2, 3, 4))
"""
if isinstance(a, (list, tuple, set)):
return _sqllist(a)
else:
return sqlparam(a).sqlquery()
class BaseResultSet:
"""Base implementation of Result Set, the result of a db query."""
def __init__(self, cursor):
self.cursor = cursor
self.names = [x[0] for x in cursor.description]
self._index = 0
def list(self):
rows = [self._prepare_row(d) for d in self.cursor.fetchall()]
self._index += len(rows)
return rows
def _prepare_row(self, row):
return storage(dict(zip(self.names, row)))
def __iter__(self):
return self
def __next__(self):
row = self.cursor.fetchone()
if row is None:
raise StopIteration()
self._index += 1
return self._prepare_row(row)
next = __next__ # for python 2.7 support
def first(self, default=None):
"""Returns the first row of this ResultSet or None when there are no
elements.
If the optional argument default is specified, that is returned instead
of None when there are no elements.
"""
try:
return next(iter(self))
except StopIteration:
return default
def __getitem__(self, i):
# todo: slices
if i < self._index:
raise IndexError("already passed " + str(i))
try:
while i > self._index:
next(self)
self._index += 1
# now self._index == i
self._index += 1
return next(self)
except StopIteration:
raise IndexError(str(i))
class ResultSet(BaseResultSet):
"""The result of a database query."""
def __len__(self):
return int(self.cursor.rowcount)
class SqliteResultSet(BaseResultSet):
"""Result Set for sqlite.
Same functionally as ResultSet except len is not supported.
"""
def __init__(self, cursor):
BaseResultSet.__init__(self, cursor)
self._head = None
def __next__(self):
if self._head is not None:
self._index += 1
return self._head
else:
return super().__next__()
def __bool__(self):
# The ResultSet class class doesn't need to support __bool__ explicitly
# because it has __len__. Since SqliteResultSet doesn't support len,
# we need to peep into the result to find if the result is empty of not.
if self._head is None:
try:
self._head = next(self)
self._index -= 1 # reset the index
except StopIteration:
return False
return True
class Transaction:
"""Database transaction."""
def __init__(self, ctx):
self.ctx = ctx
self.transaction_count = transaction_count = len(ctx.transactions)
class transaction_engine:
"""Transaction Engine used in top level transactions."""
def do_transact(self):
ctx.commit(unload=False)
def do_commit(self):
ctx.commit()
def do_rollback(self):
ctx.rollback()
class subtransaction_engine:
"""Transaction Engine used in sub transactions."""
def query(self, q):
db_cursor = ctx.db.cursor()
ctx.db_execute(db_cursor, SQLQuery(q % transaction_count))
def do_transact(self):
self.query("SAVEPOINT webpy_sp_%s")
def do_commit(self):
self.query("RELEASE SAVEPOINT webpy_sp_%s")
def do_rollback(self):
self.query("ROLLBACK TO SAVEPOINT webpy_sp_%s")
class dummy_engine:
"""Transaction Engine used instead of subtransaction_engine
when sub transactions are not supported."""
do_transact = do_commit = do_rollback = lambda self: None
if self.transaction_count:
# nested transactions are not supported in some databases
if self.ctx.get("ignore_nested_transactions"):
self.engine = dummy_engine()
else:
self.engine = subtransaction_engine()
else:
self.engine = transaction_engine()
self.engine.do_transact()
self.ctx.transactions.append(self)
def __enter__(self):
return self
def __exit__(self, exctype, excvalue, traceback):
if exctype is not None:
self.rollback()
else:
self.commit()
def commit(self):
if len(self.ctx.transactions) > self.transaction_count:
self.engine.do_commit()
self.ctx.transactions = self.ctx.transactions[: self.transaction_count]
def rollback(self):
if len(self.ctx.transactions) > self.transaction_count:
self.engine.do_rollback()
self.ctx.transactions = self.ctx.transactions[: self.transaction_count]
class DB:
"""Database"""
def __init__(self, db_module, keywords):
"""Creates a database."""
# some DB implementations take optional parameter `driver` to use a
# specific driver module but it should not be passed to `connect`.
keywords.pop("driver", None)
self.db_module = db_module
self.keywords = keywords
self._ctx = threadeddict()
# flag to enable/disable printing queries
self.printing = config.get("debug_sql", config.get("debug", False))
self.supports_multiple_insert = False
try:
import dbutils # noqa: F401
# enable pooling if DBUtils module is available.
self.has_pooling = True
except ImportError:
self.has_pooling = False
# Pooling can be disabled by passing pooling=False in the keywords.
self.has_pooling = self.keywords.pop("pooling", True) and self.has_pooling
def _getctx(self):
if not self._ctx.get("db"):
self._load_context(self._ctx)
return self._ctx
ctx = property(_getctx)
def _load_context(self, ctx):
ctx.dbq_count = 0
ctx.transactions = [] # stack of transactions
if self.has_pooling:
ctx.db = self._connect_with_pooling(self.keywords)
else:
ctx.db = self._connect(self.keywords)
ctx.db_execute = self._db_execute
if not hasattr(ctx.db, "commit"):
ctx.db.commit = lambda: None
if not hasattr(ctx.db, "rollback"):
ctx.db.rollback = lambda: None
def commit(unload=True):
# do db commit and release the connection if pooling is enabled.
ctx.db.commit()
if unload and self.has_pooling:
self._unload_context(self._ctx)
def rollback():
# do db rollback and release the connection if pooling is enabled.
ctx.db.rollback()
if self.has_pooling:
self._unload_context(self._ctx)
ctx.commit = commit
ctx.rollback = rollback
def _unload_context(self, ctx):
del ctx.db
def _connect(self, keywords):
return self.db_module.connect(**keywords)
def _connect_with_pooling(self, keywords):
def get_pooled_db():
# In DBUtils 2.0.0, names were made pep8 compliant
# https://webwareforpython.github.io/DBUtils/changelog.html
from dbutils import pooled_db as PooledDB
# In DBUtils 0.9.3, `dbapi` argument is renamed as `creator`
# see Bug#122112
if PooledDB.__version__.split(".") < "0.9.3".split("."):
return PooledDB.PooledDB(dbapi=self.db_module, **keywords)
else:
return PooledDB.PooledDB(creator=self.db_module, **keywords)
if getattr(self, "_pooleddb", None) is None:
self._pooleddb = get_pooled_db()
return self._pooleddb.connection()
def _db_cursor(self):
return self.ctx.db.cursor()
def _param_marker(self):
"""Returns parameter marker based on paramstyle attribute if this database."""
style = getattr(self, "paramstyle", "pyformat")
if style == "qmark":
return "?"
elif style == "numeric":
return ":1"
elif style in ["format", "pyformat"]:
return "%s"
raise UnknownParamstyle(style)
def _db_execute(self, cur, sql_query):
"""executes an sql query"""
self.ctx.dbq_count += 1
try:
a = time.time()
query, params = self._process_query(sql_query)
out = cur.execute(query, params)
b = time.time()
except:
if self.printing:
print("ERR:", str(sql_query), file=debug)
if self.ctx.transactions:
self.ctx.transactions[-1].rollback()
else:
self.ctx.rollback()
raise
if self.printing:
print(
f"{round(b - a, 2)} ({self.ctx.dbq_count}): {str(sql_query)}",
file=debug,
)
return out
def _process_query(self, sql_query):
"""Takes the SQLQuery object and returns query string and parameters."""
paramstyle = getattr(self, "paramstyle", "pyformat")
query = sql_query.query(paramstyle)
params = sql_query.values()
return query, params
def _where(self, where, vars):
if isinstance(where, int):
where = "id = " + sqlparam(where)
# @@@ for backward-compatibility
elif isinstance(where, (list, tuple)) and len(where) == 2:
where = SQLQuery(where[0], where[1])
elif isinstance(where, dict):
where = self._where_dict(where)
elif isinstance(where, SQLQuery):
pass
else:
where = reparam(where, vars)
return where
def _where_dict(self, where):
where_clauses = []
for k, v in sorted(iteritems(where), key=lambda t: t[0]):
where_clauses.append(k + " = " + sqlquote(v))
if where_clauses:
return SQLQuery.join(where_clauses, " AND ")
else:
return None
def query(self, sql_query, vars=None, processed=False, _test=False):
"""
Execute SQL query `sql_query` using dictionary `vars` to interpolate it.
If `processed=True`, `vars` is a `reparam`-style list to use
instead of interpolating.
>>> db = DB(None, {})
>>> db.query("SELECT * FROM foo", _test=True)
>>> db.query("SELECT * FROM foo WHERE x = $x", vars=dict(x='f'), _test=True)
>>> db.query("SELECT * FROM foo WHERE x = " + sqlquote('f'), _test=True)
"""
if vars is None:
vars = {}
if not processed and not isinstance(sql_query, SQLQuery):
sql_query = reparam(sql_query, vars)
if _test:
return sql_query
db_cursor = self._db_cursor()
self._db_execute(db_cursor, sql_query)
if db_cursor.description:
out = self.create_result_set(db_cursor)
else:
out = db_cursor.rowcount
if not self.ctx.transactions:
self.ctx.commit()
return out
def create_result_set(self, cursor):
return ResultSet(cursor)
def select(
self,
tables,
vars=None,
what="*",
where=None,
order=None,
group=None,
limit=None,
offset=None,
_test=False,
):
"""
Selects `what` from `tables` with clauses `where`, `order`,
`group`, `limit`, and `offset`. Uses vars to interpolate.
Otherwise, each clause can be a SQLQuery.
>>> db = DB(None, {})
>>> db.select('foo', _test=True)
>>> db.select(['foo', 'bar'], where="foo.bar_id = bar.id", limit=5, _test=True)
>>> db.select('foo', where={'id': 5}, _test=True)
"""
if vars is None:
vars = {}
sql_clauses = self.sql_clauses(what, tables, where, group, order, limit, offset)
clauses = [
self.gen_clause(sql, val, vars)
for sql, val in sql_clauses
if val is not None
]
qout = SQLQuery.join(clauses)
if _test:
return qout
return self.query(qout, processed=True)
def where(
self,
table,
what="*",
order=None,
group=None,
limit=None,
offset=None,
_test=False,
**kwargs,
):
"""
Selects from `table` where keys are equal to values in `kwargs`.
>>> db = DB(None, {})
>>> db.where('foo', bar_id=3, _test=True)
>>> db.where('foo', source=2, crust='dewey', _test=True)
>>> db.where('foo', _test=True)
"""
where = self._where_dict(kwargs)
return self.select(
table,
what=what,
order=order,
group=group,
limit=limit,
offset=offset,
_test=_test,
where=where,
)
def sql_clauses(self, what, tables, where, group, order, limit, offset):
return (
("SELECT", what),
("FROM", sqllist(tables)),
("WHERE", where),
("GROUP BY", group),
("ORDER BY", order),
# The limit and offset could be the values provided by
# the end-user and are potentially unsafe.
# Using them as parameters to avoid any risk.
("LIMIT", limit and SQLParam(limit).sqlquery()),
("OFFSET", offset and SQLParam(offset).sqlquery()),
)
def gen_clause(self, sql, val, vars):
if isinstance(val, int):
if sql == "WHERE":
nout = "id = " + sqlquote(val)
else:
nout = SQLQuery(val)
# @@@
elif isinstance(val, (list, tuple)) and len(val) == 2:
nout = SQLQuery(val[0], val[1]) # backwards-compatibility
elif sql == "WHERE" and isinstance(val, dict):
nout = self._where_dict(val)
elif isinstance(val, SQLQuery):
nout = val
else:
nout = reparam(val, vars)
def xjoin(a, b):
if a and b:
return a + " " + b
else:
return a or b
return xjoin(sql, nout)
def insert(self, tablename, seqname=None, _test=False, **values):
"""
Inserts `values` into `tablename`. Returns current sequence ID.
Set `seqname` to the ID if it's not the default, or to `False`
if there isn't one.
>>> db = DB(None, {})
>>> q = db.insert('foo', name='bob', age=2, created=SQLLiteral('NOW()'), _test=True)
>>> q
>>> q.query()
'INSERT INTO foo (age, created, name) VALUES (%s, NOW(), %s)'
>>> q.values()
[2, 'bob']
"""
def q(x):
return "(" + x + ")"
if values:
# needed for Py3 compatibility with the above doctests
sorted_values = sorted(values.items(), key=lambda t: t[0])
_keys = SQLQuery.join(map(lambda t: t[0], sorted_values), ", ")
_values = SQLQuery.join(
[sqlparam(v) for v in map(lambda t: t[1], sorted_values)], ", "
)
sql_query = (
"INSERT INTO %s " % tablename + q(_keys) + " VALUES " + q(_values)
)
else:
sql_query = SQLQuery(self._get_insert_default_values_query(tablename))
if _test:
return sql_query
db_cursor = self._db_cursor()
if seqname is not False:
sql_query = self._process_insert_query(sql_query, tablename, seqname)
if isinstance(sql_query, tuple):
# for some databases, a separate query has to be made to find
# the id of the inserted row.
q1, q2 = sql_query
self._db_execute(db_cursor, q1)
self._db_execute(db_cursor, q2)
else:
self._db_execute(db_cursor, sql_query)
try:
out = db_cursor.fetchone()[0]
except Exception:
out = None
if not self.ctx.transactions:
self.ctx.commit()
return out
def _get_insert_default_values_query(self, table):
return "INSERT INTO %s DEFAULT VALUES" % table
def multiple_insert(self, tablename, values, seqname=None, _test=False):
"""
Inserts multiple rows into `tablename`. The `values` must be a list of
dictionaries, one for each row to be inserted, each with the same set
of keys. Returns the list of ids of the inserted rows.
Set `seqname` to the ID if it's not the default, or to `False`
if there isn't one.
>>> db = DB(None, {})
>>> db.supports_multiple_insert = True
>>> values = [{"name": "foo", "email": "foo@example.com"}, {"name": "bar", "email": "bar@example.com"}]
>>> db.multiple_insert('person', values=values, _test=True)
"""
if not values:
return []
if not self.supports_multiple_insert:
out = [
self.insert(tablename, seqname=seqname, _test=_test, **v)
for v in values
]
if seqname is False:
return None
else:
return out
keys = values[0].keys()
# @@ make sure all keys are valid
for v in values:
if v.keys() != keys:
raise ValueError("Not all rows have the same keys")
# enforce query order for the above doctest compatibility with Py3
keys = sorted(keys)
sql_query = SQLQuery(
"INSERT INTO {} ({}) VALUES ".format(tablename, ", ".join(keys))
)
for i, row in enumerate(values):
if i != 0:
sql_query.append(", ")
SQLQuery.join(
[SQLParam(row[k]) for k in keys],
sep=", ",
target=sql_query,
prefix="(",
suffix=")",
)
if _test:
return sql_query
db_cursor = self._db_cursor()
if seqname is not False:
sql_query = self._process_insert_query(sql_query, tablename, seqname)
if isinstance(sql_query, tuple):
# for some databases, a separate query has to be made to find
# the id of the inserted row.
q1, q2 = sql_query
self._db_execute(db_cursor, q1)
self._db_execute(db_cursor, q2)
else:
self._db_execute(db_cursor, sql_query)
try:
out = db_cursor.fetchone()[0]
# MySQL gives the first id of multiple inserted rows.
# PostgreSQL and SQLite give the last id.
if self.db_module.__name__ in mysql_drivers:
out = range(out, out + len(values))
else:
out = range(out - len(values) + 1, out + 1)
except Exception:
out = None
if not self.ctx.transactions:
self.ctx.commit()
return out
def update(self, tables, where, vars=None, _test=False, **values):
"""
Update `tables` with clause `where` (interpolated using `vars`)
and setting `values`.
>>> db = DB(None, {})
>>> name = 'Joseph'
>>> q = db.update('foo', where='name = $name', name='bob', age=2,
... created=SQLLiteral('NOW()'), vars=locals(), _test=True)
>>> q
>>> q.query()
'UPDATE foo SET age = %s, created = NOW(), name = %s WHERE name = %s'
>>> q.values()
[2, 'bob', 'Joseph']
"""
if vars is None:
vars = {}
where = self._where(where, vars)
values = sorted(values.items(), key=lambda t: t[0])
query = (
"UPDATE "
+ sqllist(tables)
+ " SET "
+ sqlwhere(values, ", ")
+ " WHERE "
+ where
)
if _test:
return query
db_cursor = self._db_cursor()
self._db_execute(db_cursor, query)
if not self.ctx.transactions:
self.ctx.commit()
return db_cursor.rowcount
def delete(self, table, where, using=None, vars=None, _test=False):
"""
Deletes from `table` with clauses `where` and `using`.
>>> db = DB(None, {})
>>> name = 'Joe'
>>> db.delete('foo', where='name = $name', vars=locals(), _test=True)
"""
if vars is None:
vars = {}
where = self._where(where, vars)
q = "DELETE FROM " + table
if using:
q += " USING " + sqllist(using)
if where:
q += " WHERE " + where
if _test:
return q
db_cursor = self._db_cursor()
self._db_execute(db_cursor, q)
if not self.ctx.transactions:
self.ctx.commit()
return db_cursor.rowcount
def _process_insert_query(self, query, tablename, seqname):
return query
def transaction(self):
"""Start a transaction."""
return Transaction(self.ctx)
class PostgresDB(DB):
"""Postgres driver."""
def __init__(self, **keywords):
if "pw" in keywords:
keywords["password"] = keywords.pop("pw")
db_module = import_driver(pg_drivers, preferred=keywords.pop("driver", None))
if db_module.__name__ == "psycopg2":
import psycopg2.extensions
psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
# if db is not provided `postgres` driver will take it from PGDATABASE
# environment variable.
if "db" in keywords:
keywords["database"] = keywords.pop("db")
self.dbname = "postgres"
self.paramstyle = db_module.paramstyle
DB.__init__(self, db_module, keywords)
self.supports_multiple_insert = True
self._sequences = None
def _process_insert_query(self, query, tablename, seqname):
if seqname is None:
# when seqname is not provided guess the seqname and make sure it exists
seqname = tablename + "_id_seq"
if seqname not in self._get_all_sequences():
seqname = None
if seqname:
query += self.get_sequence_query(seqname)
return query
def get_sequence_query(self, seqname):
import re
# Ensure the sequence name is valid
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_$]*$", seqname):
raise ValueError(f"Invalid sequence name: {seqname}")
return SQLQuery(f"; SELECT currval('{seqname}')")
def _get_all_sequences(self):
"""Query postgres to find names of all sequences used in this database."""
if self._sequences is None:
q = "SELECT c.relname FROM pg_class c WHERE c.relkind = 'S'"
self._sequences = {c.relname for c in self.query(q)}
return self._sequences
def _connect(self, keywords):
conn = DB._connect(self, keywords)
conn.set_client_encoding("UTF8")
return conn
def _connect_with_pooling(self, keywords):
conn = DB._connect_with_pooling(self, keywords)
conn._con._con.set_client_encoding("UTF8")
return conn
class MySQLDB(DB):
def __init__(self, **keywords):
db = import_driver(mysql_drivers, preferred=keywords.pop("driver", None))
if db.__name__ == "pymysql":
if "pw" in keywords:
keywords["password"] = keywords["pw"]
del keywords["pw"]
elif db.__name__ == "MySQLdb":
if "pw" in keywords:
keywords["passwd"] = keywords.pop("pw")
elif db.__name__ == "mysql.connector":
# Enabled buffered so that len can work as expected.
keywords.setdefault("buffered", True)
if "pw" in keywords:
keywords["password"] = keywords["pw"]
del keywords["pw"]
if "charset" not in keywords:
keywords["charset"] = "utf8"
elif keywords["charset"] is None:
del keywords["charset"]
self.paramstyle = db.paramstyle = "pyformat" # it's both
self.dbname = "mysql"
DB.__init__(self, db, keywords)
self.supports_multiple_insert = True
def _process_insert_query(self, query, tablename, seqname):
return query, SQLQuery("SELECT last_insert_id();")
def _get_insert_default_values_query(self, table):
return "INSERT INTO %s () VALUES()" % table
def import_driver(drivers, preferred=None):
"""Import the first available driver or preferred driver."""
if preferred:
drivers = (preferred,)
for d in drivers:
try:
return __import__(d, None, None, ["x"])
except ImportError:
pass
raise ImportError("Unable to import " + " or ".join(drivers))
class SqliteDB(DB):
def __init__(self, **keywords):
db = import_driver(sqlite_drivers, preferred=keywords.pop("driver", None))
if db.__name__ in ["sqlite3", "pysqlite2.dbapi2"]:
db.paramstyle = "qmark"
# sqlite driver doesn't create datatime objects for timestamp columns
# unless `detect_types` option is passed.
# It seems to be supported in `sqlite3` and `pysqlite2` drivers, not
# surte about `sqlite`.
keywords.setdefault("detect_types", db.PARSE_DECLTYPES)
self.dbname = "sqlite"
self.paramstyle = db.paramstyle
keywords["database"] = keywords.pop("db")
# sqlite don't allows connections to be shared by threads
keywords["pooling"] = False
DB.__init__(self, db, keywords)
def _process_insert_query(self, query, tablename, seqname):
return query, SQLQuery("SELECT last_insert_rowid();")
def create_result_set(self, cursor):
return SqliteResultSet(cursor)
class FirebirdDB(DB):
"""Firebird Database."""
def __init__(self, **keywords):
try:
import kinterbasdb as db
except Exception:
db = None
pass
if "pw" in keywords:
keywords["password"] = keywords.pop("pw")
keywords["database"] = keywords.pop("db")
self.paramstyle = db.paramstyle
DB.__init__(self, db, keywords)
def delete(self, table, where=None, using=None, vars=None, _test=False):
# firebird doesn't support using clause
using = None
return DB.delete(self, table, where, using, vars, _test)
def sql_clauses(self, what, tables, where, group, order, limit, offset):
return (
("SELECT", ""),
("FIRST", limit),
("SKIP", offset),
("", what),
("FROM", sqllist(tables)),
("WHERE", where),
("GROUP BY", group),
("ORDER BY", order),
)
class MSSQLDB(DB):
def __init__(self, **keywords):
import pymssql as db
if "pw" in keywords:
keywords["password"] = keywords.pop("pw")
keywords["database"] = keywords.pop("db")
self.dbname = "mssql"
DB.__init__(self, db, keywords)
def _process_query(self, sql_query):
"""Takes the SQLQuery object and returns query string and parameters."""
# MSSQLDB expects params to be a tuple.
# Overwriting the default implementation to convert params to tuple.
paramstyle = getattr(self, "paramstyle", "pyformat")
query = sql_query.query(paramstyle)
params = sql_query.values()
return query, tuple(params)
def sql_clauses(self, what, tables, where, group, order, limit, offset):
return (
("SELECT", what),
("TOP", limit),
("FROM", sqllist(tables)),
("WHERE", where),
("GROUP BY", group),
("ORDER BY", order),
("OFFSET", offset),
)
def _test(self):
"""Test LIMIT.
Fake presence of pymssql module for running tests.
>>> import sys
>>> sys.modules['pymssql'] = sys.modules['sys']
MSSQL has TOP clause instead of LIMIT clause.
>>> db = MSSQLDB(db='test', user='joe', pw='secret')
>>> db.select('foo', limit=4, _test=True)
"""
pass
class OracleDB(DB):
def __init__(self, **keywords):
import cx_Oracle as db
if "pw" in keywords:
keywords["password"] = keywords.pop("pw")
# @@ TODO: use db.makedsn if host, port is specified
keywords["dsn"] = keywords.pop("db")
self.dbname = "oracle"
db.paramstyle = "numeric"
self.paramstyle = db.paramstyle
# oracle doesn't support pooling
keywords.pop("pooling", None)
DB.__init__(self, db, keywords)
def _process_insert_query(self, query, tablename, seqname):
if seqname is None:
# It is not possible to get seq name from table name in Oracle
return query
else:
return query + "; SELECT %s.currval FROM dual" % seqname
def dburl2dict(url):
"""
Takes a URL to a database and parses it into an equivalent dictionary.
>>> dburl2dict('postgres:///mygreatdb') == {'pw': None, 'dbn': 'postgres', 'db': 'mygreatdb', 'host': None, 'user': None, 'port': None}
True
>>> dburl2dict('postgres://james:day@serverfarm.example.net:5432/mygreatdb') == {'pw': 'day', 'dbn': 'postgres', 'db': 'mygreatdb', 'host': 'serverfarm.example.net', 'user': 'james', 'port': 5432}
True
>>> dburl2dict('postgres://james:day@serverfarm.example.net/mygreatdb') == {'pw': 'day', 'dbn': 'postgres', 'db': 'mygreatdb', 'host': 'serverfarm.example.net', 'user': 'james', 'port': None}
True
>>> dburl2dict('postgres://james:d%40y@serverfarm.example.net/mygreatdb') == {'pw': 'd@y', 'dbn': 'postgres', 'db': 'mygreatdb', 'host': 'serverfarm.example.net', 'user': 'james', 'port': None}
True
>>> dburl2dict('mysql://james:d%40y@serverfarm.example.net/mygreatdb') == {'pw': 'd@y', 'dbn': 'mysql', 'db': 'mygreatdb', 'host': 'serverfarm.example.net', 'user': 'james', 'port': None}
True
>>> dburl2dict('sqlite:///mygreatdb.db')
{'db': 'mygreatdb.db', 'dbn': 'sqlite'}
>>> dburl2dict('sqlite:////absolute/path/mygreatdb.db')
{'db': '/absolute/path/mygreatdb.db', 'dbn': 'sqlite'}
"""
parts = urlparse(unquote(url))
if parts.scheme == "sqlite":
return {"dbn": parts.scheme, "db": parts.path[1:]}
else:
return {
"dbn": parts.scheme,
"user": parts.username,
"pw": parts.password,
"db": parts.path[1:],
"host": parts.hostname,
"port": parts.port,
}
_databases = {}
def database(dburl=None, **params):
"""Creates appropriate database using params.
Pooling will be enabled if DBUtils module is available.
Pooling can be disabled by passing pooling=False in params.
"""
if not dburl and not params:
dburl = os.environ["DATABASE_URL"]
if dburl:
params = dburl2dict(dburl)
dbn = params.pop("dbn")
if dbn in _databases:
return _databases[dbn](**params)
else:
raise UnknownDB(dbn)
def register_database(name, clazz):
"""
Register a database.
>>> class LegacyDB(DB):
... def __init__(self, **params):
... pass
...
>>> register_database('legacy', LegacyDB)
>>> db = database(dbn='legacy', db='test', user='joe', passwd='secret')
"""
_databases[name] = clazz
register_database("mysql", MySQLDB)
register_database("postgres", PostgresDB)
register_database("sqlite", SqliteDB)
register_database("firebird", FirebirdDB)
register_database("mssql", MSSQLDB)
register_database("oracle", OracleDB)
def _interpolate(format):
"""
Takes a format string and returns a list of 2-tuples of the form
(boolean, string) where boolean says whether string should be evaled
or not.
from (public domain, Ka-Ping Yee)
"""
def matchorfail(text, pos):
match = tokenprog.match(text, pos)
if match is None:
raise _ItplError(text, pos)
return match, match.end()
namechars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
chunks = []
pos = 0
while 1:
dollar = format.find("$", pos)
if dollar < 0:
break
nextchar = format[dollar + 1]
if nextchar == "{":
chunks.append((0, format[pos:dollar]))
pos, level = dollar + 2, 1
while level:
match, pos = matchorfail(format, pos)
tstart, tend = match.regs[3]
token = format[tstart:tend]
if token == "{":
level = level + 1
elif token == "}":
level = level - 1
chunks.append((1, format[dollar + 2 : pos - 1]))
elif nextchar in namechars:
chunks.append((0, format[pos:dollar]))
match, pos = matchorfail(format, dollar + 1)
while pos < len(format):
if (
format[pos] == "."
and pos + 1 < len(format)
and format[pos + 1] in namechars
):
match, pos = matchorfail(format, pos + 1)
elif format[pos] in "([":
pos, level = pos + 1, 1
while level:
match, pos = matchorfail(format, pos)
tstart, tend = match.regs[3]
token = format[tstart:tend]
if token[0] in "([":
level = level + 1
elif token[0] in ")]":
level = level - 1
else:
break
chunks.append((1, format[dollar + 1 : pos]))
else:
chunks.append((0, format[pos : dollar + 1]))
pos = dollar + 1 + (nextchar == "$")
if pos < len(format):
chunks.append((0, format[pos:]))
return chunks
class _Node:
def __init__(self, type, first, second=None):
self.type = type
self.first = first
self.second = second
def __eq__(self, other):
return (
isinstance(other, _Node)
and self.type == other.type
and self.first == other.first
and self.second == other.second
)
def __repr__(self):
return f"Node({self.type!r}, {self.first!r}, {self.second!r})"
class Parser:
"""Parser to parse string templates like "Hello $name".
Loosely based on (public domain, Ka-Ping Yee)
"""
namechars = "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
def __init__(self):
self.reset()
def reset(self):
self.pos = 0
self.level = 0
self.text = ""
def parse(self, text):
"""Parses the given text and returns a parse tree."""
self.reset()
self.text = text
return self.parse_all()
def parse_all(self):
while True:
dollar = self.text.find("$", self.pos)
if dollar < 0:
break
nextchar = self.text[dollar + 1]
if nextchar in self.namechars:
yield _Node("text", self.text[self.pos : dollar])
self.pos = dollar + 1
yield self.parse_expr()
# for supporting ${x.id}, for backward compatibility
elif nextchar == "{":
saved_pos = self.pos
self.pos = dollar + 2 # skip "${"
expr = self.parse_expr()
if self.text[self.pos] == "}":
self.pos += 1
yield _Node("text", self.text[self.pos : dollar])
yield expr
else:
self.pos = saved_pos
break
else:
yield _Node("text", self.text[self.pos : dollar + 1])
self.pos = dollar + 1
# $$ is used to escape $
if nextchar == "$":
self.pos += 1
if self.pos < len(self.text):
yield _Node("text", self.text[self.pos :])
def match(self):
match = tokenprog.match(self.text, self.pos)
if match is None:
raise _ItplError(self.text, self.pos)
return match, match.end()
def is_literal(self, text):
return text and text[0] in "0123456789\"'"
def parse_expr(self):
match, pos = self.match()
if self.is_literal(match.group()):
expr = _Node("literal", match.group())
else:
expr = _Node("param", self.text[self.pos : pos])
self.pos = pos
while self.pos < len(self.text):
if (
self.text[self.pos] == "."
and self.pos + 1 < len(self.text)
and self.text[self.pos + 1] in self.namechars
):
self.pos += 1
match, pos = self.match()
attr = match.group()
expr = _Node("getattr", expr, attr)
self.pos = pos
elif self.text[self.pos] == "[":
saved_pos = self.pos
self.pos += 1
key = self.parse_expr()
if self.text[self.pos] == "]":
self.pos += 1
expr = _Node("getitem", expr, key)
else:
self.pos = saved_pos
break
else:
break
return expr
class SafeEval:
"""Safe evaluator for binding params to db queries."""
def safeeval(self, text, mapping):
nodes = Parser().parse(text)
return SQLQuery.join([self.eval_node(node, mapping) for node in nodes], "")
def eval_node(self, node, mapping):
if node.type == "text":
return node.first
else:
return sqlquote(self.eval_expr(node, mapping))
def eval_expr(self, node, mapping):
if node.type == "literal":
return ast.literal_eval(node.first)
elif node.type == "getattr":
return getattr(self.eval_expr(node.first, mapping), node.second)
elif node.type == "getitem":
return self.eval_expr(node.first, mapping)[
self.eval_expr(node.second, mapping)
]
elif node.type == "param":
return mapping[node.first]
def test_parser():
def f(text, expected):
p = Parser()
nodes = list(p.parse(text))
print(repr(text), nodes)
assert nodes == expected, "Expected %r" % expected
f("Hello", [_Node("text", "Hello")])
f("Hello $name", [_Node("text", "Hello "), _Node("param", "name")])
f(
"Hello $name.foo",
[_Node("text", "Hello "), _Node("getattr", _Node("param", "name"), "foo")],
)
f(
"WHERE id=$self.id LIMIT 1",
[
_Node("text", "WHERE id="),
_Node("getattr", _Node("param", "self", None), "id"),
_Node("text", " LIMIT 1"),
],
)
f(
"WHERE id=$self['id'] LIMIT 1",
[
_Node("text", "WHERE id="),
_Node("getitem", _Node("param", "self", None), _Node("literal", "'id'")),
_Node("text", " LIMIT 1"),
],
)
def test_safeeval():
def f(q, vars):
return SafeEval().safeeval(q, vars)
print(f("WHERE id=$id", {"id": 1}).items)
assert f("WHERE id=$id", {"id": 1}).items == ["WHERE id=", sqlparam(1)]
if __name__ == "__main__":
import doctest
doctest.testmod()
test_parser()
test_safeeval()
webpy-webpy-99e5eed/web/debugerror.py 0000664 0000000 0000000 00000030407 15206666350 0020003 0 ustar 00root root 0000000 0000000 """
pretty debug errors
(part of web.py)
portions adapted from Django
Copyright (c) 2005, the Lawrence Journal-World
Used under the modified BSD license:
http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
"""
__all__ = ["debugerror", "djangoerror", "emailerrors"]
import os
import os.path
import pprint
import sys
import traceback
from . import webapi as web
from .net import websafe
from .template import Template
from .utils import safestr, sendmail
def update_globals_template(t, globals):
t.t.__globals__.update(globals)
whereami = os.path.join(os.getcwd(), __file__)
whereami = os.path.sep.join(whereami.split(os.path.sep)[:-1])
djangoerror_t = """\
$def with (exception_type, exception_value, frames)
$exception_type at $ctx.path
$def dicttable (d, kls='req', id=None):
$ items = d and list(d.items()) or []
$items.sort()
$:dicttable_items(items, kls, id)
$def dicttable_items(items, kls='req', id=None):
$if items:
Variable
Value
$for k, v in items:
$k
$prettify(v)
$else:
No data.
$exception_type at $ctx.path
$exception_value
Python
$frames[0].filename in $frames[0].function, line $frames[0].lineno
Web
$ctx.method $ctx.home$ctx.path
Traceback (innermost first)
$for frame in frames:
$frame.filename in $frame.function
$if frame.context_line is not None:
$if frame.pre_context:
$for line in frame.pre_context:
$line
$frame.context_line ...
$if frame.post_context:
$for line in frame.post_context:
$line
$if frame.vars:
▶ Local vars
$# $inspect.formatargvalues(*inspect.getargvalues(frame['tb'].tb_frame))
$ newctx = [(k, v) for (k, v) in ctx.iteritems() if not k.startswith('_') and not isinstance(v, dict)]
$:dicttable(dict(newctx))
ENVIRONMENT
$:dicttable(ctx.env)
You're seeing this error because you have web.config.debug
set to True. Set that to False if you don't want to see this.
""" # noqa: W605
djangoerror_r = None
def djangoerror():
def _get_lines_from_file(filename, lineno, context_lines):
"""
Returns context_lines before and after lineno from file.
Returns (pre_context_lineno, pre_context, context_line, post_context).
"""
try:
source = open(filename).readlines()
lower_bound = max(0, lineno - context_lines)
upper_bound = lineno + context_lines
pre_context = [line.strip("\n") for line in source[lower_bound:lineno]]
context_line = source[lineno].strip("\n")
post_context = [
line.strip("\n") for line in source[lineno + 1 : upper_bound]
]
return lower_bound, pre_context, context_line, post_context
except (OSError, IndexError):
return None, [], None, []
exception_type, exception_value, tback = sys.exc_info()
frames = []
while tback is not None:
filename = tback.tb_frame.f_code.co_filename
function = tback.tb_frame.f_code.co_name
lineno = tback.tb_lineno - 1
# hack to get correct line number for templates
lineno += tback.tb_frame.f_locals.get("__lineoffset__", 0)
(
pre_context_lineno,
pre_context,
context_line,
post_context,
) = _get_lines_from_file(filename, lineno, 7)
if "__hidetraceback__" not in tback.tb_frame.f_locals:
frames.append(
web.storage(
{
"tback": tback,
"filename": filename,
"function": function,
"lineno": lineno,
"vars": tback.tb_frame.f_locals,
"id": id(tback),
"pre_context": pre_context,
"context_line": context_line,
"post_context": post_context,
"pre_context_lineno": pre_context_lineno,
}
)
)
tback = tback.tb_next
frames.reverse()
def prettify(x):
try:
out = pprint.pformat(x)
except Exception as e:
out = "[could not display: <" + e.__class__.__name__ + ": " + str(e) + ">]"
return out
global djangoerror_r
if djangoerror_r is None:
djangoerror_r = Template(djangoerror_t, filename=__file__, filter=websafe)
t = djangoerror_r
globals = {
"ctx": web.ctx,
"web": web,
"dict": dict,
"str": str,
"prettify": prettify,
}
update_globals_template(t, globals)
return t(exception_type, exception_value, frames)
def debugerror():
"""
A replacement for `internalerror` that presents a nice page with lots
of debug information for the programmer.
(Based on the beautiful 500 page from [Django](http://djangoproject.com/),
designed by [Wilson Miner](http://wilsonminer.com/).)
"""
return web._InternalError(djangoerror())
def emailerrors(to_address, olderror, from_address=None):
"""
Wraps the old `internalerror` handler (pass as `olderror`) to
additionally email all errors to `to_address`, to aid in
debugging production websites.
Emails contain a normal text traceback as well as an
attachment containing the nice `debugerror` page.
"""
from_address = from_address or to_address
def emailerrors_internal():
error = olderror()
tb = sys.exc_info()
error_name = tb[0]
error_value = tb[1]
tb_txt = "".join(traceback.format_exception(*tb))
path = web.ctx.path
request = web.ctx.method + " " + web.ctx.home + web.ctx.fullpath
message = f"\n{request}\n\n{tb_txt}\n\n"
sendmail(
"your buggy site <%s>" % from_address,
"the bugfixer <%s>" % to_address,
"bug: {error_name}: {error_value} ({path})".format(**locals()),
message,
attachments=[dict(filename="bug.html", content=safestr(djangoerror()))],
)
return error
return emailerrors_internal
if __name__ == "__main__":
urls = ("/", "index")
from .application import application
app = application(urls, globals())
app.internalerror = debugerror
class index:
def GET(self):
thisdoesnotexist # noqa: F821
app.run()
webpy-webpy-99e5eed/web/form.py 0000664 0000000 0000000 00000051021 15206666350 0016601 0 ustar 00root root 0000000 0000000 """
HTML forms
(part of web.py)
"""
import copy
import re
from . import net, utils
from . import webapi as web
def attrget(obj, attr, value=None):
try:
if hasattr(obj, "has_key") and attr in obj:
return obj[attr]
except TypeError:
# Handle the case where has_key takes different number of arguments.
# This is the case with Model objects on appengine. See #134
pass
if (
hasattr(obj, "keys") and attr in obj
): # needed for Py3, has_key doesn't exist anymore
return obj[attr]
elif hasattr(obj, attr):
return getattr(obj, attr)
return value
class Form:
r"""
HTML form.
>>> f = Form(Textbox("x"))
>>> f.render()
u'
\n
\n
'
>>> f.fill(x="42")
True
>>> f.render()
u'
\n
\n
'
"""
def __init__(self, *inputs, **kw):
self.inputs = inputs
self.valid = True
self.note = None
self.validators = kw.pop("validators", [])
def __call__(self, x=None):
o = copy.deepcopy(self)
if x:
o.validates(x)
return o
def render(self):
out = ""
out += self.rendernote(self.note)
out += "
\n"
for i in self.inputs:
html = (
utils.safeunicode(i.pre)
+ i.render()
+ self.rendernote(i.note)
+ utils.safeunicode(i.post)
)
if i.is_hidden():
out += '
%s
\n' % (
html
)
else:
out += f'
{html}
\n'
out += "
"
return out
def render_css(self):
out = []
out.append(self.rendernote(self.note))
for i in self.inputs:
if not i.is_hidden():
out.append(
f''
)
out.append(i.pre)
out.append(i.render())
out.append(self.rendernote(i.note))
out.append(i.post)
out.append("\n")
return "".join(out)
def rendernote(self, note):
if note:
return '%s' % net.websafe(note)
else:
return ""
def validates(self, source=None, _validate=True, **kw):
source = source or kw or web.input()
out = True
for i in self.inputs:
v = attrget(source, i.name)
if _validate:
if not i.validate(v):
self.note = i.note
return False
else:
i.set_value(v)
if _validate:
out = out and self._validate(source)
self.valid = out
return out
def _validate(self, value):
self.value = value
for v in self.validators:
if not v.valid(value):
self.note = v.msg
return False
return True
def fill(self, source=None, **kw):
return self.validates(source, _validate=False, **kw)
def __getitem__(self, i):
for x in self.inputs:
if x.name == i:
return x
raise KeyError(i)
def __getattr__(self, name):
# don't interfere with deepcopy
inputs = self.__dict__.get("inputs") or []
for x in inputs:
if x.name == name:
return x
raise AttributeError(name)
def get(self, i, default=None):
try:
return self[i]
except KeyError:
return default
def _get_d(self): # @@ should really be form.attr, no?
return utils.storage([(i.name, i.get_value()) for i in self.inputs])
d = property(_get_d)
class Input:
"""Generic input. Type attribute must be specified when called directly.
See also:
Currently only types which can be written inside one `` tag are
supported.
- For checkbox, please use `Checkbox` class for better control.
- For radiobox, please use `Radio` class for better control.
>>> Input(name='foo', type='email', value="user@domain.com").render()
u''
>>> Input(name='foo', type='number', value="bar").render()
u''
>>> Input(name='num', type="number", min='0', max='10', step='2', value='5').render()
u''
>>> Input(name='foo', type="tel", value='55512345').render()
u''
>>> Input(name='search', type="search", value='Search').render()
u''
>>> Input(name='search', type="search", value='Search', required='required', pattern='[a-z0-9]{2,30}', placeholder='Search...').render()
u''
>>> Input(name='url', type="url", value='url').render()
u''
>>> Input(name='range', type="range", min='0', max='10', step='2', value='5').render()
u''
>>> Input(name='color', type="color").render()
u''
>>> Input(name='f', type="file", accept=".doc,.docx,.xml").render()
u''
"""
def __init__(self, name, *validators, **attrs):
self.name = name
self.validators = validators
self.attrs = attrs = AttributeList(attrs)
self.type = attrs.pop("type", None)
self.description = attrs.pop("description", name)
self.value = attrs.pop("value", None)
self.pre = attrs.pop("pre", "")
self.post = attrs.pop("post", "")
self.note = None
self.id = attrs.setdefault("id", self.get_default_id())
if "class_" in attrs:
attrs["class"] = attrs["class_"]
del attrs["class_"]
def is_hidden(self):
return False
def get_type(self):
if self.type is not None:
return self.type
else:
raise AttributeError("missing attribute 'type'")
def get_default_id(self):
return self.name
def validate(self, value):
self.set_value(value)
for v in self.validators:
if not v.valid(value):
self.note = v.msg
return False
return True
def set_value(self, value):
self.value = value
def get_value(self):
return self.value
def render(self):
attrs = self.attrs.copy()
attrs["type"] = self.get_type()
if self.value is not None:
attrs["value"] = self.value
attrs["name"] = self.name
attrs["id"] = self.id
return "" % attrs
def rendernote(self, note):
if note:
return '%s' % net.websafe(note)
else:
return ""
def addatts(self):
# add leading space for backward-compatibility
return " " + str(self.attrs)
class AttributeList(dict):
"""List of attributes of input.
>>> a = AttributeList(type='text', name='x', value=20)
>>> a
"""
def copy(self):
return AttributeList(self)
def __str__(self):
return " ".join([f'{k}="{net.websafe(v)}"' for k, v in sorted(self.items())])
def __repr__(self):
return "" % repr(str(self))
class Textbox(Input):
"""Textbox input.
>>> Textbox(name='foo', value='bar').render()
u''
>>> Textbox(name='foo', value=0).render()
u''
"""
def get_type(self):
return "text"
class Password(Input):
"""Password input.
>>> Password(name='password', value='secret').render()
u''
"""
def get_type(self):
return "password"
class Textarea(Input):
"""Textarea input.
>>> Textarea(name='foo', value='bar').render()
u''
"""
def render(self):
attrs = self.attrs.copy()
attrs["name"] = self.name
value = net.websafe(self.value or "")
return f""
class Dropdown(Input):
r"""Dropdown/select input.
>>> Dropdown(name='foo', args=['a', 'b', 'c'], value='b').render()
u'\n'
>>> Dropdown(name='foo', args=[('a', 'aa'), ('b', 'bb'), ('c', 'cc')], value='b').render()
u'\n'
"""
def __init__(self, name, args, *validators, **attrs):
self.args = args
super().__init__(name, *validators, **attrs)
def render(self):
attrs = self.attrs.copy()
attrs["name"] = self.name
x = "\n"
return x
def _render_option(self, arg, indent=" "):
if isinstance(arg, (tuple, list)):
value, desc = arg
else:
value, desc = arg, arg
value = utils.safestr(value)
if isinstance(self.value, (tuple, list)):
s_value = [utils.safestr(x) for x in self.value]
else:
s_value = utils.safestr(self.value)
if s_value == value or (isinstance(s_value, list) and value in s_value):
select_p = ' selected="selected"'
else:
select_p = ""
return (
indent
+ f'\n'
)
class GroupedDropdown(Dropdown):
r"""Grouped Dropdown/select input.
>>> GroupedDropdown(name='car_type', args=(('Swedish Cars', ('Volvo', 'Saab')), ('German Cars', ('Mercedes', 'Audi'))), value='Audi').render()
u'\n'
>>> GroupedDropdown(name='car_type', args=(('Swedish Cars', (('v', 'Volvo'), ('s', 'Saab'))), ('German Cars', (('m', 'Mercedes'), ('a', 'Audi')))), value='a').render()
u'\n'
""" # noqa: E501
def __init__(self, name, args, *validators, **attrs):
self.args = args
super().__init__(name, *validators, **attrs)
def render(self):
attrs = self.attrs.copy()
attrs["name"] = self.name
x = "\n"
return x
class Radio(Input):
def __init__(self, name, args, *validators, **attrs):
self.args = args
super().__init__(name, *validators, **attrs)
def render(self):
x = ""
for idx, arg in enumerate(self.args, start=1):
if isinstance(arg, (tuple, list)):
value, desc = arg
else:
value, desc = arg, arg
attrs = self.attrs.copy()
attrs["name"] = self.name
attrs["type"] = "radio"
attrs["value"] = value
attrs["id"] = self.name + str(idx)
if self.value == value:
attrs["checked"] = "checked"
x += f" {net.websafe(desc)}"
x += ""
return x
class Checkbox(Input):
"""Checkbox input.
>>> Checkbox('foo', value='bar', checked=True).render()
u''
>>> Checkbox('foo', value='bar').render()
u''
>>> c = Checkbox('foo', value='bar')
>>> c.validate('on')
True
>>> c.render()
u''
"""
def __init__(self, name, *validators, **attrs):
self.checked = attrs.pop("checked", False)
Input.__init__(self, name, *validators, **attrs)
def get_default_id(self):
value = utils.safestr(self.value or "")
return self.name + "_" + value.replace(" ", "_")
def render(self):
attrs = self.attrs.copy()
attrs["type"] = "checkbox"
attrs["name"] = self.name
attrs["value"] = self.value
if self.checked:
attrs["checked"] = "checked"
return "" % attrs
def set_value(self, value):
self.checked = bool(value)
def get_value(self):
return self.checked
class Button(Input):
"""HTML Button.
>>> Button("save").render()
u''
>>> Button("action", value="save", html="Save Changes").render()
u''
"""
def __init__(self, name, *validators, **attrs):
super().__init__(name, *validators, **attrs)
self.description = ""
def render(self):
attrs = self.attrs.copy()
attrs["name"] = self.name
if self.value is not None:
attrs["value"] = self.value
html = attrs.pop("html", None) or net.websafe(self.name)
return f""
class Hidden(Input):
"""Hidden Input.
>>> Hidden(name='foo', value='bar').render()
u''
"""
def is_hidden(self):
return True
def get_type(self):
return "hidden"
class File(Input):
"""File input.
>>> File(name='f', accept=".doc,.docx,.xml").render()
u''
"""
def get_type(self):
return "file"
class Telephone(Input):
"""Telephone input.
See:
>>> Telephone(name='tel', value='55512345').render()
u''
"""
def get_type(self):
return "tel"
class Email(Input):
"""Email input.
See:
>>> Email(name='email', value='me@example.org').render()
u''
"""
def get_type(self):
return "email"
class Date(Input):
"""Date input.
Note: Not supported by desktop Safari, Internet Explorer, or Opera Mini
See:
>>> Date(name='date', value='2020-04-01').render()
u''
"""
def get_type(self):
return "date"
class Time(Input):
"""Time input.
Note: Not supported by desktop Safari, Internet Explorer, or Opera Mini
See:
>>> Time(name='time', value='07:00').render()
u''
"""
def get_type(self):
return "time"
class Search(Input):
"""Search input.
See:
>> Search(name='search', value='Search').render()
u''
>>> Search(name='search', value='Search', required='required', pattern='[a-z0-9]{2,30}', placeholder='Search...').render()
u''
"""
def get_type(self):
return "search"
class Url(Input):
"""URL input.
See:
>>> Url(name='url', value='url').render()
u''
"""
def get_type(self):
return "url"
class Number(Input):
"""Number input.
See:
>>> Number(name='num', min='0', max='10', step='2', value='5').render()
u''
"""
def get_type(self):
return "number"
class Range(Input):
"""Range input.
See:
>>> Range(name='range', min='0', max='10', step='2', value='5').render()
u''
"""
def get_type(self):
return "range"
class Color(Input):
"""Color input.
Note: Not supported by Internet Explorer or Opera Mini
See:
>>> Color(name='color').render()
u''
"""
def get_type(self):
return "color"
class Datalist(Input):
"""Datalist input.
This is currently supported by Chrome, Firefox, Edge, and Opera. It is not
supported on Safari or Internet Explorer. Use it with caution.
Datalist cannot be used separately. It must be bound to an input.
>>> Datalist(name='list', args=[('a', 'b'), ('c', 'd')]).render()
u''
>>> Datalist(name='list', args=[['a', 'b'], ['c', 'd']]).render()
u''
>>> Datalist(name='list', args=['a', 'b', 'c', 'd']).render()
u''
"""
def __init__(self, name, args, *validators, **kwargs):
self.args = args
super().__init__(name, *validators, **kwargs)
def render(self):
attrs = self.attrs.copy()
attrs["name"] = self.name
label_p = ""
x = ""
return x
class Validator:
def __deepcopy__(self, memo):
return copy.copy(self)
def __init__(self, msg, test, jstest=None):
utils.autoassign(self, locals())
def valid(self, value):
try:
return self.test(value)
except:
return False
notnull = Validator("Required", bool)
class regexp(Validator):
def __init__(self, rexp, msg):
self.rexp = re.compile(rexp)
self.msg = msg
def valid(self, value):
return bool(self.rexp.match(value))
if __name__ == "__main__":
import doctest
doctest.testmod()
webpy-webpy-99e5eed/web/http.py 0000664 0000000 0000000 00000010424 15206666350 0016617 0 ustar 00root root 0000000 0000000 """
HTTP Utilities
(from web.py)
"""
__all__ = [
"expires",
"lastmodified",
"prefixurl",
"modified",
"changequery",
"url",
"profiler",
]
import datetime
from urllib.parse import urlencode as urllib_urlencode
from . import net, utils
from . import webapi as web
from .py3helpers import iteritems
def prefixurl(base=""):
"""
Sorry, this function is really difficult to explain.
Maybe some other time.
"""
url = web.ctx.path.lstrip("/")
for i in range(url.count("/")):
base += "../"
if not base:
base = "./"
return base
def expires(delta):
"""
Outputs an `Expires` header for `delta` from now.
`delta` is a `timedelta` object or a number of seconds.
"""
if isinstance(delta, int):
delta = datetime.timedelta(seconds=delta)
date_obj = datetime.datetime.utcnow() + delta
web.header("Expires", net.httpdate(date_obj))
def lastmodified(date_obj):
"""Outputs a `Last-Modified` header for `datetime`."""
web.header("Last-Modified", net.httpdate(date_obj))
def modified(date=None, etag=None):
"""
Checks to see if the page has been modified since the version in the
requester's cache.
When you publish pages, you can include `Last-Modified` and `ETag`
with the date the page was last modified and an opaque token for
the particular version, respectively. When readers reload the page,
the browser sends along the modification date and etag value for
the version it has in its cache. If the page hasn't changed,
the server can just return `304 Not Modified` and not have to
send the whole page again.
This function takes the last-modified date `date` and the ETag `etag`
and checks the headers to see if they match. If they do, it returns
`True`, or otherwise it raises NotModified error. It also sets
`Last-Modified` and `ETag` output headers.
"""
n = {x.strip('" ') for x in web.ctx.env.get("HTTP_IF_NONE_MATCH", "").split(",")}
m = net.parsehttpdate(web.ctx.env.get("HTTP_IF_MODIFIED_SINCE", "").split(";")[0])
validate = False
if etag:
if "*" in n or etag in n:
validate = True
if date and m:
# we subtract a second because
# HTTP dates don't have sub-second precision
if date - datetime.timedelta(seconds=1) <= m:
validate = True
if date:
lastmodified(date)
if etag:
web.header("ETag", '"' + etag + '"')
if validate:
raise web.notmodified()
else:
return True
def urlencode(query, doseq=0):
"""
Same as urllib.urlencode, but supports unicode strings.
>>> urlencode({'text':'foo bar'})
'text=foo+bar'
>>> urlencode({'x': [1, 2]}, doseq=True)
'x=1&x=2'
"""
def convert(value, doseq=False):
if doseq and isinstance(value, list):
return [convert(v) for v in value]
else:
return utils.safestr(value)
query = {k: convert(v, doseq) for k, v in query.items()}
return urllib_urlencode(query, doseq=doseq)
def changequery(query=None, **kw):
"""
Imagine you're at `/foo?a=1&b=2`. Then `changequery(a=3)` will return
`/foo?a=3&b=2` -- the same URL but with the arguments you requested
changed.
"""
if query is None:
query = web.rawinput(method="get")
for k, v in iteritems(kw):
if v is None:
query.pop(k, None)
else:
query[k] = v
out = web.ctx.path
if query:
out += "?" + urlencode(query, doseq=True)
return out
def url(path=None, doseq=False, **kw):
"""
Makes url by concatenating web.ctx.homepath and path and the
query string created using the arguments.
"""
if path is None:
path = web.ctx.path
if path.startswith("/"):
out = web.ctx.homepath + path
else:
out = path
if kw:
out += "?" + urlencode(kw, doseq=doseq)
return out
def profiler(app):
"""Outputs basic profiling information at the bottom of each response."""
from utils import profile
def profile_internal(e, o):
out, result = profile(app)(e, o)
return list(out) + ["
" + net.websafe(result) + "
"]
return profile_internal
if __name__ == "__main__":
import doctest
doctest.testmod()
webpy-webpy-99e5eed/web/httpserver.py 0000664 0000000 0000000 00000023411 15206666350 0020046 0 ustar 00root root 0000000 0000000 import os
import posixpath
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer, SimpleHTTPRequestHandler
from io import BytesIO
from urllib.parse import unquote, urlparse
from . import utils
from . import webapi as web
__all__ = ["runsimple"]
def runbasic(func, server_address=("0.0.0.0", 8080)):
"""
Runs a simple HTTP server hosting WSGI app `func`. The directory `static/`
is hosted statically.
Based on [WsgiServer][ws] from [Colin Stewart][cs].
[ws]: http://www.owlfish.com/software/wsgiutils/documentation/wsgi-server-api.html
[cs]: http://www.owlfish.com/
"""
# Copyright (c) 2004 Colin Stewart (http://www.owlfish.com/)
# Modified somewhat for simplicity
# Used under the modified BSD license:
# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
import errno
import traceback
import SocketServer
class WSGIHandler(SimpleHTTPRequestHandler):
def run_wsgi_app(self):
protocol, host, path, parameters, query, fragment = urlparse(
"http://dummyhost%s" % self.path
)
# we only use path, query
env = {
"wsgi.version": (1, 0),
"wsgi.url_scheme": "http",
"wsgi.input": self.rfile,
"wsgi.errors": sys.stderr,
"wsgi.multithread": 1,
"wsgi.multiprocess": 0,
"wsgi.run_once": 0,
"REQUEST_METHOD": self.command,
"REQUEST_URI": self.path,
"PATH_INFO": path,
"QUERY_STRING": query,
"CONTENT_TYPE": self.headers.get("Content-Type", ""),
"CONTENT_LENGTH": self.headers.get("Content-Length", ""),
"REMOTE_ADDR": self.client_address[0],
"SERVER_NAME": self.server.server_address[0],
"SERVER_PORT": str(self.server.server_address[1]),
"SERVER_PROTOCOL": self.request_version,
}
for http_header, http_value in self.headers.items():
env["HTTP_%s" % http_header.replace("-", "_").upper()] = http_value
# Setup the state
self.wsgi_sent_headers = 0
self.wsgi_headers = []
try:
# We have there environment, now invoke the application
result = self.server.app(env, self.wsgi_start_response)
try:
try:
for data in result:
if data:
self.wsgi_write_data(data)
finally:
if hasattr(result, "close"):
result.close()
except OSError as socket_err:
# Catch common network errors and suppress them
if socket_err.args[0] in (errno.ECONNABORTED, errno.EPIPE):
return
except TimeoutError:
return
except:
print(traceback.format_exc(), file=web.debug)
if not self.wsgi_sent_headers:
# We must write out something!
self.wsgi_write_data(" ")
return
do_POST = run_wsgi_app
do_PUT = run_wsgi_app
do_DELETE = run_wsgi_app
def do_GET(self):
if self.path.startswith("/static/"):
SimpleHTTPRequestHandler.do_GET(self)
else:
self.run_wsgi_app()
def wsgi_start_response(self, response_status, response_headers, exc_info=None):
if self.wsgi_sent_headers:
raise Exception("Headers already sent and start_response called again!")
# Should really take a copy to avoid changes in the application....
self.wsgi_headers = (response_status, response_headers)
return self.wsgi_write_data
def wsgi_write_data(self, data):
if not self.wsgi_sent_headers:
status, headers = self.wsgi_headers
# Need to send header prior to data
status_code = status[: status.find(" ")]
status_msg = status[status.find(" ") + 1 :]
self.send_response(int(status_code), status_msg)
for header, value in headers:
self.send_header(header, value)
self.end_headers()
self.wsgi_sent_headers = 1
# Send the data
self.wfile.write(data)
class WSGIServer(SocketServer.ThreadingMixIn, HTTPServer):
def __init__(self, func, server_address):
HTTPServer.HTTPServer.__init__(self, server_address, WSGIHandler)
self.app = func
self.serverShuttingDown = 0
print("http://%s:%d/" % server_address)
WSGIServer(func, server_address).serve_forever()
# The WSGIServer instance.
# Made global so that it can be stopped in embedded mode.
server = None
def runsimple(func, server_address=("0.0.0.0", 8080)):
"""
Runs [CherryPy][cp] WSGI server hosting WSGI app `func`.
The directory `static/` is hosted statically.
[cp]: http://www.cherrypy.org
"""
global server
func = StaticMiddleware(func)
func = LogMiddleware(func)
server = WSGIServer(server_address, func)
if "/" in server_address[0]:
print("%s" % server_address)
else:
if server.ssl_adapter:
print("https://%s:%d/" % server_address)
else:
print("http://%s:%d/" % server_address)
try:
server.start()
except (KeyboardInterrupt, SystemExit):
server.stop()
server = None
def WSGIServer(server_address, wsgi_app):
"""Creates CherryPy WSGI server listening at `server_address` to serve `wsgi_app`.
This function can be overwritten to customize the webserver or use a different webserver.
"""
from cheroot import wsgi
server = wsgi.Server(server_address, wsgi_app, server_name="localhost")
server.nodelay = not sys.platform.startswith(
"java"
) # TCP_NODELAY isn't supported on the JVM
return server
class StaticApp(SimpleHTTPRequestHandler):
"""WSGI application for serving static files."""
def __init__(self, environ, start_response):
self.headers = []
self.environ = environ
self.start_response = start_response
self.directory = os.getcwd()
def send_response(self, status, msg=""):
# the int(status) call is needed because in Py3 status is an enum.IntEnum and we need the integer behind
self.status = str(int(status)) + " " + msg
def send_header(self, name, value):
self.headers.append((name, value))
def end_headers(self):
pass
def log_message(*a):
pass
def __iter__(self):
environ = self.environ
self.path = environ.get("PATH_INFO", "")
self.client_address = (
environ.get("REMOTE_ADDR", "-"),
environ.get("REMOTE_PORT", "-"),
)
self.command = environ.get("REQUEST_METHOD", "-")
self.wfile = BytesIO() # for capturing error
try:
path = self.translate_path(self.path)
etag = '"%s"' % os.path.getmtime(path)
client_etag = environ.get("HTTP_IF_NONE_MATCH")
self.send_header("ETag", etag)
if etag == client_etag:
self.send_response(304, "Not Modified")
self.start_response(self.status, self.headers)
return
except OSError:
pass # Probably a 404
f = self.send_head()
self.start_response(self.status, self.headers)
if f:
block_size = 16 * 1024
while True:
buf = f.read(block_size)
if not buf:
break
yield buf
f.close()
else:
value = self.wfile.getvalue()
yield value
class StaticMiddleware:
"""WSGI middleware for serving static files."""
def __init__(self, app, prefix="/static/"):
self.app = app
self.prefix = prefix
def __call__(self, environ, start_response):
path = environ.get("PATH_INFO", "")
path = self.normpath(path)
if path.startswith(self.prefix):
return StaticApp(environ, start_response)
else:
return self.app(environ, start_response)
def normpath(self, path):
path2 = posixpath.normpath(unquote(path))
if path.endswith("/"):
path2 += "/"
return path2
class LogMiddleware:
"""WSGI middleware for logging the status."""
def __init__(self, app):
self.app = app
self.format = '%s - - [%s] "%s %s %s" - %s'
f = BytesIO()
class FakeSocket:
def makefile(self, *a):
return f
# take log_date_time_string method from BaseHTTPRequestHandler
self.log_date_time_string = BaseHTTPRequestHandler(
FakeSocket(), None, None
).log_date_time_string
def __call__(self, environ, start_response):
def xstart_response(status, response_headers, *args):
out = start_response(status, response_headers, *args)
self.log(status, environ)
return out
return self.app(environ, xstart_response)
def log(self, status, environ):
outfile = environ.get("wsgi.errors", web.debug)
req = environ.get("PATH_INFO", "_")
protocol = environ.get("ACTUAL_SERVER_PROTOCOL", "-")
method = environ.get("REQUEST_METHOD", "-")
host = "{}:{}".format(
environ.get("REMOTE_ADDR", "-"),
environ.get("REMOTE_PORT", "-"),
)
time = self.log_date_time_string()
msg = self.format % (host, time, protocol, method, req, status)
print(utils.safestr(msg), file=outfile)
webpy-webpy-99e5eed/web/net.py 0000664 0000000 0000000 00000014232 15206666350 0016427 0 ustar 00root root 0000000 0000000 """
Network Utilities
(from web.py)
"""
import datetime
import re
import socket
import time
from urllib.parse import quote
__all__ = [
"validipaddr",
"validip6addr",
"validipport",
"validip",
"validaddr",
"urlquote",
"httpdate",
"parsehttpdate",
"htmlquote",
"htmlunquote",
"websafe",
]
def validip6addr(address):
"""
Returns True if `address` is a valid IPv6 address.
>>> validip6addr('::')
True
>>> validip6addr('aaaa:bbbb:cccc:dddd::1')
True
>>> validip6addr('1:2:3:4:5:6:7:8:9:10')
False
>>> validip6addr('12:10')
False
"""
try:
socket.inet_pton(socket.AF_INET6, address)
except (OSError, AttributeError, ValueError):
return False
return True
def validipaddr(address):
"""
Returns True if `address` is a valid IPv4 address.
>>> validipaddr('192.168.1.1')
True
>>> validipaddr('192.168. 1.1')
False
>>> validipaddr('192.168.1.800')
False
>>> validipaddr('192.168.1')
False
"""
try:
octets = address.split(".")
if len(octets) != 4:
return False
for x in octets:
if " " in x:
return False
if not (0 <= int(x) <= 255):
return False
except ValueError:
return False
return True
def validipport(port):
"""
Returns True if `port` is a valid IPv4 port.
>>> validipport('9000')
True
>>> validipport('foo')
False
>>> validipport('1000000')
False
"""
try:
if not (0 <= int(port) <= 65535):
return False
except ValueError:
return False
return True
def validip(ip, defaultaddr="0.0.0.0", defaultport=8080):
"""
Returns `(ip_address, port)` from string `ip_addr_port`
>>> validip('1.2.3.4')
('1.2.3.4', 8080)
>>> validip('80')
('0.0.0.0', 80)
>>> validip('192.168.0.1:85')
('192.168.0.1', 85)
>>> validip('::')
('::', 8080)
>>> validip('[::]:88')
('::', 88)
>>> validip('[::1]:80')
('::1', 80)
"""
addr = defaultaddr
port = defaultport
# Matt Boswell's code to check for ipv6 first
match = re.search(r"^\[([^]]+)\](?::(\d+))?$", ip) # check for [ipv6]:port
if match:
if validip6addr(match.group(1)):
if match.group(2):
if validipport(match.group(2)):
return (match.group(1), int(match.group(2)))
else:
return (match.group(1), port)
else:
if validip6addr(ip):
return (ip, port)
# end ipv6 code
ip = ip.split(":", 1)
if len(ip) == 1:
if not ip[0]:
pass
elif validipaddr(ip[0]):
addr = ip[0]
elif validipport(ip[0]):
port = int(ip[0])
else:
raise ValueError(":".join(ip) + " is not a valid IP address/port")
elif len(ip) == 2:
addr, port = ip
if not validipaddr(addr) or not validipport(port):
raise ValueError(":".join(ip) + " is not a valid IP address/port")
port = int(port)
else:
raise ValueError(":".join(ip) + " is not a valid IP address/port")
return (addr, port)
def validaddr(string_):
"""
Returns either (ip_address, port) or "/path/to/socket" from string_
>>> validaddr('/path/to/socket')
'/path/to/socket'
>>> validaddr('8000')
('0.0.0.0', 8000)
>>> validaddr('127.0.0.1')
('127.0.0.1', 8080)
>>> validaddr('127.0.0.1:8000')
('127.0.0.1', 8000)
>>> validip('[::1]:80')
('::1', 80)
>>> validaddr('fff')
Traceback (most recent call last):
...
ValueError: fff is not a valid IP address/port
"""
if "/" in string_:
return string_
else:
return validip(string_)
def urlquote(val):
"""
Quotes a string for use in a URL.
>>> urlquote('://?f=1&j=1')
'%3A//%3Ff%3D1%26j%3D1'
>>> urlquote(None)
''
>>> urlquote(u'\u203d')
'%E2%80%BD'
"""
if val is None:
return ""
val = str(val).encode("utf-8")
return quote(val)
def httpdate(date_obj):
"""
Formats a datetime object for use in HTTP headers.
>>> import datetime
>>> httpdate(datetime.datetime(1970, 1, 1, 1, 1, 1))
'Thu, 01 Jan 1970 01:01:01 GMT'
"""
return date_obj.strftime("%a, %d %b %Y %H:%M:%S GMT")
def parsehttpdate(string_):
"""
Parses an HTTP date into a datetime object.
>>> parsehttpdate('Thu, 01 Jan 1970 01:01:01 GMT')
datetime.datetime(1970, 1, 1, 1, 1, 1)
"""
try:
t = time.strptime(string_, "%a, %d %b %Y %H:%M:%S %Z")
except ValueError:
return None
return datetime.datetime(*t[:6])
def htmlquote(text):
r"""
Encodes `text` for raw use in HTML.
>>> htmlquote(u"<'&\">")
u'<'&">'
"""
text = text.replace("&", "&") # Must be done first!
text = text.replace("<", "<")
text = text.replace(">", ">")
text = text.replace("'", "'")
text = text.replace('"', """)
return text
def htmlunquote(text):
r"""
Decodes `text` that's HTML quoted.
>>> htmlunquote(u'<'&">')
u'<\'&">'
"""
text = text.replace(""", '"')
text = text.replace("'", "'")
text = text.replace(">", ">")
text = text.replace("<", "<")
text = text.replace("&", "&") # Must be done last!
return text
def websafe(val):
r"""
Converts `val` so that it is safe for use in Unicode HTML.
>>> websafe("<'&\">")
u'<'&">'
>>> websafe(None)
u''
>>> websafe(u'\u203d') == u'\u203d'
True
"""
if val is None:
return ""
if isinstance(val, bytes):
val = val.decode("utf-8")
elif not isinstance(val, str):
val = str(val)
return htmlquote(val)
if __name__ == "__main__":
import doctest
doctest.testmod()
webpy-webpy-99e5eed/web/py3helpers.py 0000664 0000000 0000000 00000000315 15206666350 0017734 0 ustar 00root root 0000000 0000000 """Utilities for make the code run both on Python2 and Python3."""
# Dictionary iteration
iterkeys = lambda d: iter(d.keys())
itervalues = lambda d: iter(d.values())
iteritems = lambda d: iter(d.items())
webpy-webpy-99e5eed/web/session.py 0000664 0000000 0000000 00000031236 15206666350 0017327 0 ustar 00root root 0000000 0000000 """
Session Management
(from web.py)
"""
import datetime
import os
import os.path
import pickle
import shutil
import threading
import time
from base64 import decodebytes, encodebytes
from copy import deepcopy
from hashlib import sha1
from . import utils
from . import webapi as web
from .py3helpers import iteritems
__all__ = ["Session", "SessionExpired", "Store", "DiskStore", "DBStore", "MemoryStore"]
web.config.session_parameters = utils.storage(
{
"cookie_name": "webpy_session_id",
"cookie_domain": None,
"cookie_path": None,
"samesite": None,
"timeout": 86400, # 24 * 60 * 60, # 24 hours in seconds
"ignore_expiry": True,
"ignore_change_ip": True,
"secret_key": "fLjUfxqXtfNoIldA0A0J",
"expired_message": "Session expired",
"httponly": True,
"secure": False,
}
)
class SessionExpired(web.HTTPError):
def __init__(self, message):
web.HTTPError.__init__(self, "200 OK", {}, data=message)
class Session:
"""Session management for web.py"""
__slots__ = [
"store",
"_initializer",
"_last_cleanup_time",
"_config",
"_data",
"__getitem__",
"__setitem__",
"__delitem__",
]
def __init__(self, app, store, initializer=None):
self.store = store
self._initializer = initializer
self._last_cleanup_time = 0
self._config = utils.storage(web.config.session_parameters)
self._data = utils.threadeddict()
self.__getitem__ = self._data.__getitem__
self.__setitem__ = self._data.__setitem__
self.__delitem__ = self._data.__delitem__
if app:
app.add_processor(self._processor)
def __contains__(self, name):
return name in self._data
def __getattr__(self, name):
return getattr(self._data, name)
def __setattr__(self, name, value):
if name in self.__slots__:
object.__setattr__(self, name, value)
else:
setattr(self._data, name, value)
def __delattr__(self, name):
delattr(self._data, name)
def _processor(self, handler):
"""Application processor to setup session for every request"""
self._cleanup()
self._load()
try:
return handler()
finally:
self._save()
def _load(self):
"""Load the session from the store, by the id from cookie"""
cookie_name = self._config.cookie_name
self.session_id = web.cookies().get(cookie_name)
# Handler can do session.send_cookie = False to not send the cookie
self.send_cookie = True
# protection against session_id tampering
if self.session_id and not self._valid_session_id(self.session_id):
self.session_id = None
self._check_expiry()
if self.session_id:
d = self.store[self.session_id]
self.update(d)
self._validate_ip()
if not self.session_id:
self.session_id = self._generate_session_id()
if self._initializer:
if isinstance(self._initializer, dict):
self.update(deepcopy(self._initializer))
elif hasattr(self._initializer, "__call__"):
self._initializer()
self.ip = web.ctx.ip
def _check_expiry(self):
# check for expiry
if self.session_id and self.session_id not in self.store:
if self._config.ignore_expiry:
self.session_id = None
else:
return self.expired()
def _validate_ip(self):
# check for change of IP
if self.session_id and self.get("ip", None) != web.ctx.ip:
if not self._config.ignore_change_ip:
return self.expired()
def _save(self):
current_values = dict(self._data)
del current_values["session_id"]
del current_values["ip"]
if not self.send_cookie:
return
if not self.get("_killed"):
self._setcookie(self.session_id)
self.store[self.session_id] = dict(self._data)
else:
if web.cookies().get(self._config.cookie_name):
self._setcookie(self.session_id, expires=-1)
def _setcookie(self, session_id, expires="", **kw):
cookie_name = self._config.cookie_name
cookie_domain = self._config.cookie_domain
cookie_path = self._config.cookie_path
httponly = self._config.httponly
secure = self._config.secure
samesite = kw.get("samesite", self._config.get("samesite", None))
web.setcookie(
cookie_name,
session_id,
expires=expires,
domain=cookie_domain,
httponly=httponly,
secure=secure,
path=cookie_path,
samesite=samesite,
)
def _generate_session_id(self):
"""Generate a random id for session"""
while True:
rand = os.urandom(16)
now = time.time()
secret_key = self._config.secret_key
hashable = f"{rand}{now}{utils.safestr(web.ctx.ip)}{secret_key}"
session_id = sha1(hashable.encode("utf-8")).hexdigest()
if session_id not in self.store:
break
return session_id
def _valid_session_id(self, session_id):
rx = utils.re_compile("^[0-9a-fA-F]+$")
return rx.match(session_id)
def _cleanup(self):
"""Cleanup the stored sessions"""
current_time = time.time()
timeout = self._config.timeout
if current_time - self._last_cleanup_time > timeout:
self.store.cleanup(timeout)
self._last_cleanup_time = current_time
def expired(self):
"""Called when an expired session is atime"""
self._killed = True
self._save()
raise SessionExpired(self._config.expired_message)
def kill(self):
"""Kill the session, make it no longer available"""
del self.store[self.session_id]
self._killed = True
class Store:
"""Base class for session stores"""
def __contains__(self, key):
raise NotImplementedError()
def __getitem__(self, key):
raise NotImplementedError()
def __setitem__(self, key, value):
raise NotImplementedError()
def cleanup(self, timeout):
"""removes all the expired sessions"""
raise NotImplementedError()
def encode(self, session_dict):
"""encodes session dict as a string"""
pickled = pickle.dumps(session_dict)
return encodebytes(pickled)
def decode(self, session_data):
"""decodes the data to get back the session dict"""
if isinstance(session_data, str):
session_data = session_data.encode()
pickled = decodebytes(session_data)
return pickle.loads(pickled)
class DiskStore(Store):
"""
Store for saving a session on disk.
>>> import tempfile
>>> root = tempfile.mkdtemp()
>>> s = DiskStore(root)
>>> s['a'] = 'foo'
>>> s['a']
'foo'
>>> time.sleep(0.01)
>>> s.cleanup(0.01)
>>> s['a']
Traceback (most recent call last):
...
KeyError: 'a'
"""
def __init__(self, root):
# if the storage root doesn't exists, create it.
if not os.path.exists(root):
os.makedirs(os.path.abspath(root))
self.root = root
def _get_path(self, key):
if os.path.sep in key:
raise ValueError("Bad key: %s" % repr(key))
return os.path.join(self.root, key)
def __contains__(self, key):
path = self._get_path(key)
return os.path.exists(path)
def __getitem__(self, key):
path = self._get_path(key)
if os.path.exists(path):
with open(path, "rb") as fh:
pickled = fh.read()
return self.decode(pickled)
else:
raise KeyError(key)
def __setitem__(self, key, value):
path = self._get_path(key)
pickled = self.encode(value)
try:
tname = path + "." + threading.current_thread().name
f = open(tname, "wb")
try:
f.write(pickled)
finally:
f.close()
shutil.move(tname, path) # atomary operation
except OSError:
pass
def __delitem__(self, key):
path = self._get_path(key)
if os.path.exists(path):
os.remove(path)
def cleanup(self, timeout):
if not os.path.isdir(self.root):
return
now = time.time()
for f in os.listdir(self.root):
path = self._get_path(f)
atime = os.stat(path).st_atime
if now - atime > timeout:
if os.path.isdir(path):
shutil.rmtree(path)
else:
os.remove(path)
class DBStore(Store):
"""Store for saving a session in database
Needs a table with the following columns:
session_id CHAR(128) UNIQUE NOT NULL,
atime DATETIME NOT NULL default current_timestamp,
data TEXT
"""
def __init__(self, db, table_name):
self.db = db
self.table = table_name
def __contains__(self, key):
data = self.db.select(self.table, where="session_id=$key", vars=locals())
return bool(list(data))
def __getitem__(self, key):
now = datetime.datetime.now()
try:
s = self.db.select(self.table, where="session_id=$key", vars=locals())[0]
self.db.update(
self.table, where="session_id=$key", atime=now, vars=locals()
)
except IndexError:
raise KeyError(key)
else:
return self.decode(s.data)
def __setitem__(self, key, value):
# Remove the leading `b` of bytes object (`b"..."`), otherwise encoded
# value is invalid base64 format.
pickled = self.encode(value).decode()
now = datetime.datetime.now()
if key in self:
self.db.update(
self.table,
where="session_id=$key",
data=pickled,
atime=now,
vars=locals(),
)
else:
self.db.insert(self.table, False, session_id=key, atime=now, data=pickled)
def __delitem__(self, key):
self.db.delete(self.table, where="session_id=$key", vars=locals())
def cleanup(self, timeout):
timeout = datetime.timedelta(
timeout / (24.0 * 60 * 60)
) # timedelta takes numdays as arg
last_allowed_time = datetime.datetime.now() - timeout
self.db.delete(self.table, where="$last_allowed_time > atime", vars=locals())
class ShelfStore:
"""Store for saving session using `shelve` module.
import shelve
store = ShelfStore(shelve.open('session.shelf'))
XXX: is shelve thread-safe?
"""
def __init__(self, shelf):
self.shelf = shelf
def __contains__(self, key):
return key in self.shelf
def __getitem__(self, key):
atime, v = self.shelf[key]
self[key] = v # update atime
return v
def __setitem__(self, key, value):
self.shelf[key] = time.time(), value
def __delitem__(self, key):
try:
del self.shelf[key]
except KeyError:
pass
def cleanup(self, timeout):
now = time.time()
for k in self.shelf:
atime, v = self.shelf[k]
if now - atime > timeout:
del self[k]
class MemoryStore(Store):
"""Store for saving a session in memory.
Useful where there is limited fs writes on the disk, like
flash memories
Data will be saved into a dict:
k: (time, pydata)
"""
def __init__(self, d_store=None):
if d_store is None:
d_store = {}
self.d_store = d_store
def __contains__(self, key):
return key in self.d_store
def __getitem__(self, key):
"""Return the value and update the last seen value"""
t, value = self.d_store[key]
self.d_store[key] = (time.time(), value)
return value
def __setitem__(self, key, value):
self.d_store[key] = (time.time(), value)
def __delitem__(self, key):
del self.d_store[key]
def cleanup(self, timeout):
now = time.time()
to_del = []
for k, (atime, value) in iteritems(self.d_store):
if now - atime > timeout:
to_del.append(k)
# to avoid exception on "dict change during iterations"
for k in to_del:
del self.d_store[k]
if __name__ == "__main__":
import doctest
doctest.testmod()
webpy-webpy-99e5eed/web/template.py 0000664 0000000 0000000 00000143061 15206666350 0017457 0 ustar 00root root 0000000 0000000 """
simple, elegant templating
(part of web.py)
Template design:
Template string is split into tokens and the tokens are combined into nodes.
Parse tree is a nodelist. TextNode and ExpressionNode are simple nodes and
for-loop, if-loop etc are block nodes, which contain multiple child nodes.
Each node can emit some python string. python string emitted by the
root node is validated for safeeval and executed using python in the given environment.
Enough care is taken to make sure the generated code and the template has line to line match,
so that the error messages can point to exact line number in template. (It doesn't work in some cases still.)
Grammar:
template -> defwith sections
defwith -> '$def with (' arguments ')' | ''
sections -> section*
section -> block | assignment | line
assignment -> '$ '
line -> (text|expr)*
text ->
expr -> '$' pyexpr | '$(' pyexpr ')' | '${' pyexpr '}'
pyexpr ->
"""
import ast
import builtins
import glob
import itertools
import os
import sys
import token
import tokenize
from functools import partial
from more_itertools import peekable
from .net import websafe
from .utils import re_compile, safestr, safeunicode, storage
from .webapi import config
__all__ = [
"Template",
"Render",
"render",
"frender",
"ParseError",
"SecurityError",
"test",
]
from collections.abc import MutableMapping
def splitline(text):
r"""
Splits the given text at newline.
>>> splitline('foo\nbar')
('foo\n', 'bar')
>>> splitline('foo')
('foo', '')
>>> splitline('')
('', '')
"""
index = text.find("\n") + 1
if index:
return text[:index], text[index:]
else:
return text, ""
class Parser:
"""Parser Base."""
def __init__(self):
self.statement_nodes = STATEMENT_NODES
self.keywords = KEYWORDS
def parse(self, text, name=""):
self.text = text
self.name = name
defwith, text = self.read_defwith(text)
suite = self.read_suite(text)
return DefwithNode(defwith, suite)
def read_defwith(self, text):
if text.startswith("$def with"):
defwith, text = splitline(text)
defwith = defwith[1:].strip() # strip $ and spaces
return defwith, text
else:
return "", text
def read_section(self, text):
r"""Reads one section from the given text.
section -> block | assignment | line
>>> read_section = Parser().read_section
>>> read_section('foo\nbar\n')
(, 'bar\n')
>>> read_section('$ a = b + 1\nfoo\n')
(, 'foo\n')
read_section('$for in range(10):\n hello $i\nfoo)
"""
if text.lstrip(" ").startswith("$"):
index = text.index("$")
begin_indent, text2 = text[:index], text[index + 1 :]
ahead = self.python_lookahead(text2)
if ahead == "var":
return self.read_var(text2)
elif ahead in self.statement_nodes:
return self.read_block_section(text2, begin_indent)
elif ahead in self.keywords:
return self.read_keyword(text2)
elif ahead.strip() == "":
# assignments starts with a space after $
# ex: $ a = b + 2
return self.read_assignment(text2)
return self.readline(text)
def read_var(self, text):
r"""Reads a var statement.
>>> read_var = Parser().read_var
>>> read_var('var x=10\nfoo')
(, 'foo')
>>> read_var('var x: hello $name\nfoo')
(, 'foo')
"""
line, text = splitline(text)
tokens = self.python_tokens(line)
if len(tokens) < 4:
raise SyntaxError("Invalid var statement")
name = tokens[1]
sep = tokens[2]
value = line.split(sep, 1)[1].strip()
if sep == "=":
pass # no need to process value
elif sep == ":":
# @@ Hack for backward-compatability
if tokens[3] == "\n": # multi-line var statement
block, text = self.read_indented_block(text, " ")
lines = [self.readline(x)[0] for x in block.splitlines()]
nodes = []
for x in lines:
nodes.extend(x.nodes)
nodes.append(TextNode("\n"))
else: # single-line var statement
linenode, _ = self.readline(value)
nodes = linenode.nodes
parts = [node.emit("") for node in nodes]
value = "join_(%s)" % ", ".join(parts)
else:
raise SyntaxError("Invalid var statement")
return VarNode(name, value), text
def read_suite(self, text):
r"""Reads section by section till end of text.
>>> read_suite = Parser().read_suite
>>> read_suite('hello $name\nfoo\n')
[, ]
"""
sections = []
while text:
section, text = self.read_section(text)
sections.append(section)
return SuiteNode(sections)
def readline(self, text):
r"""Reads one line from the text. Newline is suppressed if the line ends with \.
>>> readline = Parser().readline
>>> readline('hello $name!\nbye!')
(, 'bye!')
>>> readline('hello $name!\\\nbye!')
(, 'bye!')
>>> readline('$f()\n\n')
(, '\n')
"""
line, text = splitline(text)
# suppress new line if line ends with \
if line.endswith("\\\n"):
line = line[:-2]
nodes = []
while line:
node, line = self.read_node(line)
nodes.append(node)
return LineNode(nodes), text
def read_node(self, text):
r"""Reads a node from the given text and returns the node and remaining text.
>>> read_node = Parser().read_node
>>> read_node('hello $name')
(t'hello ', '$name')
>>> read_node('$name')
($name, '')
"""
if text.startswith("$$"):
return TextNode("$"), text[2:]
elif text.startswith("$#"): # comment
line, text = splitline(text)
return TextNode("\n"), text
elif text.startswith("$"):
text = text[1:] # strip $
if text.startswith(":"):
escape = False
text = text[1:] # strip :
else:
escape = True
return self.read_expr(text, escape=escape)
else:
return self.read_text(text)
def read_text(self, text):
r"""Reads a text node from the given text.
>>> read_text = Parser().read_text
>>> read_text('hello $name')
(t'hello ', '$name')
"""
index = text.find("$")
if index < 0:
return TextNode(text), ""
else:
return TextNode(text[:index]), text[index:]
def read_keyword(self, text):
line, text = splitline(text)
return StatementNode(line.strip() + "\n"), text
def read_expr(self, text, escape=True): # noqa: C901, PLR0915
"""Reads a python expression from the text and returns the expression and remaining text.
expr -> simple_expr | paren_expr
simple_expr -> id extended_expr
extended_expr -> attr_access | paren_expr extended_expr | ''
attr_access -> dot id extended_expr
paren_expr -> [ tokens ] | ( tokens ) | { tokens }
>>> read_expr = Parser().read_expr
>>> read_expr("name")
($name, '')
>>> read_expr("a.b and c")
($a.b, ' and c')
>>> read_expr("a. b")
($a, '. b')
>>> read_expr("name")
($name, '')
>>> read_expr("(limit)ing")
($(limit), 'ing')
>>> read_expr('a[1, 2][:3].f(1+2, "weird string[).", 3 + 4) done.')
($a[1, 2][:3].f(1+2, "weird string[).", 3 + 4), ' done.')
"""
def simple_expr():
identifier()
extended_expr()
def identifier():
return next(tokens)
def extended_expr():
lookahead = tokens.peek()
if lookahead is None:
return
elif lookahead.value == ".":
attr_access()
elif lookahead.value in parens:
paren_expr()
extended_expr()
else:
return
def attr_access():
from token import NAME # python token constants
if tokens[1].type == NAME:
next(tokens) # consume dot
identifier()
extended_expr()
def paren_expr():
begin = next(tokens).value
end = parens[begin]
while True:
if tokens.peek().value in parens:
paren_expr()
else:
t = next(tokens)
if t.value == end:
break
parens = {"(": ")", "[": "]", "{": "}"}
def get_tokens(text: str):
"""tokenize text using python tokenizer.
Python tokenizer ignores spaces, but they might be important in some cases.
This function introduces dummy space tokens when it identifies any ignored space.
Each token is a storage object containing type, value, begin and end.
"""
def tokenize_text(input_text):
i = iter([input_text])
readline = lambda: next(i)
end = None
for t in tokenize.generate_tokens(readline):
t = storage(type=t[0], value=t[1], begin=t[2], end=t[3])
if end is not None and end != t.begin:
_, x1 = end
_, x2 = t.begin
yield storage(
type=-1, value=input_text[x1:x2], begin=end, end=t.begin
)
end = t.end
yield t
try:
yield from tokenize_text(text)
except tokenize.TokenError as e:
# Things like unterminated string literals or EOF in multi-line literals will raise exceptions
# tokenize the error free portion, then return an error token with the rest of the text
error_pos = e.args[1][1] - 1
fixed_text = text[0:error_pos]
yield from itertools.chain(
tokenize_text(fixed_text),
error_token_generator(text, error_pos + 1, len(text)),
)
def error_token_generator(text, start, end):
yield storage(
type=token.ERRORTOKEN, value=text[start:], begin=start, end=end
)
class peekable2(peekable):
"""
A peekable class which caches the last item returned by next()
"""
def __init__(self, iterable):
super().__init__(iterable)
self.current_item = None
def __next__(self):
self.current_item = super().__next__()
return self.current_item
tokens = peekable2(get_tokens(text))
if tokens.peek().value in parens:
paren_expr()
else:
simple_expr()
row, col = tokens.current_item.end
return ExpressionNode(text[:col], escape=escape), text[col:]
def read_assignment(self, text):
r"""Reads assignment statement from text.
>>> read_assignment = Parser().read_assignment
>>> read_assignment('a = b + 1\nfoo')
(, 'foo')
"""
line, text = splitline(text)
return AssignmentNode(line.strip()), text
def python_lookahead(self, text):
"""Returns the first python token from the given text.
>>> python_lookahead = Parser().python_lookahead
>>> python_lookahead('for i in range(10):')
'for'
>>> python_lookahead('else:')
'elsweb/template.py
>>> python_lookahead(' x = 1')
' '
"""
i = iter([text])
readline = lambda: next(i)
tokens = tokenize.generate_tokens(readline)
return next(tokens)[1]
def python_tokens(self, text):
i = iter([text])
readline = lambda: next(i)
tokens = tokenize.generate_tokens(readline)
return [t[1] for t in tokens]
def read_indented_block(self, text, indent):
r"""Read a block of text. A block is what typically follows a for or it statement.
It can be in the same line as that of the statement or an indented block.
>>> read_indented_block = Parser().read_indented_block
>>> read_indented_block(' a\n b\nc', ' ')
('a\nb\n', 'c')
>>> read_indented_block(' a\n b\n c\nd', ' ')
('a\n b\nc\n', 'd')
>>> read_indented_block(' a\n\n b\nc', ' ')
('a\n\n b\n', 'c')
"""
if indent == "":
return "", text
block = ""
while text:
line, text2 = splitline(text)
if line.strip() == "":
block += "\n"
elif line.startswith(indent):
block += line[len(indent) :]
else:
break
text = text2
return block, text
def read_statement(self, text):
r"""Reads a python statement.
>>> read_statement = Parser().read_statement
>>> read_statement('for i in range(10): hello $name')
('for i in range(10):', ' hello $name')
"""
tok = PythonTokenizer(text)
tok.consume_till(":")
return text[: tok.index], text[tok.index :]
def read_block_section(self, text, begin_indent=""):
r"""
>>> read_block_section = Parser().read_block_section
>>> read_block_section('for i in range(10): hello $i\nfoo')
(]>, 'foo')
>>> read_block_section('for i in range(10):\n hello $i\n foo', begin_indent=' ')
(]>, ' foo')
>>> read_block_section('for i in range(10):\n hello $i\nfoo')
(]>, 'foo')
With inline comment:
>>> read_block_section('for i in range(10): $# inline comment\n hello $i\nfoo')
(, ' hello $i\nfoo')
"""
line, text = splitline(text)
stmt, line = self.read_statement(line)
keyword = self.python_lookahead(stmt)
# if there is some thing left in the line
if line.strip() and not line.lstrip().startswith("$#"):
block = line.lstrip()
else:
def find_indent(text):
rx = re_compile(" +")
match = rx.match(text)
first_indent = match and match.group(0)
return first_indent or ""
# find the indentation of the block by looking at the first line
first_indent = find_indent(text)[len(begin_indent) :]
# TODO: fix this special case
if keyword == "code":
indent = begin_indent + first_indent
else:
indent = begin_indent + min(first_indent, INDENT)
block, text = self.read_indented_block(text, indent)
return self.create_block_node(keyword, stmt, block, begin_indent), text
def create_block_node(self, keyword, stmt, block, begin_indent):
if keyword in self.statement_nodes:
return self.statement_nodes[keyword](stmt, block, begin_indent)
else:
raise ParseError("Unknown statement: %s" % repr(keyword))
class PythonTokenizer:
"""Utility wrapper over python tokenizer."""
def __init__(self, text):
self.text = text
i = iter([text])
readline = lambda: next(i)
self.tokens = tokenize.generate_tokens(readline)
self.index = 0
def consume_till(self, delim):
"""Consumes tokens till colon.
>>> tok = PythonTokenizer('for i in range(10): hello $i')
>>> tok.consume_till(':')
>>> tok.text[:tok.index]
'for i in range(10):'
>>> tok.text[tok.index:]
' hello $i'
"""
try:
while True:
t = next(self)
if t.value == delim:
break
elif t.value == "(":
self.consume_till(")")
elif t.value == "[":
self.consume_till("]")
elif t.value == "{":
self.consume_till("}")
# if end of line is found, it is an exception.
# Since there is no easy way to report the line number,
# leave the error reporting to the python parser later
# @@ This should be fixed.
if t.value == "\n":
break
except:
# raise ParseError, "Expected %s, found end of line." % repr(delim)
# raising ParseError doesn't show the line number.
# if this error is ignored, then it will be caught when compiling the python code.
return
def __next__(self):
type, t, begin, end, line = next(self.tokens)
row, col = end
self.index = col
return storage(type=type, value=t, begin=begin, end=end)
class DefwithNode:
def __init__(self, defwith, suite):
if defwith:
self.defwith = defwith.replace("with", "__template__") + ":"
# offset 4 lines. for encoding, _lineoffset_, loop and self.
self.defwith += "\n _lineoffset_ = -4"
else:
self.defwith = "def __template__():"
# offset 4 lines for encoding, __template__, _lineoffset_, loop and self.
self.defwith += "\n _lineoffset_ = -5"
self.defwith += "\n loop = ForLoop()"
self.defwith += "\n self = TemplateResult(); extend_ = self.extend"
self.suite = suite
self.end = "\n return self"
def emit(self, indent):
encoding = "# coding: utf-8\n"
return encoding + self.defwith + self.suite.emit(indent + INDENT) + self.end
def __repr__(self):
return f""
class TextNode:
def __init__(self, value):
self.value = value
def emit(self, indent, begin_indent=""):
return repr(safeunicode(self.value))
def __repr__(self):
return "t" + repr(self.value)
class ExpressionNode:
def __init__(self, value, escape=True):
self.value = value.strip()
# convert ${...} to $(...)
if value.startswith("{") and value.endswith("}"):
self.value = "(" + self.value[1:-1] + ")"
self.escape = escape
def emit(self, indent, begin_indent=""):
return f"escape_({self.value}, {bool(self.escape)})"
def __repr__(self):
if self.escape:
escape = ""
else:
escape = ":"
return f"${escape}{self.value}"
class AssignmentNode:
def __init__(self, code):
self.code = code
def emit(self, indent, begin_indent=""):
return indent + self.code + "\n"
def __repr__(self):
return "" % repr(self.code)
class LineNode:
def __init__(self, nodes):
self.nodes = nodes
def emit(self, indent, text_indent="", name=""):
text = [node.emit("") for node in self.nodes]
if text_indent:
text = [repr(text_indent)] + text
return indent + "extend_([%s])\n" % ", ".join(text)
def __repr__(self):
return "" % repr(self.nodes)
INDENT = " " # 4 spaces
class BlockNode:
def __init__(self, stmt, block, begin_indent=""):
self.stmt = stmt
self.suite = Parser().read_suite(block)
self.begin_indent = begin_indent
def emit(self, indent, text_indent=""):
text_indent = self.begin_indent + text_indent
out = indent + self.stmt + self.suite.emit(indent + INDENT, text_indent)
return out
def __repr__(self):
return f""
class ForNode(BlockNode):
def __init__(self, stmt, block, begin_indent=""):
self.original_stmt = stmt
tok = PythonTokenizer(stmt)
tok.consume_till("in")
a = stmt[: tok.index] # for i in
b = stmt[tok.index : -1] # rest of for stmt excluding :
stmt = a + " loop.setup(" + b.strip() + "):"
BlockNode.__init__(self, stmt, block, begin_indent)
def __repr__(self):
return f""
class CodeNode:
def __init__(self, stmt, block, begin_indent=""):
# compensate one line for $code:
self.code = "\n" + block
def emit(self, indent, text_indent=""):
import re
rx = re.compile("^", re.M)
return rx.sub(indent, self.code).rstrip(" ")
def __repr__(self):
return "" % repr(self.code)
class StatementNode:
def __init__(self, stmt):
self.stmt = stmt
def emit(self, indent, begin_indent=""):
return indent + self.stmt
def __repr__(self):
return "" % repr(self.stmt)
class IfNode(BlockNode):
pass
class ElseNode(BlockNode):
pass
class ElifNode(BlockNode):
pass
class DefNode(BlockNode):
def __init__(self, *a, **kw):
BlockNode.__init__(self, *a, **kw)
code = CodeNode("", "")
code.code = "self = TemplateResult(); extend_ = self.extend\n"
self.suite.sections.insert(0, code)
code = CodeNode("", "")
code.code = "return self\n"
self.suite.sections.append(code)
def emit(self, indent, text_indent=""):
text_indent = self.begin_indent + text_indent
out = indent + self.stmt + self.suite.emit(indent + INDENT, text_indent)
return indent + "_lineoffset_ -= 3\n" + out
class VarNode:
def __init__(self, name, value):
self.name = name
self.value = value
def emit(self, indent, text_indent):
return indent + f"self[{repr(self.name)}] = {self.value}\n"
def __repr__(self):
return f""
class SuiteNode:
"""Suite is a list of sections."""
def __init__(self, sections):
self.sections = sections
def emit(self, indent, text_indent=""):
return "\n" + "".join([s.emit(indent, text_indent) for s in self.sections])
def __repr__(self):
return repr(self.sections)
STATEMENT_NODES = {
"for": ForNode,
"while": BlockNode,
"if": IfNode,
"elif": ElifNode,
"else": ElseNode,
"def": DefNode,
"code": CodeNode,
}
KEYWORDS = ["pass", "break", "continue", "return"]
TEMPLATE_BUILTIN_NAMES = [
"dict",
"enumerate",
"float",
"int",
"bool",
"list",
"long",
"reversed",
"set",
"slice",
"tuple",
"xrange",
"abs",
"all",
"any",
"callable",
"chr",
"cmp",
"divmod",
"filter",
"hex",
"id",
"isinstance",
"iter",
"len",
"max",
"min",
"oct",
"ord",
"pow",
"range",
"round",
"True",
"False",
"None",
"__import__", # some c-libraries like datetime requires __import__ to present in the namespace
]
TEMPLATE_BUILTINS = {
name: getattr(builtins, name)
for name in TEMPLATE_BUILTIN_NAMES
if name in builtins.__dict__
}
class ForLoop:
"""
Wrapper for expression in for stament to support loop.xxx helpers.
>>> loop = ForLoop()
>>> for x in loop.setup(['a', 'b', 'c']):
... print(loop.index, loop.revindex, loop.parity, x)
...
1 3 odd a
2 2 even b
3 1 odd c
>>> loop.index
Traceback (most recent call last):
...
AttributeError: index
"""
def __init__(self):
self._ctx = None
def __getattr__(self, name):
if self._ctx is None:
raise AttributeError(name)
else:
return getattr(self._ctx, name)
def setup(self, seq):
self._push()
return self._ctx.setup(seq)
def _push(self):
self._ctx = ForLoopContext(self, self._ctx)
def _pop(self):
self._ctx = self._ctx.parent
class ForLoopContext:
"""Stackable context for ForLoop to support nested for loops."""
def __init__(self, forloop, parent):
self._forloop = forloop
self.parent = parent
def setup(self, seq):
try:
self.length = len(seq)
except:
self.length = 0
self.index = 0
for a in seq:
self.index += 1
yield a
self._forloop._pop()
index0 = property(lambda self: self.index - 1)
first = property(lambda self: self.index == 1)
last = property(lambda self: self.index == self.length)
odd = property(lambda self: self.index % 2 == 1)
even = property(lambda self: self.index % 2 == 0)
parity = property(lambda self: ["odd", "even"][self.even])
revindex0 = property(lambda self: self.length - self.index)
revindex = property(lambda self: self.length - self.index + 1)
class BaseTemplate:
def __init__(self, code, filename, filter, globals, builtins):
self.filename = filename
self.filter = filter
self._globals = globals
self._builtins = builtins
if code:
self.t = self._compile(code)
else:
self.t = lambda: ""
def _compile(self, code):
env = self.make_env(self._globals or {}, self._builtins)
exec(code, env)
# __template__ is a global function declared when executing "code"
return env["__template__"]
def __call__(self, *a, **kw):
__hidetraceback__ = True # noqa: F841
return self.t(*a, **kw)
def make_env(self, globals, builtins_):
if sys.implementation.name == "pypy":
# Pypy's `__builtins__` can't be overridden in exec. More details see issue #598.
overridden_builtins = builtins.__dict__.keys() - builtins_.keys()
def f(name, *args, **kwargs):
raise NameError("name '%s' is not defined" % name)
for name in overridden_builtins:
if name not in globals:
globals[name] = partial(f, name)
return dict(
globals,
__builtins__=builtins_,
ForLoop=ForLoop,
TemplateResult=TemplateResult,
escape_=self._escape,
join_=self._join,
)
def _join(self, *items):
return "".join(items)
def _escape(self, value, escape=False):
if value is None:
value = ""
value = safeunicode(value)
if escape and self.filter:
value = self.filter(value)
return value
class Template(BaseTemplate):
CONTENT_TYPES = {
".html": "text/html; charset=utf-8",
".xhtml": "application/xhtml+xml; charset=utf-8",
".txt": "text/plain",
}
FILTERS = {".html": websafe, ".xhtml": websafe, ".xml": websafe}
globals = {}
def __init__(
self,
text,
filename="",
filter=None,
globals=None,
builtins=None,
extensions=None,
):
self.extensions = extensions or []
text = Template.normalize_text(text)
code = self.compile_template(text, filename)
_, ext = os.path.splitext(filename)
filter = filter or self.FILTERS.get(ext, None)
self.content_type = self.CONTENT_TYPES.get(ext, None)
if globals is None:
globals = self.globals
if builtins is None:
builtins = TEMPLATE_BUILTINS
BaseTemplate.__init__(
self,
code=code,
filename=filename,
filter=filter,
globals=globals,
builtins=builtins,
)
def __repr__(self):
"""
>>> Template(text='Template text', filename='burndown_chart.html')
"""
return f"<{self.__class__.__name__} {self.filename}>"
def normalize_text(text):
"""Normalizes template text by correcting \r\n, tabs and BOM chars."""
text = text.replace("\r\n", "\n").replace("\r", "\n").expandtabs()
if not text.endswith("\n"):
text += "\n"
# ignore BOM chars at the beginning of template
BOM = "\xef\xbb\xbf"
if isinstance(text, str) and text.startswith(BOM):
text = text[len(BOM) :]
# support fort \$ for backward-compatibility
text = text.replace(r"\$", "$$")
return text
normalize_text = staticmethod(normalize_text)
def __call__(self, *a, **kw):
__hidetraceback__ = True # noqa: F841
from . import webapi as web
if "headers" in web.ctx and self.content_type:
web.header("Content-Type", self.content_type, unique=True)
return BaseTemplate.__call__(self, *a, **kw)
def generate_code(text, filename, parser=None):
# parse the text
parser = parser or Parser()
rootnode = parser.parse(text, filename)
# generate python code from the parse tree
code = rootnode.emit(indent="").strip()
return safestr(code)
generate_code = staticmethod(generate_code)
def create_parser(self):
p = Parser()
for ext in self.extensions:
p = ext(p)
return p
def compile_template(self, template_string, filename):
code = Template.generate_code(
template_string, filename, parser=self.create_parser()
)
def get_source_line(filename, lineno):
try:
lines = open(filename, encoding="utf-8").read().splitlines()
return lines[lineno]
except:
return None
try:
# compile the code first to report the errors, if any, with the filename
compiled_code = compile(code, filename, "exec")
except SyntaxError as err:
# display template line that caused the error along with the traceback.
err.msg += f"\n\nTemplate traceback:\n File {repr(err.filename)}, line {err.lineno}\n {get_source_line(err.filename, err.lineno - 1)}"
raise
# make sure code is safe
ast_node = ast.parse(code, filename)
SafeVisitor().walk(ast_node, filename)
return compiled_code
class CompiledTemplate(Template):
def __init__(self, f, filename):
Template.__init__(self, "", filename)
self.t = f
def compile_template(self, *a):
return None
def _compile(self, *a):
return None
class Render:
"""The most preferred way of using templates.
render = web.template.render('templates')
print render.foo()
Optional parameter can be `base` can be used to pass output of
every template through the base template.
render = web.template.render('templates', base='layout')
"""
def __init__(self, loc="templates", cache=None, base=None, **keywords):
self._loc = loc
self._keywords = keywords
if cache is None:
cache = not config.get("debug", False)
if cache:
self._cache = {}
else:
self._cache = None
if base and not hasattr(base, "__call__"):
# make base a function, so that it can be passed to sub-renders
self._base = lambda page: self._template(base)(page)
else:
self._base = base
def _add_global(self, obj, name=None):
"""Add a global to this rendering instance."""
if "globals" not in self._keywords:
self._keywords["globals"] = {}
if not name:
name = obj.__name__
self._keywords["globals"][name] = obj
def _lookup(self, name):
path = os.path.join(self._loc, name)
if os.path.isdir(path):
return "dir", path
else:
path = self._findfile(path)
if path:
return "file", path
else:
return "none", None
def _load_template(self, name):
kind, path = self._lookup(name)
if kind == "dir":
return Render(
path, cache=self._cache is not None, base=self._base, **self._keywords
)
elif kind == "file":
with open(path, encoding="utf-8") as tmpl_file:
return Template(tmpl_file.read(), filename=path, **self._keywords)
else:
raise AttributeError("No template named " + name)
def _findfile(self, path_prefix):
p = [
f for f in glob.glob(path_prefix + ".*") if not f.endswith("~")
] # skip backup files
p.sort() # sort the matches for deterministic order
# support templates without extension (#364)
# When no templates are found and a file is found with the exact name, use it.
if not p and os.path.exists(path_prefix):
p = [path_prefix]
return p and p[0]
def _template(self, name):
if self._cache is not None:
if name not in self._cache:
self._cache[name] = self._load_template(name)
return self._cache[name]
else:
return self._load_template(name)
def __getattr__(self, name):
t = self._template(name)
if self._base and isinstance(t, Template):
def template(*a, **kw):
return self._base(t(*a, **kw))
return template
else:
return self._template(name)
class GAE_Render(Render):
# Render gets over-written. make a copy here.
super = Render
def __init__(self, loc, *a, **kw):
GAE_Render.super.__init__(self, loc, *a, **kw)
import types
if isinstance(loc, types.ModuleType):
self.mod = loc
else:
name = loc.rstrip("/").replace("/", ".")
self.mod = __import__(name, None, None, ["x"])
self.mod.__dict__.update(kw.get("builtins", TEMPLATE_BUILTINS))
self.mod.__dict__.update(Template.globals)
self.mod.__dict__.update(kw.get("globals", {}))
def _load_template(self, name):
t = getattr(self.mod, name)
import types
if isinstance(t, types.ModuleType):
return GAE_Render(
t, cache=self._cache is not None, base=self._base, **self._keywords
)
else:
return t
render = Render
# setup render for Google App Engine.
try:
from google import appengine # noqa: F401
render = Render = GAE_Render
except ImportError:
pass
def frender(path, **keywords):
"""Creates a template from the given file path."""
return Template(open(path, encoding="utf-8").read(), filename=path, **keywords)
def compile_templates(root):
"""Compiles templates to python code."""
for dirpath, dirnames, filenames in os.walk(root):
filenames = [
f
for f in filenames
if not f.startswith(".")
and not f.endswith("~")
and not f.startswith("__init__.py")
]
for d in dirnames[:]:
if d.startswith("."):
dirnames.remove(d) # don't visit this dir
out = open(os.path.join(dirpath, "__init__.py"), "w", encoding="utf-8")
out.write(
"from web.template import CompiledTemplate, ForLoop, TemplateResult\n\n"
)
if dirnames:
out.write("import " + ", ".join(dirnames))
out.write("\n")
for f in filenames:
path = os.path.join(dirpath, f)
if "." in f:
name, _ = f.split(".", 1)
else:
name = f
text = open(path, encoding="utf-8").read()
text = Template.normalize_text(text)
code = Template.generate_code(text, path)
code = code.replace("__template__", name, 1)
out.write(code)
out.write("\n\n")
out.write(f"{name} = CompiledTemplate({name}, {repr(path)})\n")
out.write(f"join_ = {name}._join; escape_ = {name}._escape\n\n")
# create template to make sure it compiles
Template(open(path, encoding="utf-8").read(), path)
out.close()
class ParseError(Exception):
pass
class SecurityError(Exception):
"""The template seems to be trying to do something naughty."""
pass
ALLOWED_AST_NODES = [
"Add",
"And",
"Assign",
"Attribute",
"AugAssign",
"AugLoad",
"AugStore",
"BinOp",
"BitAnd",
"BitOr",
"BitXor",
"BoolOp",
"Break",
"Call",
"ClassDef",
"Compare",
"Constant",
"Continue",
"Del",
"Delete",
"Dict",
"DictComp",
"Div",
"Ellipsis",
"Eq",
"ExceptHandler",
"Expr",
"Expression",
"ExtSlice",
"FloorDiv",
"For",
"FunctionDef",
"GeneratorExp",
"Gt",
"GtE",
"If",
"IfExp",
"In",
"Index",
"Interactive",
"Invert",
"Is",
"IsNot",
"JoinedStr",
"LShift",
"Lambda",
"List",
"ListComp",
"Load",
"Lt",
"LtE",
"Mod",
"Module",
"Mult",
"Name",
"NameConstant",
"Not",
"NotEq",
"NotIn",
"Num",
"Or",
"Param",
"Pass",
"Pow",
"RShift",
"Return",
"Set",
"SetComp",
"Slice",
"Store",
"Str",
"Sub",
"Subscript",
"Suite",
"Tuple",
"UAdd",
"USub",
"UnaryOp",
"While",
"With",
"Yield",
"alias",
"arg",
"arguments",
"comprehension",
"keyword",
]
# Assert Exec Global Import ImportFrom Print Raise Repr TryExcept TryFinally
class SafeVisitor(ast.NodeVisitor):
"""
Make sure code is safe by walking through the AST.
Code considered unsafe if:
* it has restricted AST nodes (only nodes defined in ALLOWED_AST_NODES are allowed)
* it is trying to assign to attributes
* it is trying to access resricted attributes
Adopted from http://www.zafar.se/bkz/uploads/safe.txt (public domain, Babar K. Zafar)
* Using ast rather than compiler tree, for jython and Py3 support since Py2.6
* Simplified with ast.NodeVisitor class
"""
def __init__(self, *args, **kwargs):
"Initialize visitor by generating callbacks for all AST node types."
super().__init__(*args, **kwargs)
self.errors = []
def walk(self, tree, filename):
"Validate each node in AST and raise SecurityError if the code is not safe."
self.filename = filename
self.visit(tree)
if self.errors:
raise SecurityError("\n".join([str(err) for err in self.errors]))
def generic_visit(self, node):
nodename = type(node).__name__
if nodename not in ALLOWED_AST_NODES:
self.fail_node(node, nodename)
super().generic_visit(node)
def visit_Name(self, node):
if node.id.startswith("__"):
self.fail_name(node)
def visit_Attribute(self, node):
attrname = self.get_node_attr(node)
if self.is_unallowed_attr(attrname):
self.fail_attribute(node, attrname)
super().generic_visit(node)
def visit_Assign(self, node):
self.check_assign_targets(node)
def visit_AugAssign(self, node):
self.check_assign_target(node)
def check_assign_targets(self, node):
for target in node.targets:
self.check_assign_target(target)
super().generic_visit(node)
def check_assign_target(self, targetnode):
targetname = type(targetnode).__name__
if targetname == "Attribute":
attrname = self.get_node_attr(targetnode)
self.fail_attribute(targetnode, attrname)
# failure modes
def fail_node(self, node, nodename):
lineno = self.get_node_lineno(node)
e = SecurityError(
"%s:%d - execution of '%s' statements is denied"
% (self.filename, lineno, nodename)
)
self.errors.append(e)
def fail_attribute(self, node, attrname):
lineno = self.get_node_lineno(node)
e = SecurityError(
"%s:%d - access to attribute '%s' is denied"
% (self.filename, lineno, attrname)
)
self.errors.append(e)
def fail_name(self, node):
lineno = self.get_node_lineno(node)
e = SecurityError(
"%s:%d - access to name '%s' is denied" % (self.filename, lineno, node.id)
)
self.errors.append(e)
# helpers
def is_unallowed_attr(self, name):
return (
name.startswith("_") or name.startswith("func_") or name.startswith("im_")
)
def get_node_attr(self, node):
return "attr" in node._fields and node.attr or None
def get_node_lineno(self, node):
return (node.lineno) and node.lineno or 0
class TemplateResult(MutableMapping):
"""Dictionary like object for storing template output.
The result of a template execution is usually a string, but sometimes it
contains attributes set using $var. This class provides a simple
dictionary like interface for storing the output of the template and the
attributes. The output is stored with a special key __body__. Converting
the TemplateResult to string or unicode returns the value of __body__.
When the template is in execution, the output is generated part by part
and those parts are combined at the end. Parts are added to the
TemplateResult by calling the `extend` method and the parts are combined
seamlessly when __body__ is accessed.
>>> d = TemplateResult(__body__='hello, world', x='foo')
>>> print(d)
hello, world
>>> d.x
'foo'
>>> d = TemplateResult()
>>> d.extend([u'hello', u'world'])
>>> d
"""
def __init__(self, *a, **kw):
self.__dict__["_d"] = dict(*a, **kw)
self._d.setdefault("__body__", "")
self.__dict__["_parts"] = []
self.__dict__["extend"] = self._parts.extend
self._d.setdefault("__body__", None)
def keys(self):
return self._d.keys()
def _prepare_body(self):
"""Prepare value of __body__ by joining parts."""
if self._parts:
value = "".join(self._parts)
self._parts[:] = []
body = self._d.get("__body__")
if body:
self._d["__body__"] = body + value
else:
self._d["__body__"] = value
def __getitem__(self, name):
if name == "__body__":
self._prepare_body()
return self._d[name]
def __setitem__(self, name, value):
if name == "__body__":
self._prepare_body()
return self._d.__setitem__(name, value)
def __delitem__(self, name):
if name == "__body__":
self._prepare_body()
return self._d.__delitem__(name)
def __getattr__(self, key):
try:
return self[key]
except KeyError as k:
raise AttributeError(k)
def __setattr__(self, key, value):
self[key] = value
def __delattr__(self, key):
try:
del self[key]
except KeyError as k:
raise AttributeError(k)
def __unicode__(self):
self._prepare_body()
return self["__body__"]
def __str__(self):
self._prepare_body()
return self["__body__"]
def __repr__(self):
self._prepare_body()
return "" % self._d
def __len__(self):
return self._d.__len__()
def __iter__(self):
for i in self._d.__iter__():
if i == "__body__":
self._prepare_body()
yield i
def test():
r"""Doctest for testing template module.
Define a utility function to run template test.
>>> class TestResult:
... def __init__(self, t): self.t = t
... def __getattr__(self, name): return getattr(self.t, name)
... def __repr__(self): return str(self.t)
...
>>> def t(code, **keywords):
... tmpl = Template(code, **keywords)
... return lambda *a, **kw: TestResult(tmpl(*a, **kw))
...
Simple tests.
>>> t('1')()
u'1\n'
>>> t('$def with ()\n1')()
u'1\n'
>>> t('$def with (a)\n$a')(1)
u'1\n'
>>> t('$def with (a=0)\n$a')(1)
u'1\n'
>>> t('$def with (a=0)\n$a')(a=1)
u'1\n'
Test complicated expressions.
>>> t('$def with (x)\n$x.upper()')('hello')
u'HELLO\n'
>>> t('$(2 * 3 + 4 * 5)')()
u'26\n'
>>> t('${2 * 3 + 4 * 5}')()
u'26\n'
>>> t('$def with (limit)\nkeep $(limit)ing.')('go')
u'keep going.\n'
>>> t('$def with (a)\n$a.b[0]')(storage(b=[1]))
u'1\n'
Test html escaping.
>>> t('$def with (x)\n$x', filename='a.html')('')
u'<html>\n'
>>> t('$def with (x)\n$x', filename='a.txt')('')
u'\n'
Test if, for and while.
>>> t('$if 1: 1')()
u'1\n'
>>> t('$if 1:\n 1')()
u'1\n'
>>> t('$if 1:\n 1\\')()
u'1'
>>> t('$if 0: 0\n$elif 1: 1')()
u'1\n'
>>> t('$if 0: 0\n$elif None: 0\n$else: 1')()
u'1\n'
>>> t('$if 0 < 1 and 1 < 2: 1')()
u'1\n'
>>> t('$for x in [1, 2, 3]: $x')()
u'1\n2\n3\n'
>>> t('$def with (d)\n$for k, v in d.items(): $k')({1: 1})
u'1\n'
>>> t('$for x in [1, 2, 3]:\n\t$x')()
u' 1\n 2\n 3\n'
>>> t('$def with (a)\n$while a and a.pop():1')([1, 2, 3])
u'1\n1\n1\n'
The space after : must be ignored.
>>> t('$if True: foo')()
u'foo\n'
Test loop.xxx.
>>> t("$for i in range(5):$loop.index, $loop.parity")()
u'1, odd\n2, even\n3, odd\n4, even\n5, odd\n'
>>> t("$for i in range(2):\n $for j in range(2):$loop.parent.parity $loop.parity")()
u'odd odd\nodd even\neven odd\neven even\n'
Test assignment.
>>> t('$ a = 1\n$a')()
u'1\n'
>>> t('$ a = [1]\n$a[0]')()
u'1\n'
>>> t('$ a = {1: 1}\n$list(a.keys())[0]')()
u'1\n'
>>> t('$ a = []\n$if not a: 1')()
u'1\n'
>>> t('$ a = {}\n$if not a: 1')()
u'1\n'
>>> t('$ a = -1\n$a')()
u'-1\n'
>>> t('$ a = "1"\n$a')()
u'1\n'
Test comments.
>>> t('$# 0')()
u'\n'
>>> t('hello$#comment1\nhello$#comment2')()
u'hello\nhello\n'
>>> t('$#comment0\nhello$#comment1\nhello$#comment2')()
u'\nhello\nhello\n'
Test unicode.
>>> t('$def with (a)\n$a')(u'\u203d')
u'\u203d\n'
>>> t(u'$def with (a)\n$a $:a')(u'\u203d')
u'\u203d \u203d\n'
>>> t(u'$def with ()\nfoo')()
u'foo\n'
>>> def f(x): return x
...
>>> t(u'$def with (f)\n$:f("x")')(f)
u'x\n'
>>> t('$def with (f)\n$:f("x")')(f)
u'x\n'
Test dollar escaping.
>>> t("Stop, $$money isn't evaluated.")()
u"Stop, $money isn't evaluated.\n"
>>> t("Stop, \$money isn't evaluated.")()
u"Stop, $money isn't evaluated.\n"
Test space sensitivity.
>>> t('$def with (x)\n$x')(1)
u'1\n'
>>> t('$def with(x ,y)\n$x')(1, 1)
u'1\n'
>>> t('$(1 + 2*3 + 4)')()
u'11\n'
Make sure globals are working.
>>> t('$x')()
Traceback (most recent call last):
...
NameError: global name 'x' is not defined
>>> t('$x', globals={'x': 1})()
u'1\n'
Can't change globals.
>>> t('$ x = 2\n$x', globals={'x': 1})()
u'2\n'
>>> t('$ x = x + 1\n$x', globals={'x': 1})()
Traceback (most recent call last):
...
UnboundLocalError: local variable 'x' referenced before assignment
Make sure builtins are customizable.
>>> t('$min(1, 2)')()
u'1\n'
>>> t('$min(1, 2)', builtins={})()
Traceback (most recent call last):
...
NameError: global name 'min' is not defined
Test vars.
>>> x = t('$var x: 1')()
>>> x.x
u'1'
>>> x = t('$var x = 1')()
>>> x.x
1
>>> x = t('$var x: \n foo\n bar')()
>>> x.x
u'foo\nbar\n'
Test BOM chars.
>>> t('\xef\xbb\xbf$def with(x)\n$x')('foo')
u'foo\n'
Test for with weird cases.
>>> t('$for i in range(10)[1:5]:\n $i')()
u'1\n2\n3\n4\n'
>>> t("$for k, v in sorted({'a': 1, 'b': 2}.items()):\n $k $v", globals={'sorted':sorted})()
u'a 1\nb 2\n'
Test for syntax error.
>>> try:
... t("$for k, v in ({'a': 1, 'b': 2}.items():\n $k $v")()
... except SyntaxError:
... print("OK")
... else:
... print("Expected SyntaxError")
...
OK
Test datetime.
>>> import datetime
>>> t("$def with (date)\n$date.strftime('%m %Y')")(datetime.datetime(2009, 1, 1))
u'01 2009\n'
"""
pass
if __name__ == "__main__":
if "--compile" in sys.argv:
compile_templates(sys.argv[2])
else:
import doctest
doctest.testmod()
webpy-webpy-99e5eed/web/test.py 0000664 0000000 0000000 00000002540 15206666350 0016617 0 ustar 00root root 0000000 0000000 """test utilities
(part of web.py)
"""
import doctest
import sys
import unittest
TestCase = unittest.TestCase
TestSuite = unittest.TestSuite
def load_modules(names):
return [__import__(name, None, None, "x") for name in names]
def module_suite(module, classnames=None):
"""Makes a suite from a module."""
if classnames:
return unittest.TestLoader().loadTestsFromNames(classnames, module)
elif hasattr(module, "suite"):
return module.suite()
else:
return unittest.TestLoader().loadTestsFromModule(module)
def doctest_suite(module_names):
"""Makes a test suite from doctests."""
suite = TestSuite()
for mod in load_modules(module_names):
suite.addTest(doctest.DocTestSuite(mod))
return suite
def suite(module_names):
"""Creates a suite from multiple modules."""
suite = TestSuite()
for mod in load_modules(module_names):
suite.addTest(module_suite(mod))
return suite
def runTests(suite):
runner = unittest.TextTestRunner()
return runner.run(suite)
def main(suite=None):
if not suite:
main_module = __import__("__main__")
# allow command line switches
args = [a for a in sys.argv[1:] if not a.startswith("-")]
suite = module_suite(main_module, args or None)
result = runTests(suite)
sys.exit(not result.wasSuccessful())
webpy-webpy-99e5eed/web/utils.py 0000775 0000000 0000000 00000120047 15206666350 0017006 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
"""
General Utilities
(part of web.py)
"""
import datetime
import os
import re
import shutil
import subprocess
import sys
import threading
import time
import traceback
from io import StringIO
from threading import local as threadlocal
from .py3helpers import iteritems, itervalues
__all__ = [
"Storage",
"storage",
"storify",
"Counter",
"counter",
"iters",
"rstrips",
"lstrips",
"strips",
"safeunicode",
"safestr",
"timelimit",
"Memoize",
"memoize",
"re_compile",
"re_subm",
"group",
"uniq",
"iterview",
"IterBetter",
"iterbetter",
"safeiter",
"safewrite",
"dictreverse",
"dictfind",
"dictfindall",
"dictincr",
"dictadd",
"requeue",
"restack",
"listget",
"intget",
"datestr",
"numify",
"denumify",
"commify",
"dateify",
"nthstr",
"cond",
"CaptureStdout",
"capturestdout",
"Profile",
"profile",
"tryall",
"ThreadedDict",
"threadeddict",
"autoassign",
"to36",
"sendmail",
]
class Storage(dict):
"""
A Storage object is like a dictionary except `obj.foo` can be used
in addition to `obj['foo']`.
>>> o = storage(a=1)
>>> o.a
1
>>> o['a']
1
>>> o.a = 2
>>> o['a']
2
>>> del o.a
>>> o.a
Traceback (most recent call last):
...
AttributeError: 'a'
"""
def __getattr__(self, key):
try:
return self[key]
except KeyError as k:
raise AttributeError(k)
def __setattr__(self, key, value):
self[key] = value
def __delattr__(self, key):
try:
del self[key]
except KeyError as k:
raise AttributeError(k)
def __repr__(self):
return ""
storage = Storage
def storify(mapping, *requireds, **defaults):
"""
Creates a `storage` object from dictionary `mapping`, raising `KeyError` if
d doesn't have all of the keys in `requireds` and using the default
values for keys found in `defaults`.
For example, `storify({'a':1, 'c':3}, b=2, c=0)` will return the equivalent of
`storage({'a':1, 'b':2, 'c':3})`.
If a `storify` value is a list (e.g. multiple values in a form submission),
`storify` returns the last element of the list, unless the key appears in
`defaults` as a list. Thus:
>>> storify({'a':[1, 2]}).a
2
>>> storify({'a':[1, 2]}, a=[]).a
[1, 2]
>>> storify({'a':1}, a=[]).a
[1]
>>> storify({}, a=[]).a
[]
Similarly, if the value has a `value` attribute, `storify will return _its_
value, unless the key appears in `defaults` as a dictionary.
>>> storify({'a':storage(value=1)}).a
1
>>> storify({'a':storage(value=1)}, a={}).a
>>> storify({}, a={}).a
{}
"""
_unicode = defaults.pop("_unicode", False)
# if _unicode is callable object, use it convert a string to unicode.
to_unicode = safeunicode
if _unicode is not False and hasattr(_unicode, "__call__"):
to_unicode = _unicode
def unicodify(s):
if _unicode and isinstance(s, str):
return to_unicode(s)
else:
return s
def getvalue(x):
if hasattr(x, "file") and hasattr(x, "raw"):
return x.file.read()
else:
return unicodify(getattr(x, "value", x))
stor = Storage()
for key in requireds + tuple(mapping.keys()):
value = mapping[key]
if isinstance(value, list):
if isinstance(defaults.get(key), list):
value = [getvalue(x) for x in value]
else:
value = value[-1]
if not isinstance(defaults.get(key), dict):
value = getvalue(value)
if isinstance(defaults.get(key), list) and not isinstance(value, list):
value = [value]
setattr(stor, key, value)
for key, value in iteritems(defaults):
result = value
if hasattr(stor, key):
result = stor[key]
if value == () and not isinstance(result, tuple):
result = (result,)
setattr(stor, key, result)
return stor
class Counter(storage):
"""Keeps count of how many times something is added.
>>> c = counter()
>>> c.add('x')
>>> c.add('x')
>>> c.add('x')
>>> c.add('x')
>>> c.add('x')
>>> c.add('y')
>>> c['y']
1
>>> c['x']
5
>>> c.most()
['x']
"""
def add(self, n):
self.setdefault(n, 0)
self[n] += 1
def most(self):
"""Returns the keys with maximum count."""
m = max(itervalues(self))
return [k for k, v in iteritems(self) if v == m]
def least(self):
"""Returns the keys with minimum count."""
m = min(self.itervalues())
return [k for k, v in iteritems(self) if v == m]
def percent(self, key):
"""Returns what percentage a certain key is of all entries.
>>> c = counter()
>>> c.add('x')
>>> c.add('x')
>>> c.add('x')
>>> c.add('y')
>>> c.percent('x')
0.75
>>> c.percent('y')
0.25
"""
return float(self[key]) / sum(self.values())
def sorted_keys(self):
"""Returns keys sorted by value.
>>> c = counter()
>>> c.add('x')
>>> c.add('x')
>>> c.add('y')
>>> c.sorted_keys()
['x', 'y']
"""
return sorted(self.keys(), key=lambda k: self[k], reverse=True)
def sorted_values(self):
"""Returns values sorted by value.
>>> c = counter()
>>> c.add('x')
>>> c.add('x')
>>> c.add('y')
>>> c.sorted_values()
[2, 1]
"""
return [self[k] for k in self.sorted_keys()]
def sorted_items(self):
"""Returns items sorted by value.
>>> c = counter()
>>> c.add('x')
>>> c.add('x')
>>> c.add('y')
>>> c.sorted_items()
[('x', 2), ('y', 1)]
"""
return [(k, self[k]) for k in self.sorted_keys()]
def __repr__(self):
return ""
counter = Counter
iters = [list, tuple, set, frozenset]
class _hack(tuple):
pass
iters = _hack(iters)
iters.__doc__ = """
A list of iterable items (like lists, but not strings). Includes whichever
of lists, tuples, sets, and Sets are available in this version of Python.
"""
def _strips(direction, text, remove):
if isinstance(remove, iters):
for subr in remove:
text = _strips(direction, text, subr)
return text
if direction == "l":
if text.startswith(remove):
return text[len(remove) :]
elif direction == "r":
if text.endswith(remove):
return text[: -len(remove)]
else:
raise ValueError("Direction needs to be r or l.")
return text
def rstrips(text, remove):
"""
removes the string `remove` from the right of `text`
>>> rstrips("foobar", "bar")
'foo'
"""
return _strips("r", text, remove)
def lstrips(text, remove):
"""
removes the string `remove` from the left of `text`
>>> lstrips("foobar", "foo")
'bar'
>>> lstrips('http://foo.org/', ['http://', 'https://'])
'foo.org/'
>>> lstrips('FOOBARBAZ', ['FOO', 'BAR'])
'BAZ'
>>> lstrips('FOOBARBAZ', ['BAR', 'FOO'])
'BARBAZ'
"""
return _strips("l", text, remove)
def strips(text, remove):
"""
removes the string `remove` from the both sides of `text`
>>> strips("foobarfoo", "foo")
'bar'
"""
return rstrips(lstrips(text, remove), remove)
def safestr(obj, encoding="utf-8"):
r"""
Converts any given object to utf-8 encoded string.
>>> safestr('hello')
'hello'
>>> safestr(2)
'2'
"""
if obj and hasattr(obj, "__next__"):
return [safestr(i) for i in obj]
else:
return str(obj)
# Since Python3, utf-8 encoded strings and unicode strings are the same thing
safeunicode = safestr
def timelimit(timeout):
"""
A decorator to limit a function to `timeout` seconds, raising `TimeoutError`
if it takes longer.
>>> import time
>>> def meaningoflife():
... time.sleep(.2)
... return 42
>>>
>>> timelimit(.1)(meaningoflife)()
Traceback (most recent call last):
...
RuntimeError: took too long
>>> timelimit(1)(meaningoflife)()
42
_Caveat:_ The function isn't stopped after `timeout` seconds but continues
executing in a separate thread. (There seems to be no way to kill a thread.)
inspired by
"""
def _1(function):
def _2(*args, **kw):
class Dispatch(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.result = None
self.error = None
self.daemon = True
self.start()
def run(self):
try:
self.result = function(*args, **kw)
except:
self.error = sys.exc_info()
c = Dispatch()
c.join(timeout)
if c.is_alive():
raise RuntimeError("took too long")
if c.error:
raise c.error[1]
return c.result
return _2
return _1
class Memoize:
"""
'Memoizes' a function, caching its return values for each input.
If `expires` is specified, values are recalculated after `expires` seconds.
If `background` is specified, values are recalculated in a separate thread.
>>> calls = 0
>>> def howmanytimeshaveibeencalled():
... global calls
... calls += 1
... return calls
>>> fastcalls = memoize(howmanytimeshaveibeencalled)
>>> howmanytimeshaveibeencalled()
1
>>> howmanytimeshaveibeencalled()
2
>>> fastcalls()
3
>>> fastcalls()
3
>>> import time
>>> fastcalls = memoize(howmanytimeshaveibeencalled, .1, background=False)
>>> fastcalls()
4
>>> fastcalls()
4
>>> time.sleep(.2)
>>> fastcalls()
5
>>> def slowfunc():
... time.sleep(.1)
... return howmanytimeshaveibeencalled()
>>> fastcalls = memoize(slowfunc, .2, background=True)
>>> fastcalls()
6
>>> timelimit(.05)(fastcalls)()
6
>>> time.sleep(.2)
>>> timelimit(.05)(fastcalls)()
6
>>> timelimit(.05)(fastcalls)()
6
>>> time.sleep(.2)
>>> timelimit(.05)(fastcalls)()
7
>>> fastcalls = memoize(slowfunc, None, background=True)
>>> threading.Thread(target=fastcalls).start()
>>> time.sleep(.01)
>>> fastcalls()
9
"""
def __init__(self, func, expires=None, background=True):
self.func = func
self.cache = {}
self.expires = expires
self.background = background
self.running = {}
self.running_lock = threading.Lock()
def __call__(self, *args, **keywords):
key = (args, tuple(keywords.items()))
with self.running_lock:
if not self.running.get(key):
self.running[key] = threading.Lock()
def update(block=False):
if self.running[key].acquire(block):
try:
self.cache[key] = (self.func(*args, **keywords), time.time())
finally:
self.running[key].release()
if key not in self.cache:
update(block=True)
elif self.expires and (time.time() - self.cache[key][1]) > self.expires:
if self.background:
threading.Thread(target=update).start()
else:
update()
return self.cache[key][0]
memoize = Memoize
re_compile = memoize(re.compile)
re_compile.__doc__ = """
A memoized version of re.compile.
"""
class _re_subm_proxy:
def __init__(self):
self.match = None
def __call__(self, match):
self.match = match
return ""
def re_subm(pat, repl, string):
"""
Like re.sub, but returns the replacement _and_ the match object.
>>> t, m = re_subm('g(oo+)fball', r'f\\1lish', 'goooooofball')
>>> t
'foooooolish'
>>> m.groups()
('oooooo',)
"""
compiled_pat = re_compile(pat)
proxy = _re_subm_proxy()
compiled_pat.sub(proxy.__call__, string)
return compiled_pat.sub(repl, string), proxy.match
def group(seq, size):
"""
Returns an iterator over a series of lists of length size from iterable.
>>> list(group([1,2,3,4], 2))
[[1, 2], [3, 4]]
>>> list(group([1,2,3,4,5], 2))
[[1, 2], [3, 4], [5]]
"""
return (seq[i : i + size] for i in range(0, len(seq), size))
def uniq(seq, key=None):
"""
Removes duplicate elements from a list while preserving the order of the rest.
>>> uniq([9,0,2,1,0])
[9, 0, 2, 1]
The value of the optional `key` parameter should be a function that
takes a single argument and returns a key to test the uniqueness.
>>> uniq(["Foo", "foo", "bar"], key=lambda s: s.lower())
['Foo', 'bar']
"""
key = key or (lambda x: x)
seen = set()
result = []
for v in seq:
k = key(v)
if k in seen:
continue
seen.add(k)
result.append(v)
return result
def iterview(x):
"""
Takes an iterable `x` and returns an iterator over it
which prints its progress to stderr as it iterates through.
"""
WIDTH = 70
def plainformat(n, lenx):
return "%5.1f%% (%*d/%d)" % ((float(n) / lenx) * 100, len(str(lenx)), n, lenx)
def bars(size, n, lenx):
val = int((float(n) * size) / lenx + 0.5)
if size - val:
spacing = ">" + (" " * (size - val))[1:]
else:
spacing = ""
return "[{}{}]".format("=" * val, spacing)
def eta(elapsed, n, lenx):
if n == 0:
return "--:--:--"
if n == lenx:
secs = int(elapsed)
else:
secs = int((elapsed / n) * (lenx - n))
mins, secs = divmod(secs, 60)
hrs, mins = divmod(mins, 60)
return "%02d:%02d:%02d" % (hrs, mins, secs)
def format(starttime, n, lenx):
out = plainformat(n, lenx) + " "
if n == lenx:
end = " "
else:
end = " ETA "
end += eta(time.time() - starttime, n, lenx)
out += bars(WIDTH - len(out) - len(end), n, lenx)
out += end
return out
starttime = time.time()
lenx = len(x)
for n, y in enumerate(x):
sys.stderr.write("\r" + format(starttime, n, lenx))
yield y
sys.stderr.write("\r" + format(starttime, n + 1, lenx) + "\n")
class IterBetter:
"""
Returns an object that can be used as an iterator
but can also be used via __getitem__ (although it
cannot go backwards -- that is, you cannot request
`iterbetter[0]` after requesting `iterbetter[1]`).
>>> import itertools
>>> c = iterbetter(itertools.count())
>>> c[1]
1
>>> c[5]
5
>>> c[3]
Traceback (most recent call last):
...
IndexError: already passed 3
It is also possible to get the first value of the iterator or None.
>>> c = iterbetter(iter([3, 4, 5]))
>>> print(c.first())
3
>>> c = iterbetter(iter([]))
>>> print(c.first())
None
For boolean test, IterBetter peeps at first value in the itertor without effecting the iteration.
>>> c = iterbetter(iter(range(5)))
>>> bool(c)
True
>>> list(c)
[0, 1, 2, 3, 4]
>>> c = iterbetter(iter([]))
>>> bool(c)
False
>>> list(c)
[]
"""
def __init__(self, iterator):
self.i, self.c = iterator, 0
def first(self, default=None):
"""Returns the first element of the iterator or None when there are no
elements.
If the optional argument default is specified, that is returned instead
of None when there are no elements.
"""
try:
return next(iter(self))
except StopIteration:
return default
def __iter__(self):
if hasattr(self, "_head"):
yield self._head
while 1:
try:
yield next(self.i)
except StopIteration:
return
self.c += 1
def __getitem__(self, i):
# todo: slices
if i < self.c:
raise IndexError("already passed " + str(i))
try:
while i > self.c:
next(self.i)
self.c += 1
# now self.c == i
self.c += 1
return next(self.i)
except StopIteration:
raise IndexError(str(i))
def __nonzero__(self):
if hasattr(self, "__len__"):
return self.__len__() != 0
elif hasattr(self, "_head"):
return True
else:
try:
self._head = next(self.i)
except StopIteration:
return False
else:
return True
__bool__ = __nonzero__
iterbetter = IterBetter
def safeiter(it, cleanup=None, ignore_errors=True):
"""Makes an iterator safe by ignoring the exceptions occurred during the iteration."""
def next():
while True:
try:
return next(it)
except StopIteration:
raise
except:
traceback.print_exc()
it = iter(it)
while True:
yield next()
def safewrite(filename, content):
"""Writes the content to a temp file and then moves the temp file to
given filename to avoid overwriting the existing file in case of errors.
"""
with open(filename + ".tmp", "w") as f:
f.write(content)
shutil.move(f.name, filename)
def dictreverse(mapping):
"""
Returns a new dictionary with keys and values swapped.
>>> dictreverse({1: 2, 3: 4})
{2: 1, 4: 3}
"""
return {value: key for (key, value) in iteritems(mapping)}
def dictfind(dictionary, element):
"""
Returns a key whose value in `dictionary` is `element`
or, if none exists, None.
>>> d = {1:2, 3:4}
>>> dictfind(d, 4)
3
>>> dictfind(d, 5)
"""
for key, value in iteritems(dictionary):
if element is value:
return key
def dictfindall(dictionary, element):
"""
Returns the keys whose values in `dictionary` are `element`
or, if none exists, [].
>>> d = {1:4, 3:4}
>>> dictfindall(d, 4)
[1, 3]
>>> dictfindall(d, 5)
[]
"""
res = []
for key, value in iteritems(dictionary):
if element is value:
res.append(key)
return res
def dictincr(dictionary, element):
"""
Increments `element` in `dictionary`,
setting it to one if it doesn't exist.
>>> d = {1:2, 3:4}
>>> dictincr(d, 1)
3
>>> d[1]
3
>>> dictincr(d, 5)
1
>>> d[5]
1
"""
dictionary.setdefault(element, 0)
dictionary[element] += 1
return dictionary[element]
def dictadd(*dicts):
"""
Returns a dictionary consisting of the keys in the argument dictionaries.
If they share a key, the value from the last argument is used.
>>> dictadd({1: 0, 2: 0}, {2: 1, 3: 1})
{1: 0, 2: 1, 3: 1}
"""
result = {}
for dct in dicts:
result.update(dct)
return result
def requeue(queue, index=-1):
"""Returns the element at index after moving it to the beginning of the queue.
>>> x = [1, 2, 3, 4]
>>> requeue(x)
4
>>> x
[4, 1, 2, 3]
"""
x = queue.pop(index)
queue.insert(0, x)
return x
def restack(stack, index=0):
"""Returns the element at index after moving it to the top of stack.
>>> x = [1, 2, 3, 4]
>>> restack(x)
1
>>> x
[2, 3, 4, 1]
"""
x = stack.pop(index)
stack.append(x)
return x
def listget(lst, ind, default=None):
"""
Returns `lst[ind]` if it exists, `default` otherwise.
>>> listget(['a'], 0)
'a'
>>> listget(['a'], 1)
>>> listget(['a'], 1, 'b')
'b'
"""
if len(lst) - 1 < ind:
return default
return lst[ind]
def intget(integer, default=None):
"""
Returns `integer` as an int or `default` if it can't.
>>> intget('3')
3
>>> intget('3a')
>>> intget('3a', 0)
0
"""
try:
return int(integer)
except (TypeError, ValueError):
return default
def datestr(then, now=None):
"""
Converts a (UTC) datetime object to a nice string representation.
>>> from datetime import datetime, timedelta
>>> d = datetime(1970, 5, 1)
>>> datestr(d, now=d)
'0 microseconds ago'
>>> for t, v in iteritems({
... timedelta(microseconds=1): '1 microsecond ago',
... timedelta(microseconds=2): '2 microseconds ago',
... -timedelta(microseconds=1): '1 microsecond from now',
... -timedelta(microseconds=2): '2 microseconds from now',
... timedelta(microseconds=2000): '2 milliseconds ago',
... timedelta(seconds=2): '2 seconds ago',
... timedelta(seconds=2*60): '2 minutes ago',
... timedelta(seconds=2*60*60): '2 hours ago',
... timedelta(days=2): '2 days ago',
... }):
... assert datestr(d, now=d+t) == v
>>> datestr(datetime(1970, 1, 1), now=d)
'January 1'
>>> datestr(datetime(1969, 1, 1), now=d)
'January 1, 1969'
>>> datestr(datetime(1970, 6, 1), now=d)
'June 1, 1970'
>>> datestr(None)
''
"""
def agohence(n, what, divisor=None):
if divisor:
n = n // divisor
out = str(abs(n)) + " " + what # '2 days'
if abs(n) != 1:
out += "s" # '2 days'
out += " " # '2 days '
if n < 0:
out += "from now"
else:
out += "ago"
return out # '2 days ago'
oneday = 24 * 60 * 60
if not then:
return ""
if not now:
now = datetime.datetime.utcnow()
if type(now).__name__ == "DateTime":
now = datetime.datetime.fromtimestamp(now)
if type(then).__name__ == "DateTime":
then = datetime.datetime.fromtimestamp(then)
elif type(then).__name__ == "date":
then = datetime.datetime(then.year, then.month, then.day)
delta = now - then
deltaseconds = int(delta.days * oneday + delta.seconds + delta.microseconds * 1e-06)
deltadays = abs(deltaseconds) // oneday
if deltaseconds < 0:
deltadays *= -1 # fix for oddity of floor
if deltadays:
if abs(deltadays) < 4:
return agohence(deltadays, "day")
# Trick to display 'June 3' instead of 'June 03'
# Even though the %e format in strftime does that, it doesn't work on Windows.
out = then.strftime("%B %d").replace(" 0", " ")
if then.year != now.year or deltadays < 0:
out += ", %s" % then.year
return out
if int(deltaseconds):
if abs(deltaseconds) > (60 * 60):
return agohence(deltaseconds, "hour", 60 * 60)
elif abs(deltaseconds) > 60:
return agohence(deltaseconds, "minute", 60)
else:
return agohence(deltaseconds, "second")
deltamicroseconds = delta.microseconds
if delta.days:
deltamicroseconds = int(delta.microseconds - 1e6) # datetime oddity
if abs(deltamicroseconds) > 1000:
return agohence(deltamicroseconds, "millisecond", 1000)
return agohence(deltamicroseconds, "microsecond")
def numify(string):
"""
Removes all non-digit characters from `string`.
>>> numify('800-555-1212')
'8005551212'
>>> numify('800.555.1212')
'8005551212'
"""
return "".join([c for c in str(string) if c.isdigit()])
def denumify(string, pattern):
"""
Formats `string` according to `pattern`, where the letter X gets replaced
by characters from `string`.
>>> denumify("8005551212", "(XXX) XXX-XXXX")
'(800) 555-1212'
"""
out = []
for c in pattern:
if c == "X":
out.append(string[0])
string = string[1:]
else:
out.append(c)
return "".join(out)
def commify(n):
"""
Add commas to an integer `n`.
>>> commify(1)
'1'
>>> commify(123)
'123'
>>> commify(-123)
'-123'
>>> commify(1234)
'1,234'
>>> commify(1234567890)
'1,234,567,890'
>>> commify(123.0)
'123.0'
>>> commify(1234.5)
'1,234.5'
>>> commify(1234.56789)
'1,234.56789'
>>> commify(' %.2f ' % -1234.5)
'-1,234.50'
>>> commify(None)
>>>
"""
if n is None:
return None
n = str(n).strip()
if n.startswith("-"):
prefix = "-"
n = n[1:].strip()
else:
prefix = ""
if "." in n:
dollars, cents = n.split(".")
else:
dollars, cents = n, None
r = []
for i, c in enumerate(str(dollars)[::-1]):
if i and (not (i % 3)):
r.insert(0, ",")
r.insert(0, c)
out = "".join(r)
if cents:
out += "." + cents
return prefix + out
def dateify(datestring):
"""
Formats a numified `datestring` properly.
"""
return denumify(datestring, "XXXX-XX-XX XX:XX:XX")
def nthstr(n):
"""
Formats an ordinal.
Doesn't handle negative numbers.
>>> nthstr(1)
'1st'
>>> nthstr(0)
'0th'
>>> [nthstr(x) for x in [2, 3, 4, 5, 10, 11, 12, 13, 14, 15]]
['2nd', '3rd', '4th', '5th', '10th', '11th', '12th', '13th', '14th', '15th']
>>> [nthstr(x) for x in [91, 92, 93, 94, 99, 100, 101, 102]]
['91st', '92nd', '93rd', '94th', '99th', '100th', '101st', '102nd']
>>> [nthstr(x) for x in [111, 112, 113, 114, 115]]
['111th', '112th', '113th', '114th', '115th']
"""
assert n >= 0
if n % 100 in [11, 12, 13]:
return "%sth" % n
return {1: "%sst", 2: "%snd", 3: "%srd"}.get(n % 10, "%sth") % n
def cond(predicate, consequence, alternative=None):
"""
Function replacement for if-else to use in expressions.
>>> x = 2
>>> cond(x % 2 == 0, "even", "odd")
'even'
>>> cond(x % 2 == 0, "even", "odd") + '_row'
'even_row'
"""
if predicate:
return consequence
else:
return alternative
class CaptureStdout:
"""
Captures everything `func` prints to stdout and returns it instead.
>>> def idiot():
... print("foo")
>>> capturestdout(idiot)()
'foo\\n'
**WARNING:** Not threadsafe!
"""
def __init__(self, func):
self.func = func
def __call__(self, *args, **keywords):
out = StringIO()
oldstdout = sys.stdout
sys.stdout = out
try:
self.func(*args, **keywords)
finally:
sys.stdout = oldstdout
return out.getvalue()
capturestdout = CaptureStdout
class Profile:
"""
Profiles `func` and returns a tuple containing its output
and a string with human-readable profiling information.
>>> import time
>>> out, inf = profile(time.sleep)(.001)
>>> out
>>> inf[:10].strip()
'took 0.0'
"""
def __init__(self, func):
self.func = func
def __call__(self, *args): # , **kw): kw unused
import cProfile
import os
import pstats
import tempfile
f, filename = tempfile.mkstemp()
os.close(f)
prof = cProfile.Profile()
stime = time.time()
result = prof.runcall(self.func, *args)
stime = time.time() - stime
out = StringIO()
stats = pstats.Stats(prof, stream=out)
stats.strip_dirs()
stats.sort_stats("time", "calls")
stats.print_stats(40)
stats.print_callers()
x = "\n\ntook " + str(stime) + " seconds\n"
x += out.getvalue()
# remove the tempfile
try:
os.remove(filename)
except OSError:
pass
return result, x
profile = Profile
def tryall(context, prefix=None):
"""
Tries a series of functions and prints their results.
`context` is a dictionary mapping names to values;
the value will only be tried if it's callable.
>>> tryall(dict(j=lambda: True))
j: True
----------------------------------------
results:
True: 1
For example, you might have a file `test/stuff.py`
with a series of functions testing various things in it.
At the bottom, have a line:
if __name__ == "__main__": tryall(globals())
Then you can run `python test/stuff.py` and get the results of
all the tests.
"""
context = context.copy() # vars() would update
results = {}
for key, value in iteritems(context):
if not hasattr(value, "__call__"):
continue
if prefix and not key.startswith(prefix):
continue
print(key + ":", end=" ")
try:
r = value()
dictincr(results, r)
print(r)
except:
print("ERROR")
dictincr(results, "ERROR")
print(" " + "\n ".join(traceback.format_exc().split("\n")))
print("-" * 40)
print("results:")
for key, value in iteritems(results):
print(" " * 2, str(key) + ":", value)
class ThreadedDict(threadlocal):
"""
Thread local storage.
>>> d = ThreadedDict()
>>> d.x = 1
>>> d.x
1
>>> import threading
>>> def f(): d.x = 2
...
>>> t = threading.Thread(target=f)
>>> t.start()
>>> t.join()
>>> d.x
1
"""
_instances = set()
def __init__(self):
ThreadedDict._instances.add(self)
def __del__(self):
ThreadedDict._instances.remove(self)
def __hash__(self):
return id(self)
def clear_all():
"""Clears all ThreadedDict instances."""
for t in list(ThreadedDict._instances):
t.clear()
clear_all = staticmethod(clear_all)
# Define all these methods to more or less fully emulate dict -- attribute access
# is built into threading.local.
def __getitem__(self, key):
return self.__dict__[key]
def __setitem__(self, key, value):
self.__dict__[key] = value
def __delitem__(self, key):
del self.__dict__[key]
def __contains__(self, key):
return key in self.__dict__
has_key = __contains__
def clear(self):
self.__dict__.clear()
def copy(self):
return self.__dict__.copy()
def get(self, key, default=None):
return self.__dict__.get(key, default)
def items(self):
return self.__dict__.items()
def iteritems(self):
return iteritems(self.__dict__)
def keys(self):
return self.__dict__.keys()
def iterkeys(self):
try:
return iterkeys(self.__dict__)
except NameError:
return self.__dict__.keys()
iter = iterkeys
def values(self):
return self.__dict__.values()
def itervalues(self):
return itervalues(self.__dict__)
def pop(self, key, *args):
return self.__dict__.pop(key, *args)
def popitem(self):
return self.__dict__.popitem()
def setdefault(self, key, default=None):
return self.__dict__.setdefault(key, default)
def update(self, *args, **kwargs):
self.__dict__.update(*args, **kwargs)
def __repr__(self):
return "" % self.__dict__
__str__ = __repr__
threadeddict = ThreadedDict
def autoassign(self, locals):
"""
Automatically assigns local variables to `self`.
>>> self = storage()
>>> autoassign(self, dict(a=1, b=2))
>>> self.a
1
>>> self.b
2
Generally used in `__init__` methods, as in:
def __init__(self, foo, bar, baz=1): autoassign(self, locals())
"""
for key, value in iteritems(locals):
if key == "self":
continue
setattr(self, key, value)
def to36(q):
"""
Converts an integer to base 36 (a useful scheme for human-sayable IDs).
>>> to36(35)
'z'
>>> to36(119292)
'2k1o'
>>> int(to36(939387374), 36)
939387374
>>> to36(0)
'0'
>>> to36(-393)
Traceback (most recent call last):
...
ValueError: must supply a positive integer
"""
if q < 0:
raise ValueError("must supply a positive integer")
letters = "0123456789abcdefghijklmnopqrstuvwxyz"
converted = []
while q != 0:
q, r = divmod(q, 36)
converted.insert(0, letters[r])
return "".join(converted) or "0"
r_url = re_compile(r"(?"
def __str__(self):
return self.message.as_string()
if __name__ == "__main__":
import doctest
doctest.testmod()
webpy-webpy-99e5eed/web/webapi.py 0000664 0000000 0000000 00000042017 15206666350 0017112 0 ustar 00root root 0000000 0000000 """
Web API (wrapper around WSGI)
(from web.py)
"""
import pprint
import sys
import urllib
from http.cookies import CookieError, Morsel, SimpleCookie
from urllib.parse import parse_qs, quote, unquote, urljoin
import multipart
from .utils import dictadd, intget, safestr, storage, storify, threadeddict
__all__ = [
"config",
"header",
"debug",
"input",
"data",
"setcookie",
"cookies",
"ctx",
"HTTPError",
# 200, 201, 202, 204
"OK",
"Created",
"Accepted",
"NoContent",
"ok",
"created",
"accepted",
"nocontent",
# 301, 302, 303, 304, 307
"Redirect",
"Found",
"SeeOther",
"NotModified",
"TempRedirect",
"redirect",
"found",
"seeother",
"notmodified",
"tempredirect",
# 400, 401, 403, 404, 405, 406, 409, 410, 412, 415, 451
"BadRequest",
"Unauthorized",
"Forbidden",
"NotFound",
"NoMethod",
"NotAcceptable",
"Conflict",
"Gone",
"PreconditionFailed",
"UnsupportedMediaType",
"UnavailableForLegalReasons",
"badrequest",
"unauthorized",
"forbidden",
"notfound",
"nomethod",
"notacceptable",
"conflict",
"gone",
"preconditionfailed",
"unsupportedmediatype",
"unavailableforlegalreasons",
# 500
"InternalError",
"internalerror",
]
config = storage()
config.__doc__ = """
A configuration object for various aspects of web.py.
`debug`
: when True, enables reloading, disabled template caching and sets internalerror to debugerror.
"""
class HTTPError(Exception):
def __init__(self, status, headers={}, data=""):
ctx.status = status
for k, v in headers.items():
header(k, v)
self.data = data
Exception.__init__(self, status)
def _status_code(status, data=None, classname=None, docstring=None):
if data is None:
data = status.split(" ", 1)[1]
classname = status.split(" ", 1)[1].replace(
" ", ""
) # 304 Not Modified -> NotModified
docstring = docstring or "`%s` status" % status
def __init__(self, data=data, headers={}):
HTTPError.__init__(self, status, headers, data)
# trick to create class dynamically with dynamic docstring.
return type(
classname, (HTTPError, object), {"__doc__": docstring, "__init__": __init__}
)
ok = OK = _status_code("200 OK", data="")
created = Created = _status_code("201 Created")
accepted = Accepted = _status_code("202 Accepted")
nocontent = NoContent = _status_code("204 No Content")
class Redirect(HTTPError):
"""A `301 Moved Permanently` redirect."""
def __init__(self, url, status="301 Moved Permanently", absolute=False):
"""
Returns a `status` redirect to the new URL.
`url` is joined with the base URL so that things like
`redirect("about") will work properly.
"""
newloc = urljoin(ctx.path, url)
if newloc.startswith("/"):
if absolute:
home = ctx.realhome
else:
home = ctx.home
newloc = home + newloc
headers = {"Content-Type": "text/html", "Location": newloc}
HTTPError.__init__(self, status, headers, "")
redirect = Redirect
class Found(Redirect):
"""A `302 Found` redirect."""
def __init__(self, url, absolute=False):
Redirect.__init__(self, url, "302 Found", absolute=absolute)
found = Found
class SeeOther(Redirect):
"""A `303 See Other` redirect."""
def __init__(self, url, absolute=False):
Redirect.__init__(self, url, "303 See Other", absolute=absolute)
seeother = SeeOther
class NotModified(HTTPError):
"""A `304 Not Modified` status."""
def __init__(self):
HTTPError.__init__(self, "304 Not Modified")
notmodified = NotModified
class TempRedirect(Redirect):
"""A `307 Temporary Redirect` redirect."""
def __init__(self, url, absolute=False):
Redirect.__init__(self, url, "307 Temporary Redirect", absolute=absolute)
tempredirect = TempRedirect
class BadRequest(HTTPError):
"""`400 Bad Request` error."""
message = "bad request"
def __init__(self, message=None):
status = "400 Bad Request"
headers = {"Content-Type": "text/html"}
HTTPError.__init__(self, status, headers, message or self.message)
badrequest = BadRequest
class Unauthorized(HTTPError):
"""`401 Unauthorized` error."""
message = "unauthorized"
def __init__(self, message=None):
status = "401 Unauthorized"
headers = {"Content-Type": "text/html"}
HTTPError.__init__(self, status, headers, message or self.message)
unauthorized = Unauthorized
class Forbidden(HTTPError):
"""`403 Forbidden` error."""
message = "forbidden"
def __init__(self, message=None):
status = "403 Forbidden"
headers = {"Content-Type": "text/html"}
HTTPError.__init__(self, status, headers, message or self.message)
forbidden = Forbidden
class _NotFound(HTTPError):
"""`404 Not Found` error."""
message = "not found"
def __init__(self, message=None):
status = "404 Not Found"
headers = {"Content-Type": "text/html; charset=utf-8"}
HTTPError.__init__(self, status, headers, message or self.message)
def NotFound(message=None):
"""Returns HTTPError with '404 Not Found' error from the active application."""
if message:
return _NotFound(message)
elif ctx.get("app_stack"):
return ctx.app_stack[-1].notfound()
else:
return _NotFound()
notfound = NotFound
class NoMethod(HTTPError):
"""A `405 Method Not Allowed` error."""
message = "method not allowed"
def __init__(self, cls=None):
status = "405 Method Not Allowed"
headers = {}
headers["Content-Type"] = "text/html"
methods = ["GET", "HEAD", "POST", "PUT", "DELETE"]
if cls:
methods = [method for method in methods if hasattr(cls, method)]
headers["Allow"] = ", ".join(methods)
HTTPError.__init__(self, status, headers, self.message)
nomethod = NoMethod
class NotAcceptable(HTTPError):
"""`406 Not Acceptable` error."""
message = "not acceptable"
def __init__(self, message=None):
status = "406 Not Acceptable"
headers = {"Content-Type": "text/html"}
HTTPError.__init__(self, status, headers, message or self.message)
notacceptable = NotAcceptable
class Conflict(HTTPError):
"""`409 Conflict` error."""
message = "conflict"
def __init__(self, message=None):
status = "409 Conflict"
headers = {"Content-Type": "text/html"}
HTTPError.__init__(self, status, headers, message or self.message)
conflict = Conflict
class Gone(HTTPError):
"""`410 Gone` error."""
message = "gone"
def __init__(self, message=None):
status = "410 Gone"
headers = {"Content-Type": "text/html"}
HTTPError.__init__(self, status, headers, message or self.message)
gone = Gone
class PreconditionFailed(HTTPError):
"""`412 Precondition Failed` error."""
message = "precondition failed"
def __init__(self, message=None):
status = "412 Precondition Failed"
headers = {"Content-Type": "text/html"}
HTTPError.__init__(self, status, headers, message or self.message)
preconditionfailed = PreconditionFailed
class UnsupportedMediaType(HTTPError):
"""`415 Unsupported Media Type` error."""
message = "unsupported media type"
def __init__(self, message=None):
status = "415 Unsupported Media Type"
headers = {"Content-Type": "text/html"}
HTTPError.__init__(self, status, headers, message or self.message)
unsupportedmediatype = UnsupportedMediaType
class _UnavailableForLegalReasons(HTTPError):
"""`451 Unavailable For Legal Reasons` error."""
message = "unavailable for legal reasons"
def __init__(self, message=None):
status = "451 Unavailable For Legal Reasons"
headers = {"Content-Type": "text/html"}
HTTPError.__init__(self, status, headers, message or self.message)
def UnavailableForLegalReasons(message=None):
"""Returns HTTPError with '415 Unavailable For Legal Reasons' error from the active application."""
if message:
return _UnavailableForLegalReasons(message)
elif ctx.get("app_stack"):
return ctx.app_stack[-1].unavailableforlegalreasons()
else:
return _UnavailableForLegalReasons()
unavailableforlegalreasons = UnavailableForLegalReasons
class _InternalError(HTTPError):
"""500 Internal Server Error`."""
message = "internal server error"
def __init__(self, message=None):
status = "500 Internal Server Error"
headers = {"Content-Type": "text/html"}
HTTPError.__init__(self, status, headers, message or self.message)
def InternalError(message=None):
"""Returns HTTPError with '500 internal error' error from the active application."""
if message:
return _InternalError(message)
elif ctx.get("app_stack"):
return ctx.app_stack[-1].internalerror()
else:
return _InternalError()
internalerror = InternalError
def header(hdr, value, unique=False):
"""
Adds the header `hdr: value` with the response.
If `unique` is True and a header with that name already exists,
it doesn't add a new one.
"""
hdr, value = safestr(hdr), safestr(value)
# protection against HTTP response splitting attack
if "\n" in hdr or "\r" in hdr or "\n" in value or "\r" in value:
raise ValueError("invalid characters in header")
if unique is True:
for h, v in ctx.headers:
if h.lower() == hdr.lower():
return
ctx.headers.append((hdr, value))
def rawinput(method=None):
"""Returns storage object with GET or POST arguments."""
method = method or "both"
def dictify(fs):
return {k: fs[k] for k in fs}
env = ctx.env.copy()
post_req = get_req = {}
if method.lower() in ["both", "post", "put", "patch"]:
if env["REQUEST_METHOD"] in ["POST", "PUT", "PATCH"]:
if env.get("CONTENT_TYPE", "").lower().startswith("multipart/"):
# since wsgi.input is directly passed to multipart,
# it can not be called multiple times. Saving the result
# object in ctx to allow calling web.input multiple times.
post_req = ctx.get(
"_fieldstorage"
) # TODO: Rename? is this visible anywhere else?
if not post_req:
try:
# This returns two dicts, forms & files.
forms, files = multipart.parse_form_data(environ=env)
post_req = dictadd(forms, files)
ctx._fieldstorage = post_req
except IndexError:
post_req = {}
else:
post_data = data().decode("utf-8")
post_req = parse_qs(post_data, keep_blank_values=True)
post_req = dictify(post_req)
if method.lower() in ["both", "get"]:
env["REQUEST_METHOD"] = "GET"
get_req = dict(
urllib.parse.parse_qs(env.get("QUERY_STRING", ""), keep_blank_values=True)
)
def process_values(values):
if isinstance(values, list):
return [process_values(x) for x in values]
elif hasattr(values, "filename") and values.filename is None:
return values.value
else:
return values
return storage(
[(k, process_values(v)) for k, v in dictadd(get_req, post_req).items()]
)
def input(*requireds, **defaults):
"""
Returns a `storage` object with the GET and POST arguments.
See `storify` for how `requireds` and `defaults` work.
"""
_method = defaults.pop("_method", "both")
out = rawinput(_method)
try:
defaults.setdefault("_unicode", True) # force unicode conversion by default.
return storify(out, *requireds, **defaults)
except KeyError:
raise badrequest()
def data():
"""Returns the data sent with the request."""
if "data" not in ctx:
if ctx.env.get("HTTP_TRANSFER_ENCODING") == "chunked":
ctx.data = ctx.env["wsgi.input"].read()
else:
cl = intget(ctx.env.get("CONTENT_LENGTH"), 0)
ctx.data = ctx.env["wsgi.input"].read(cl)
return ctx.data
def setcookie(
name,
value,
expires="",
domain=None,
secure=False,
httponly=False,
path=None,
samesite=None,
):
"""Sets a cookie."""
morsel = Morsel()
name, value = safestr(name), safestr(value)
morsel.set(name, value, quote(value))
if isinstance(expires, int) and expires < 0:
expires = -1000000000
morsel["expires"] = expires
morsel["path"] = path or ctx.homepath + "/"
if domain:
morsel["domain"] = domain
if secure:
morsel["secure"] = secure
if httponly:
morsel["httponly"] = True
value = morsel.OutputString()
if samesite and samesite.lower() in ("strict", "lax", "none"):
value += "; SameSite=%s" % samesite
header("Set-Cookie", value)
def parse_cookies(http_cookie):
r"""Parse a HTTP_COOKIE header and return dict of cookie names and decoded values.
>>> sorted(parse_cookies('').items())
[]
>>> sorted(parse_cookies('a=1').items())
[('a', '1')]
>>> sorted(parse_cookies('a=1%202').items())
[('a', '1 2')]
>>> sorted(parse_cookies('a=Z%C3%A9Z').items())
[('a', 'Z\xc3\xa9Z')]
>>> sorted(parse_cookies('a=1; b=2; c=3').items())
[('a', '1'), ('b', '2'), ('c', '3')]
# TODO: cclauss re-enable this test
# >>> sorted(parse_cookies('a=1; b=w("x")|y=z; c=3').items())
# [('a', '1'), ('b', 'w('), ('c', '3')]
>>> sorted(parse_cookies('a=1; b=w(%22x%22)|y=z; c=3').items())
[('a', '1'), ('b', 'w("x")|y=z'), ('c', '3')]
>>> sorted(parse_cookies('keebler=E=mc2').items())
[('keebler', 'E=mc2')]
>>> sorted(parse_cookies(r'keebler="E=mc2; L=\"Loves\"; fudge=\012;"').items())
[('keebler', 'E=mc2; L="Loves"; fudge=\n;')]
"""
# print "parse_cookies"
if '"' in http_cookie:
# HTTP_COOKIE has quotes in it, use slow but correct cookie parsing
cookie = SimpleCookie()
try:
cookie.load(http_cookie)
except CookieError:
# If HTTP_COOKIE header is malformed, try at least to load the cookies we can by
# first splitting on ';' and loading each attr=value pair separately
cookie = SimpleCookie()
for attr_value in http_cookie.split(";"):
try:
cookie.load(attr_value)
except CookieError:
pass
cookies = {k: unquote(v.value) for k, v in cookie.items()}
else:
# HTTP_COOKIE doesn't have quotes, use fast cookie parsing
cookies = {}
for key_value in http_cookie.split(";"):
key_value = key_value.split("=", 1)
if len(key_value) == 2:
key, value = key_value
cookies[key.strip()] = unquote(value.strip())
return cookies
def cookies(*requireds, **defaults):
"""Returns a `storage` object with all the request cookies in it.
See `storify` for how `requireds` and `defaults` work.
This is forgiving on bad HTTP_COOKIE input, it tries to parse at least
the cookies it can.
The values are converted to unicode if _unicode=True is passed.
"""
# parse cookie string and cache the result for next time.
if "_parsed_cookies" not in ctx:
http_cookie = ctx.env.get("HTTP_COOKIE", "")
ctx._parsed_cookies = parse_cookies(http_cookie)
try:
return storify(ctx._parsed_cookies, *requireds, **defaults)
except KeyError:
badrequest()
raise StopIteration()
def debug(*args):
"""
Prints a prettyprinted version of `args` to stderr.
"""
try:
out = ctx.environ["wsgi.errors"]
except:
out = sys.stderr
for arg in args:
print(pprint.pformat(arg), file=out)
return ""
def _debugwrite(x):
try:
out = ctx.environ["wsgi.errors"]
except:
out = sys.stderr
out.write(x)
debug.write = _debugwrite
ctx = context = threadeddict()
ctx.__doc__ = """
A `storage` object containing various information about the request:
`environ` (aka `env`)
: A dictionary containing the standard WSGI environment variables.
`host`
: The domain (`Host` header) requested by the user.
`home`
: The base path for the application.
`ip`
: The IP address of the requester.
`method`
: The HTTP method used.
`path`
: The path request.
`query`
: If there are no query arguments, the empty string. Otherwise, a `?` followed
by the query string.
`fullpath`
: The full path requested, including query arguments (`== path + query`).
### Response Data
`status` (default: "200 OK")
: The status code to be used in the response.
`headers`
: A list of 2-tuples to be used in the response.
`output`
: A string to be used as the response.
"""
if __name__ == "__main__":
import doctest
doctest.testmod()
webpy-webpy-99e5eed/web/wsgi.py 0000664 0000000 0000000 00000004475 15206666350 0016622 0 ustar 00root root 0000000 0000000 """
WSGI Utilities
(from web.py)
"""
import os
import sys
from . import httpserver
from . import webapi as web
from .net import validaddr
from .utils import intget, listget
def runfcgi(func, addr=("localhost", 8000)):
"""Runs a WSGI function as a FastCGI server."""
import flup.server.fcgi as flups
return flups.WSGIServer(func, multiplexed=True, bindAddress=addr, debug=False).run()
def runscgi(func, addr=("localhost", 4000)):
"""Runs a WSGI function as an SCGI server."""
import flup.server.scgi as flups
return flups.WSGIServer(func, bindAddress=addr, debug=False).run()
def runwsgi(func):
"""
Runs a WSGI-compatible `func` using FCGI, SCGI, or a simple web server,
as appropriate based on context and `sys.argv`.
"""
if "SERVER_SOFTWARE" in os.environ: # cgi
os.environ["FCGI_FORCE_CGI"] = "Y"
# PHP_FCGI_CHILDREN is used by lighttpd fastcgi
if "PHP_FCGI_CHILDREN" in os.environ or "SERVER_SOFTWARE" in os.environ:
return runfcgi(func, None)
if "fcgi" in sys.argv or "fastcgi" in sys.argv:
args = sys.argv[1:]
if "fastcgi" in args:
args.remove("fastcgi")
elif "fcgi" in args:
args.remove("fcgi")
if args:
return runfcgi(func, validaddr(args[0]))
else:
return runfcgi(func, None)
if "scgi" in sys.argv:
args = sys.argv[1:]
args.remove("scgi")
if args:
return runscgi(func, validaddr(args[0]))
else:
return runscgi(func)
server_addr = validaddr(listget(sys.argv, 1, ""))
if "PORT" in os.environ: # e.g. Heroku
server_addr = ("0.0.0.0", intget(os.environ["PORT"]))
return httpserver.runsimple(func, server_addr)
def _is_dev_mode():
# Some embedded python interpreters won't have sys.arv
# For details, see https://github.com/webpy/webpy/issues/87
argv = getattr(sys, "argv", [])
# quick hack to check if the program is running in dev mode.
if (
"SERVER_SOFTWARE" in os.environ
or "PHP_FCGI_CHILDREN" in os.environ
or "fcgi" in argv
or "fastcgi" in argv
or "mod_wsgi" in argv
):
return False
return True
# When running the builtin-server, enable debug mode if not already set.
web.config.setdefault("debug", _is_dev_mode())