Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b810346aa4 | ||
|
|
ae5d330b2d | ||
|
|
4c0afa2bec | ||
|
|
125af4b4a9 | ||
|
|
c5e1873523 | ||
|
|
dfbe2edd79 | ||
|
|
3e29af5775 | ||
|
|
1aacb3cf46 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
tests/files/* binary
|
||||||
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
github: miguelgrinberg
|
||||||
|
patreon: miguelgrinberg
|
||||||
|
custom: https://paypal.me/miguelgrinberg
|
||||||
53
.github/workflows/tests.yml
vendored
Normal file
53
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
- run: python -m pip install --upgrade pip wheel
|
||||||
|
- run: pip install tox tox-gh-actions
|
||||||
|
- run: tox -eflake8
|
||||||
|
tests:
|
||||||
|
name: tests
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
python: ['3.6', '3.7', '3.8', '3.9']
|
||||||
|
fail-fast: false
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python }}
|
||||||
|
- run: python -m pip install --upgrade pip wheel
|
||||||
|
- run: pip install tox tox-gh-actions
|
||||||
|
- run: tox
|
||||||
|
tests-micropython:
|
||||||
|
name: tests-micropython
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
- run: python -m pip install --upgrade pip wheel
|
||||||
|
- run: pip install tox tox-gh-actions
|
||||||
|
- run: tox -eupy
|
||||||
|
coverage:
|
||||||
|
name: coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
- run: python -m pip install --upgrade pip wheel
|
||||||
|
- run: pip install tox tox-gh-actions codecov
|
||||||
|
- run: tox
|
||||||
|
- run: codecov
|
||||||
18
.travis.yml
18
.travis.yml
@@ -1,18 +0,0 @@
|
|||||||
dist: xenial
|
|
||||||
language: python
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- python: 3.7
|
|
||||||
env: TOXENV=flake8
|
|
||||||
- python: 3.5
|
|
||||||
env: TOXENV=py35
|
|
||||||
- python: 3.6
|
|
||||||
env: TOXENV=py36
|
|
||||||
- python: 3.7
|
|
||||||
env: TOXENV=py37
|
|
||||||
- python: 3.7
|
|
||||||
env: TOXENV=upy
|
|
||||||
install:
|
|
||||||
- pip install tox
|
|
||||||
script:
|
|
||||||
- tox
|
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
# microdot
|
# microdot
|
||||||
[](https://travis-ci.org/miguelgrinberg/microdot)
|
[](https://github.com/miguelgrinberg/microdot/actions) [](https://codecov.io/gh/miguelgrinberg/microdot)
|
||||||
|
|
||||||
A minimalistic Python web framework for microcontrollers inspired by Flask
|
A minimalistic Python web framework for microcontrollers inspired by Flask
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Coming soon!
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ set -e
|
|||||||
for PKG in microdot*; do
|
for PKG in microdot*; do
|
||||||
echo Building $PKG...
|
echo Building $PKG...
|
||||||
cd $PKG
|
cd $PKG
|
||||||
sed -i "s/version.*$/version=\"$VERSION\",/" setup.py
|
sed -i "" "s/version.*$/version=\"$VERSION\",/" setup.py
|
||||||
git add setup.py
|
git add setup.py
|
||||||
rm -rf dist
|
rm -rf dist
|
||||||
python setup.py sdist bdist_wheel --universal
|
python setup.py sdist bdist_wheel --universal
|
||||||
|
|||||||
26
examples/hello.py
Normal file
26
examples/hello.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from microdot import Microdot, Response
|
||||||
|
|
||||||
|
app = Microdot()
|
||||||
|
|
||||||
|
htmldoc = """<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Microdot Example Page</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<h1>Microdot Example Page</h1>
|
||||||
|
<p>Hello from Microdot!</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("", methods=["GET", "POST"])
|
||||||
|
def serial_number(request):
|
||||||
|
print(request.headers)
|
||||||
|
return Response(body=htmldoc, headers={"Content-Type": "text/html"})
|
||||||
|
|
||||||
|
|
||||||
|
app.run(debug=True)
|
||||||
@@ -17,6 +17,8 @@ class Request(BaseRequest):
|
|||||||
async def create(stream, client_addr):
|
async def create(stream, client_addr):
|
||||||
# request line
|
# request line
|
||||||
line = (await stream.readline()).strip().decode()
|
line = (await stream.readline()).strip().decode()
|
||||||
|
if not line: # pragma: no cover
|
||||||
|
return None
|
||||||
method, url, http_version = line.split()
|
method, url, http_version = line.split()
|
||||||
http_version = http_version.split('/', 1)[1]
|
http_version = http_version.split('/', 1)[1]
|
||||||
|
|
||||||
@@ -59,7 +61,17 @@ class Response(BaseResponse):
|
|||||||
|
|
||||||
# body
|
# body
|
||||||
if self.body:
|
if self.body:
|
||||||
await stream.awrite(self.body)
|
if hasattr(self.body, 'read'):
|
||||||
|
while True:
|
||||||
|
buf = self.body.read(self.send_file_buffer_size)
|
||||||
|
if len(buf):
|
||||||
|
await stream.awrite(buf)
|
||||||
|
if len(buf) < self.send_file_buffer_size:
|
||||||
|
break
|
||||||
|
if hasattr(self.body, 'close'):
|
||||||
|
self.body.close()
|
||||||
|
else:
|
||||||
|
await stream.awrite(self.body)
|
||||||
|
|
||||||
|
|
||||||
class Microdot(BaseMicrodot):
|
class Microdot(BaseMicrodot):
|
||||||
@@ -93,48 +105,52 @@ class Microdot(BaseMicrodot):
|
|||||||
|
|
||||||
async def dispatch_request(self, reader, writer):
|
async def dispatch_request(self, reader, writer):
|
||||||
req = await Request.create(reader, writer.get_extra_info('peername'))
|
req = await Request.create(reader, writer.get_extra_info('peername'))
|
||||||
f = self.find_route(req)
|
if req:
|
||||||
try:
|
f = self.find_route(req)
|
||||||
res = None
|
try:
|
||||||
if f:
|
res = None
|
||||||
for handler in self.before_request_handlers:
|
if f:
|
||||||
res = await self._invoke_handler(handler, req)
|
for handler in self.before_request_handlers:
|
||||||
if res:
|
res = await self._invoke_handler(handler, req)
|
||||||
break
|
if res:
|
||||||
if res is None:
|
break
|
||||||
res = await self._invoke_handler(f, req, **req.url_args)
|
if res is None:
|
||||||
if isinstance(res, tuple):
|
res = await self._invoke_handler(
|
||||||
res = Response(*res)
|
f, req, **req.url_args)
|
||||||
elif not isinstance(res, Response):
|
if isinstance(res, tuple):
|
||||||
res = Response(res)
|
res = Response(*res)
|
||||||
for handler in self.after_request_handlers:
|
elif not isinstance(res, Response):
|
||||||
res = await self._invoke_handler(handler, req, res) or res
|
res = Response(res)
|
||||||
elif 404 in self.error_handlers:
|
for handler in self.after_request_handlers:
|
||||||
res = await self._invoke_handler(self.error_handlers[404], req)
|
res = await self._invoke_handler(
|
||||||
else:
|
handler, req, res) or res
|
||||||
res = 'Not found', 404
|
elif 404 in self.error_handlers:
|
||||||
except Exception as exc:
|
|
||||||
print_exception(exc)
|
|
||||||
res = None
|
|
||||||
if exc.__class__ in self.error_handlers:
|
|
||||||
try:
|
|
||||||
res = await self._invoke_handler(
|
res = await self._invoke_handler(
|
||||||
self.error_handlers[exc.__class__], req, exc)
|
self.error_handlers[404], req)
|
||||||
except Exception as exc2: # pragma: no cover
|
|
||||||
print_exception(exc2)
|
|
||||||
if res is None:
|
|
||||||
if 500 in self.error_handlers:
|
|
||||||
res = await self._invoke_handler(self.error_handlers[500],
|
|
||||||
req)
|
|
||||||
else:
|
else:
|
||||||
res = 'Internal server error', 500
|
res = 'Not found', 404
|
||||||
if isinstance(res, tuple):
|
except Exception as exc:
|
||||||
res = Response(*res)
|
print_exception(exc)
|
||||||
elif not isinstance(res, Response):
|
res = None
|
||||||
res = Response(res)
|
if exc.__class__ in self.error_handlers:
|
||||||
await res.write(writer)
|
try:
|
||||||
|
res = await self._invoke_handler(
|
||||||
|
self.error_handlers[exc.__class__], req, exc)
|
||||||
|
except Exception as exc2: # pragma: no cover
|
||||||
|
print_exception(exc2)
|
||||||
|
if res is None:
|
||||||
|
if 500 in self.error_handlers:
|
||||||
|
res = await self._invoke_handler(
|
||||||
|
self.error_handlers[500], req)
|
||||||
|
else:
|
||||||
|
res = 'Internal server error', 500
|
||||||
|
if isinstance(res, tuple):
|
||||||
|
res = Response(*res)
|
||||||
|
elif not isinstance(res, Response):
|
||||||
|
res = Response(res)
|
||||||
|
await res.write(writer)
|
||||||
await writer.aclose()
|
await writer.aclose()
|
||||||
if self.debug: # pragma: no cover
|
if self.debug and req: # pragma: no cover
|
||||||
print('{method} {path} {status_code}'.format(
|
print('{method} {path} {status_code}'.format(
|
||||||
method=req.method, path=req.path,
|
method=req.method, path=req.path,
|
||||||
status_code=res.status_code))
|
status_code=res.status_code))
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from setuptools import setup
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='microdot-asyncio',
|
name='microdot-asyncio',
|
||||||
version="0.3.0",
|
version="0.3.1",
|
||||||
url='http://github.com/miguelgrinberg/microdot/',
|
url='http://github.com/miguelgrinberg/microdot/',
|
||||||
license='MIT',
|
license='MIT',
|
||||||
author='Miguel Grinberg',
|
author='Miguel Grinberg',
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ except ImportError:
|
|||||||
try:
|
try:
|
||||||
import usocket as socket
|
import usocket as socket
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import socket
|
try:
|
||||||
|
import socket
|
||||||
|
except ImportError: # pragma: no cover
|
||||||
|
socket = None
|
||||||
|
|
||||||
|
|
||||||
def urldecode(string):
|
def urldecode(string):
|
||||||
@@ -99,6 +102,8 @@ class Request():
|
|||||||
def create(client_stream, client_addr):
|
def create(client_stream, client_addr):
|
||||||
# request line
|
# request line
|
||||||
line = client_stream.readline().strip().decode()
|
line = client_stream.readline().strip().decode()
|
||||||
|
if not line: # pragma: no cover
|
||||||
|
return None
|
||||||
method, url, http_version = line.split()
|
method, url, http_version = line.split()
|
||||||
http_version = http_version.split('/', 1)[1]
|
http_version = http_version.split('/', 1)[1]
|
||||||
|
|
||||||
@@ -154,6 +159,7 @@ class Response():
|
|||||||
'png': 'image/png',
|
'png': 'image/png',
|
||||||
'txt': 'text/plain',
|
'txt': 'text/plain',
|
||||||
}
|
}
|
||||||
|
send_file_buffer_size = 1024
|
||||||
|
|
||||||
def __init__(self, body='', status_code=200, headers=None):
|
def __init__(self, body='', status_code=200, headers=None):
|
||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
@@ -163,10 +169,9 @@ class Response():
|
|||||||
self.headers['Content-Type'] = 'application/json'
|
self.headers['Content-Type'] = 'application/json'
|
||||||
elif isinstance(body, str):
|
elif isinstance(body, str):
|
||||||
self.body = body.encode()
|
self.body = body.encode()
|
||||||
elif isinstance(body, bytes):
|
|
||||||
self.body = body
|
|
||||||
else:
|
else:
|
||||||
self.body = str(body).encode()
|
# this applies to bytes or file-like objects
|
||||||
|
self.body = body
|
||||||
|
|
||||||
def set_cookie(self, cookie, value, path=None, domain=None, expires=None,
|
def set_cookie(self, cookie, value, path=None, domain=None, expires=None,
|
||||||
max_age=None, secure=False, http_only=False):
|
max_age=None, secure=False, http_only=False):
|
||||||
@@ -190,7 +195,8 @@ class Response():
|
|||||||
self.headers['Set-Cookie'] = [http_cookie]
|
self.headers['Set-Cookie'] = [http_cookie]
|
||||||
|
|
||||||
def complete(self):
|
def complete(self):
|
||||||
if 'Content-Length' not in self.headers:
|
if isinstance(self.body, bytes) and \
|
||||||
|
'Content-Length' not in self.headers:
|
||||||
self.headers['Content-Length'] = str(len(self.body))
|
self.headers['Content-Length'] = str(len(self.body))
|
||||||
if 'Content-Type' not in self.headers:
|
if 'Content-Type' not in self.headers:
|
||||||
self.headers['Content-Type'] = 'text/plain'
|
self.headers['Content-Type'] = 'text/plain'
|
||||||
@@ -213,7 +219,17 @@ class Response():
|
|||||||
|
|
||||||
# body
|
# body
|
||||||
if self.body:
|
if self.body:
|
||||||
stream.write(self.body)
|
if hasattr(self.body, 'read'):
|
||||||
|
while True:
|
||||||
|
buf = self.body.read(self.send_file_buffer_size)
|
||||||
|
if len(buf):
|
||||||
|
stream.write(buf)
|
||||||
|
if len(buf) < self.send_file_buffer_size:
|
||||||
|
break
|
||||||
|
if hasattr(self.body, 'close'):
|
||||||
|
self.body.close()
|
||||||
|
else:
|
||||||
|
stream.write(self.body)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def redirect(cls, location, status_code=302):
|
def redirect(cls, location, status_code=302):
|
||||||
@@ -227,11 +243,9 @@ class Response():
|
|||||||
content_type = Response.types_map[ext]
|
content_type = Response.types_map[ext]
|
||||||
else:
|
else:
|
||||||
content_type = 'application/octet-stream'
|
content_type = 'application/octet-stream'
|
||||||
with open(filename) as f:
|
f = open(filename, 'rb')
|
||||||
body = f.read()
|
return cls(body=f, status_code=status_code,
|
||||||
return cls(body=body, status_code=status_code,
|
headers={'Content-Type': content_type})
|
||||||
headers={'Content-Type': content_type,
|
|
||||||
'Content-Length': str(len(body))})
|
|
||||||
|
|
||||||
|
|
||||||
class URLPattern():
|
class URLPattern():
|
||||||
@@ -350,48 +364,49 @@ class Microdot():
|
|||||||
stream = sock
|
stream = sock
|
||||||
|
|
||||||
req = Request.create(stream, addr)
|
req = Request.create(stream, addr)
|
||||||
f = self.find_route(req)
|
if req:
|
||||||
try:
|
f = self.find_route(req)
|
||||||
res = None
|
try:
|
||||||
if f:
|
res = None
|
||||||
for handler in self.before_request_handlers:
|
if f:
|
||||||
res = handler(req)
|
for handler in self.before_request_handlers:
|
||||||
if res:
|
res = handler(req)
|
||||||
break
|
if res:
|
||||||
if res is None:
|
break
|
||||||
res = f(req, **req.url_args)
|
if res is None:
|
||||||
if isinstance(res, tuple):
|
res = f(req, **req.url_args)
|
||||||
res = Response(*res)
|
if isinstance(res, tuple):
|
||||||
elif not isinstance(res, Response):
|
res = Response(*res)
|
||||||
res = Response(res)
|
elif not isinstance(res, Response):
|
||||||
for handler in self.after_request_handlers:
|
res = Response(res)
|
||||||
res = handler(req, res) or res
|
for handler in self.after_request_handlers:
|
||||||
elif 404 in self.error_handlers:
|
res = handler(req, res) or res
|
||||||
res = self.error_handlers[404](req)
|
elif 404 in self.error_handlers:
|
||||||
else:
|
res = self.error_handlers[404](req)
|
||||||
res = 'Not found', 404
|
|
||||||
except Exception as exc:
|
|
||||||
print_exception(exc)
|
|
||||||
res = None
|
|
||||||
if exc.__class__ in self.error_handlers:
|
|
||||||
try:
|
|
||||||
res = self.error_handlers[exc.__class__](req, exc)
|
|
||||||
except Exception as exc2: # pragma: no cover
|
|
||||||
print_exception(exc2)
|
|
||||||
if res is None:
|
|
||||||
if 500 in self.error_handlers:
|
|
||||||
res = self.error_handlers[500](req)
|
|
||||||
else:
|
else:
|
||||||
res = 'Internal server error', 500
|
res = 'Not found', 404
|
||||||
if isinstance(res, tuple):
|
except Exception as exc:
|
||||||
res = Response(*res)
|
print_exception(exc)
|
||||||
elif not isinstance(res, Response):
|
res = None
|
||||||
res = Response(res)
|
if exc.__class__ in self.error_handlers:
|
||||||
res.write(stream)
|
try:
|
||||||
|
res = self.error_handlers[exc.__class__](req, exc)
|
||||||
|
except Exception as exc2: # pragma: no cover
|
||||||
|
print_exception(exc2)
|
||||||
|
if res is None:
|
||||||
|
if 500 in self.error_handlers:
|
||||||
|
res = self.error_handlers[500](req)
|
||||||
|
else:
|
||||||
|
res = 'Internal server error', 500
|
||||||
|
if isinstance(res, tuple):
|
||||||
|
res = Response(*res)
|
||||||
|
elif not isinstance(res, Response):
|
||||||
|
res = Response(res)
|
||||||
|
res.write(stream)
|
||||||
stream.close()
|
stream.close()
|
||||||
if stream != sock: # pragma: no cover
|
if stream != sock: # pragma: no cover
|
||||||
sock.close()
|
sock.close()
|
||||||
if self.debug: # pragma: no cover
|
if self.debug and req: # pragma: no cover
|
||||||
print('{method} {path} {status_code}'.format(
|
print('{method} {path} {status_code}'.format(
|
||||||
method=req.method, path=req.path,
|
method=req.method, path=req.path,
|
||||||
status_code=res.status_code))
|
status_code=res.status_code))
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from setuptools import setup
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='microdot',
|
name='microdot',
|
||||||
version="0.3.0",
|
version="0.3.1",
|
||||||
url='http://github.com/miguelgrinberg/microdot/',
|
url='http://github.com/miguelgrinberg/microdot/',
|
||||||
license='MIT',
|
license='MIT',
|
||||||
author='Miguel Grinberg',
|
author='Miguel Grinberg',
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class TestResponse(unittest.TestCase):
|
|||||||
res = Response(123)
|
res = Response(123)
|
||||||
self.assertEqual(res.status_code, 200)
|
self.assertEqual(res.status_code, 200)
|
||||||
self.assertEqual(res.headers, {})
|
self.assertEqual(res.headers, {})
|
||||||
self.assertEqual(res.body, b'123')
|
self.assertEqual(res.body, 123)
|
||||||
|
|
||||||
def test_create_with_status_code(self):
|
def test_create_with_status_code(self):
|
||||||
res = Response('not found', 404)
|
res = Response('not found', 404)
|
||||||
@@ -161,11 +161,9 @@ class TestResponse(unittest.TestCase):
|
|||||||
res = Response.send_file('tests/files/' + file)
|
res = Response.send_file('tests/files/' + file)
|
||||||
self.assertEqual(res.status_code, 200)
|
self.assertEqual(res.status_code, 200)
|
||||||
self.assertEqual(res.headers['Content-Type'], content_type)
|
self.assertEqual(res.headers['Content-Type'], content_type)
|
||||||
self.assertEqual(res.headers['Content-Length'], '4')
|
self.assertEqual(res.body.read(), b'foo\n')
|
||||||
self.assertEqual(res.body, b'foo\n')
|
|
||||||
res = Response.send_file('tests/files/test.txt',
|
res = Response.send_file('tests/files/test.txt',
|
||||||
content_type='text/html')
|
content_type='text/html')
|
||||||
self.assertEqual(res.status_code, 200)
|
self.assertEqual(res.status_code, 200)
|
||||||
self.assertEqual(res.headers['Content-Type'], 'text/html')
|
self.assertEqual(res.headers['Content-Type'], 'text/html')
|
||||||
self.assertEqual(res.headers['Content-Length'], '4')
|
self.assertEqual(res.body.read(), b'foo\n')
|
||||||
self.assertEqual(res.body, b'foo\n')
|
|
||||||
|
|||||||
17
tox.ini
17
tox.ini
@@ -1,22 +1,23 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist=flake8,py35,py36,py37,upy
|
envlist=flake8,py36,py37,py38,py39,upy
|
||||||
skipsdist=True
|
skipsdist=True
|
||||||
skip_missing_interpreters=True
|
skip_missing_interpreters=True
|
||||||
|
|
||||||
|
[gh-actions]
|
||||||
|
python =
|
||||||
|
3.6: py36
|
||||||
|
3.7: py37
|
||||||
|
3.8: py38
|
||||||
|
3.9: py39
|
||||||
|
pypy3: pypy3
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands=
|
commands=
|
||||||
pip install -e microdot
|
pip install -e microdot
|
||||||
pip install -e microdot-asyncio
|
pip install -e microdot-asyncio
|
||||||
coverage run --branch --include="microdot*.py" -m unittest tests
|
coverage run --branch --include="microdot*.py" -m unittest tests
|
||||||
coverage report --show-missing
|
coverage report --show-missing
|
||||||
coverage erase
|
|
||||||
deps=coverage
|
deps=coverage
|
||||||
basepython=
|
|
||||||
flake8: python3.7
|
|
||||||
py35: python3.5
|
|
||||||
py36: python3.6
|
|
||||||
py37: python3.7
|
|
||||||
upy: python3.7
|
|
||||||
|
|
||||||
[testenv:flake8]
|
[testenv:flake8]
|
||||||
deps=
|
deps=
|
||||||
|
|||||||
Reference in New Issue
Block a user