Skip to content

Commit

Permalink
Add option --server to brython CPython package
Browse files Browse the repository at this point in the history
  • Loading branch information
Pierre Quentel committed Sep 18, 2017
1 parent 668e1d0 commit 66ef8c4
Show file tree
Hide file tree
Showing 3 changed files with 290 additions and 12 deletions.
27 changes: 21 additions & 6 deletions setup/brython.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,43 @@
import list_modules

parser = argparse.ArgumentParser()

parser.add_argument('--install', help='Install Brython in an empty directory',
action="store_true")
parser.add_argument('--make_dist',
help='Make a Python distribution',

parser.add_argument('--make_dist', help='Make a Python distribution',
action="store_true")
parser.add_argument('--modules',

parser.add_argument('--modules',
help='Create brython_modules.js with all the modules used by the application',
action="store_true")

parser.add_argument('--port', help='Port for the built-in server',
default=8080)

parser.add_argument('--reset', help='Reset brython_modules.js to stdlib',
action="store_true")

parser.add_argument('--server', help='Start the built-in server',
action="store_true")

parser.add_argument('--update', help='Update Brython scripts',
action="store_true")

args = parser.parse_args()

files = 'README.txt', 'demo.html', 'brython.js', 'brython_stdlib.js'

if args.install:
print('Installing Brython in an empty directory')

src_path = os.path.join(os.path.dirname(__file__), 'data')

if os.listdir(os.getcwd()):
print('Brython can only be installed in an empty folder')
import sys
sys.exit()

for path in files:
shutil.copyfile(os.path.join(src_path, path), path)

Expand All @@ -46,12 +57,16 @@

for path in files:
shutil.copyfile(os.path.join(src_path, path), path)

if args.reset:
print('Reset brython_modules.js to standard distribution')
shutil.copyfile(os.path.join(os.getcwd(), 'brython_stdlib.js'),
os.path.join(os.getcwd(), 'brython_modules.js'))

if args.server:
import server
server.run(int(args.port))

if args.modules:
print('Create brython_modules.js with all the modules used by the application')
finder = list_modules.ModulesFinder()
Expand Down
257 changes: 257 additions & 0 deletions setup/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
#

"""Simple HTTP Server.
Supports browser cache and HTTP compression."""

import os
import sys
import datetime
import io

import email
from http import HTTPStatus
import http.cookiejar
import http.server as server
from http.server import SimpleHTTPRequestHandler

# Python might be built without gzip / zlib
try:
import gzip
import zlib
except ImportError:
gzip = None

# List of commonly compressed content types, copied from
# https://github.com/h5bp/server-configs-apache.
# compressed_types is set to this list when the server is started with
# command line option --gzip.
commonly_compressed_types = [ "application/atom+xml",
"application/javascript",
"application/json",
"application/ld+json",
"application/manifest+json",
"application/rdf+xml",
"application/rss+xml",
"application/schema+json",
"application/vnd.geo+json",
"application/vnd.ms-fontobject",
"application/x-font-ttf",
"application/x-javascript",
"application/x-web-app-manifest+json",
"application/xhtml+xml",
"application/xml",
"font/eot",
"font/opentype",
"image/bmp",
"image/svg+xml",
"image/vnd.microsoft.icon",
"image/x-icon",
"text/cache-manifest",
"text/css",
"text/html",
"text/javascript",
"text/plain",
"text/vcard",
"text/vnd.rim.location.xloc",
"text/vtt",
"text/x-component",
"text/x-cross-domain-policy",
"text/xml"
]

# Generators for HTTP compression

def _zlib_producer(fileobj, wbits):
"""Generator that yields pieces of compressed data read from the file
object fileobj, using the zlib library.
It yields non-empty bytes objects and ends by yielding b'', for compliance
with the Chunked Transfer Encoding protocol.
wbits is the same argument as for zlib.compressobj.
"""
bufsize = 2 << 17
producer = zlib.compressobj(wbits=wbits)
with fileobj:
while True:
buf = fileobj.read(bufsize)
if not buf: # end of file
data = producer.flush()
if data:
yield data
yield b''
return
data = producer.compress(buf)
if data:
yield data

def _gzip_producer(fileobj):
"""Generator for gzip compression."""
return _zlib_producer(fileobj, 25)

def _deflate_producer(fileobj):
"""Generator for deflage compression."""
return _zlib_producer(fileobj, 15)

class RequestHandler(SimpleHTTPRequestHandler):

# List of Content Types that are returned with HTTP compression (gzip).
# Set to the empty list by default (no compression).
compressed_types = commonly_compressed_types

# Dictionary mapping an encoding (in an Accept-Encoding header) to a
# generator of compressed data. By default, the only supported encoding is
# gzip. Override if a subclass wants to use another compression algorithm.
compressions = {
'deflate': _deflate_producer,
'gzip': _gzip_producer,
'x-gzip': _gzip_producer
}

def _make_chunk(self, data):
return f"{len(data):X}".encode("ascii") + b"\r\n" + data + b"\r\n"

def send_head(self):
"""Common code for GET and HEAD commands.
This sends the response code and MIME headers.
Return value is either a file object (which has to be copied
to the outputfile by the caller unless the command was HEAD,
and must be closed by the caller under all circumstances), or
None, in which case the caller has nothing further to do.
"""
path = self.translate_path(self.path)
f = None
if os.path.isdir(path):
parts = urllib.parse.urlsplit(self.path)
if not parts.path.endswith('/'):
# redirect browser - doing basically what apache does
self.send_response(HTTPStatus.MOVED_PERMANENTLY)
new_parts = (parts[0], parts[1], parts[2] + '/',
parts[3], parts[4])
new_url = urllib.parse.urlunsplit(new_parts)
self.send_header("Location", new_url)
self.end_headers()
return None
for index in "index.html", "index.htm":
index = os.path.join(path, index)
if os.path.exists(index):
path = index
break
else:
return self.list_directory(path)
ctype = self.guess_type(path)
try:
f = open(path, 'rb')
except OSError:
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
return None

try:
fs = os.fstat(f.fileno())
content_length = fs[6]
# Use browser cache if possible
if ("If-Modified-Since" in self.headers
and "If-None-Match" not in self.headers):
# compare If-Modified-Since and time of last file modification
try:
ims = email.utils.parsedate_to_datetime(
self.headers["If-Modified-Since"])
except (TypeError, IndexError, OverflowError, ValueError):
# ignore ill-formed values
pass
else:
if ims.tzinfo is None:
# obsolete format with no timezone, cf.
# https://tools.ietf.org/html/rfc7231#section-7.1.1.1
ims = ims.replace(tzinfo=datetime.timezone.utc)
if ims.tzinfo is datetime.timezone.utc:
# compare to UTC datetime of last modification
last_modif = datetime.datetime.fromtimestamp(
fs.st_mtime, datetime.timezone.utc)
# remove microseconds, like in If-Modified-Since
last_modif = last_modif.replace(microsecond=0)

if last_modif <= ims:
self.send_response(HTTPStatus.NOT_MODIFIED)
self.end_headers()
f.close()
return None

self.send_response(HTTPStatus.OK)
self.send_header("Content-type", ctype)
self.send_header("Last-Modified",
self.date_time_string(fs.st_mtime))

if not gzip or ctype not in self.compressed_types:
self.send_header("Content-Length", str(content_length))
self.end_headers()
return f

# Use HTTP compression (gzip) if possible

# Get accepted encodings ; "encodings" is a dictionary mapping
# encodings to their quality ; eg for header "gzip; q=0.8",
# encodings["gzip"] is set to 0.8
accept_encoding = self.headers.get_all("Accept-Encoding", ())
encodings = {}
for accept in http.cookiejar.split_header_words(accept_encoding):
params = iter(accept)
encoding = next(params, ("", ""))[0]
quality, value = next(params, ("", ""))
if quality == "q" and value:
try:
q = float(value)
except ValueError:
# Invalid quality : ignore encoding
q = 0
else:
q = 1 # quality default to 1
if q:
encodings[encoding] = max(encodings.get(encoding, 0), q)

compressions = set(encodings).intersection(self.compressions)
compression = None
if compressions:
# Take the encoding with highest quality
compression = sorted((encodings[enc], enc)
for enc in compressions)[-1][1]
elif '*' in encodings:
# If no specified encoding is supported but "*" is accepted,
# use gzip.
compression = "gzip"
if compression:
# If at least one encoding is accepted, send data compressed
# with the selected compression algorithm.
producer = self.compressions[compression]
self.send_header("Content-Encoding", compression)
if content_length < 2 << 18:
# For small files, load content in memory
with f:
content = b''.join(producer(f))
content_length = len(content)
f = io.BytesIO(content)
else:
chunked = self.protocol_version >= "HTTP/1.1"
if chunked:
# Use Chunked Transfer Encoding (RFC 7230 section 4.1)
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
# Return a generator of pieces of compressed data
return producer(f)

self.send_header("Content-Length", str(content_length))
self.end_headers()
return f
except:
f.close()
raise

def run(port=8080):
server_address, handler = ('', port), RequestHandler
httpd = server.HTTPServer(server_address, handler)

print(("Server running on port http://localhost:{}.".format(server_address[1])))
print("Press CTRL+C to Quit.")
httpd.serve_forever()
18 changes: 12 additions & 6 deletions setup/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
shutil.copyfile(os.path.join(os.path.dirname(os.getcwd()),
"www", "src", "{}.js".format(fname)),
os.path.join("data", "{}.js".format(fname)))

setup(
name='brython',

version='3.3.3',

description='Brython is an implementation of Python 3 running in the browser',

long_description = LONG_DESCRIPTION,

# The project's main homepage.
Expand All @@ -28,9 +28,15 @@
# Author details
author='Pierre Quentel',
author_email='[email protected]',

packages = ['data', 'data.tools'],

entry_points={
'console_scripts': [
'brython = data.__main__:main'
]
},

# Choose your license
license='BSD',

Expand All @@ -40,7 +46,7 @@
# Indicate who your project is intended for
'Intended Audience :: Developers',
'Topic :: Software Development :: Interpreters',

'Operating System :: OS Independent',

# Pick your license as you wish (should match "license" above)
Expand All @@ -60,7 +66,7 @@

# Alternatively, if you want to distribute just a my_module.py, uncomment
# this:
py_modules=["brython", "list_modules"],
py_modules=["brython", "list_modules", "server"],


# If there are data files included in your packages that need to be
Expand All @@ -69,7 +75,7 @@
package_data={
'data': [
'README.txt',
'demo.html',
'demo.html',
'brython.js',
'brython_stdlib.js'
],
Expand Down

0 comments on commit 66ef8c4

Please sign in to comment.