25 Commits

Author SHA1 Message Date
1398cfe3db put some sidebars on the site for readability 2020-10-25 17:48:19 -05:00
f63de031f6 tox updates: run py38, combine coverage, dist-as-dir 2020-10-20 16:07:49 -05:00
46bce5a0a5 recompile all requirements, add flake8-mutable 2020-10-20 16:05:17 -05:00
0af0f4e8aa tox.ini updates, use requirements-dev.txt, fix pathing 2020-06-23 13:33:15 -05:00
08896a18c1 reorganize requirements-dev.in, add dlint and flake8-fixme, bandit 2020-06-23 13:30:49 -05:00
ea7c9a1e07 let TODOs through linting, but warn about them 2020-06-22 19:09:39 -05:00
63da59efd5 enable flake8-logging-format violations 2020-06-22 18:50:13 -05:00
c7d4a1c930 add any suppressed flake8-fixme messages in the fail-open run 2020-06-22 18:49:34 -05:00
421d0e6f8e properly create the symlink to dist/ across multiple runs of tox 2020-06-22 18:48:22 -05:00
5c1fc93ff9 combine tox deps in order to unconfuse flake8-isort
with pytest not being included in the lint environment, flake8-isort
didn't know how to treat it vs. incorporealcms imports, leading to false
positives only inside tox. this makes it so that certain packages
(defined in base deps) can be imported in any/all envs, because they
show up in analyzed/imported/etc code rather than being merely tools
2020-06-22 18:48:18 -05:00
7b5f7ff00b add dlint and flake8-fixme 2020-06-20 10:48:46 -05:00
9db5189c65 add flake8-isort, with a caveat 2020-06-19 20:23:23 -05:00
ab2d754e43 reorganize tox.ini a bit and use pytest-cov rather than coverage directly 2020-06-19 20:01:06 -05:00
0f7495bf2b add the ability to redirect a file-looking request to a dir
if the client has requested /foo, and foo is actually a directory,
this redirects the client to /foo/
2020-06-19 19:58:12 -05:00
cf8f0325a2 fix /most/ isort problems, but conftest.py is being weird 2020-06-19 19:54:01 -05:00
718b217868 add flake8 and many plugins to requirements-dev, for vim's sake 2020-06-19 19:40:01 -05:00
ebaccbd0ad organize tests a bit better between unit and functional tests 2020-06-18 23:36:51 -05:00
63f13398e0 versioneer.py doesn't need to be included in the package 2020-06-18 23:29:37 -05:00
605a82680d add bandit and flake8 plugins to tox, remove redundant deps 2020-06-18 17:39:34 -05:00
14f6125f4e use new-style tox.ini, add flake8-docstrings, add docstrings 2020-06-17 20:18:43 -05:00
21f65813fb properly run pytest + cov in the tox env 2020-06-17 16:34:50 -05:00
f77aebb097 replace CI tools with tox invocation 2020-06-16 23:00:49 -05:00
5994b73b2e give tables a lighter border 2020-06-14 10:56:57 -05:00
dadc902c49 put a bit of a background behind blockquote
closes #2
2020-06-14 10:55:24 -05:00
5c8251d01a explicitly set the footer margin-top 2020-06-14 10:01:07 -05:00
17 changed files with 326 additions and 91 deletions

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@ __pycache__/
build/
develop-eggs/
dist/
dist
downloads/
eggs/
.eggs/

View File

@@ -1,5 +1,4 @@
graft incorporealcms/static
graft incorporealcms/templates
include versioneer.py
global-exclude *.pyc
global-exclude *.swp

View File

@@ -1,4 +1,4 @@
"""create_app application factory function and similar things."""
"""An application for running my Markdown-based sites."""
import logging
import os
from logging.config import dictConfig
@@ -12,6 +12,7 @@ del get_versions
def create_app(instance_path=None, test_config=None):
"""Create the Flask app, with allowances for customizing path and test settings."""
app = Flask(__name__, instance_relative_config=True, instance_path=instance_path)
# if it doesn't already exist, create the instance folder

View File

@@ -4,7 +4,9 @@ import logging
import os
import markdown
from flask import Blueprint, Markup, abort, current_app as app, render_template
from flask import Blueprint, Markup, abort
from flask import current_app as app
from flask import redirect, render_template
from tzlocal import get_localzone
logger = logging.getLogger(__name__)
@@ -17,6 +19,9 @@ md = markdown.Markdown(extensions=['meta', 'tables'])
@bp.route('/<path:path>')
def display_page(path):
"""Get the file contents of the requested path and render the file."""
if is_file_path_actually_dir_path(path):
return redirect(f'{path}/', code=301)
resolved_path = resolve_page_file(path)
logger.debug("received request for path '%s', resolved to '%s'", path, resolved_path)
try:
@@ -50,6 +55,22 @@ def resolve_page_file(path):
return path
def is_file_path_actually_dir_path(path):
"""Check if requested path which looks like a file is actually a directory.
If, for example, /foo used to be a file (foo.md) which later became a directory,
foo/, this returns True. Useful for when I make my structure more complicated
than it originally was, or if users are just weird.
"""
if not path.endswith('/'):
logger.debug("requested path '%s' looks like a file", path)
if os.path.isdir(f'{app.instance_path}/pages/{path}'):
logger.debug("...and it's actually a dir")
return True
return False
def generate_parent_navs(path):
"""Create a series of paths/links to navigate up from the given path."""
# derive additional path/location stuff based on path

View File

@@ -1,14 +1,29 @@
html {
font-family: sans-serif;
padding: 0;
padding-bottom: 16px;
color: #222;
}
body {
background: #888;
margin: 0;
}
.site-wrap {
background: white;
max-width: 70pc;
height: 100vh;
margin: 0;
margin-left: auto;
margin-right: auto;
border: 1px solid #ddd;
border-top: none;
border-bottom: none;
}
h1,h2,h3,h4,h5,h6 {
color: #811610;
}
@@ -90,6 +105,8 @@ footer {
font-size: 75%;
color: #999;
padding: 0 1em;
padding-bottom: 16px;
margin-top: 15px;
}
table {
@@ -98,10 +115,15 @@ table {
table, th, td {
padding: 5px;
border: 1px solid #222;
border: 1px solid #ccc;
margin-bottom: 15px;
}
th {
background: #eee;
}
blockquote {
background-color: rgba(120, 120, 120, 0.1);
padding: 1px 10px;
}

View File

@@ -2,15 +2,17 @@
<title>{{ title }}{% if title %} - {% endif %}{{ config.TITLE_SUFFIX }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}">
<section class="nav">
{% for nav in navs %}
<a href="{{ nav.1 }}">{{ nav.0 }}</a>
{% if not loop.last %} &raquo; {% endif %}
{% endfor %}
<section class="site-wrap">
<section class="nav">
{% for nav in navs %}
<a href="{{ nav.1 }}">{{ nav.0 }}</a>
{% if not loop.last %} &raquo; {% endif %}
{% endfor %}
</section>
<section class="content">
{{ content }}
</section>
<footer>
<i>Last modified: {{ mtime }}</i>
</footer>
</section>
<section class="content">
{{ content }}
</section>
<footer>
<i>Last modified: {{ mtime }}</i>
</footer>

View File

@@ -1,7 +1,24 @@
-r requirements.in
flake8 # python code quality stuff
# testing runner, test reporting, packages used during testing (e.g. requests-mock), etc.
pytest
pytest-cov
# linting and other static code analysis
bandit
dlint
flake8
flake8-blind-except
flake8-builtins
flake8-docstrings
flake8-executable
flake8-fixme
flake8-isort
flake8-logging-format
flake8-mutable
# maintenance utilities and tox
pip-tools # pip-compile
pytest # unit tests
pytest-cov # coverage in unit tests
tox # CI stuff
tox-wheel # build wheels in tox
versioneer # automatic version numbering

View File

@@ -4,34 +4,62 @@
#
# pip-compile --output-file=requirements/requirements-dev.txt requirements/requirements-dev.in
#
attrs==19.3.0 # via pytest
appdirs==1.4.4 # via virtualenv
attrs==20.2.0 # via pytest
bandit==1.6.2 # via -r requirements/requirements-dev.in
click==7.1.2 # via flask, pip-tools
coverage==5.1 # via pytest-cov
flake8==3.8.2 # via -r requirements/requirements-dev.in
coverage==5.3 # via pytest-cov
distlib==0.3.1 # via virtualenv
dlint==0.10.3 # via -r requirements/requirements-dev.in
filelock==3.0.12 # via tox, virtualenv
flake8-blind-except==0.1.1 # via -r requirements/requirements-dev.in
flake8-builtins==1.5.3 # via -r requirements/requirements-dev.in
flake8-docstrings==1.5.0 # via -r requirements/requirements-dev.in
flake8-executable==2.0.4 # via -r requirements/requirements-dev.in
flake8-fixme==1.1.1 # via -r requirements/requirements-dev.in
flake8-isort==4.0.0 # via -r requirements/requirements-dev.in
flake8-logging-format==0.6.0 # via -r requirements/requirements-dev.in
flake8-mutable==1.2.0 # via -r requirements/requirements-dev.in
flake8==3.8.4 # via -r requirements/requirements-dev.in, dlint, flake8-builtins, flake8-docstrings, flake8-executable, flake8-isort, flake8-mutable
flask==1.1.2 # via -r requirements/requirements.in
importlib-metadata==1.6.0 # via flake8, markdown, pluggy, pytest
gitdb==4.0.5 # via gitpython
gitpython==3.1.9 # via bandit
importlib-metadata==2.0.0 # via flake8, markdown, pluggy, pytest, stevedore, tox, virtualenv
iniconfig==1.1.1 # via pytest
isort==5.6.4 # via flake8-isort
itsdangerous==1.1.0 # via flask
jinja2==2.11.2 # via flask
markdown==3.2.2 # via -r requirements/requirements.in
markdown==3.3.2 # via -r requirements/requirements.in
markupsafe==1.1.1 # via jinja2
mccabe==0.6.1 # via flake8
more-itertools==8.3.0 # via pytest
packaging==20.4 # via pytest
pip-tools==5.2.0 # via -r requirements/requirements-dev.in
pluggy==0.13.1 # via pytest
py==1.8.1 # via pytest
packaging==20.4 # via pytest, tox
pbr==5.5.1 # via stevedore
pip-tools==5.3.1 # via -r requirements/requirements-dev.in
pluggy==0.13.1 # via pytest, tox
py==1.9.0 # via pytest, tox
pycodestyle==2.6.0 # via flake8
pydocstyle==5.1.1 # via flake8-docstrings
pyflakes==2.2.0 # via flake8
pyparsing==2.4.7 # via packaging
pytest-cov==2.9.0 # via -r requirements/requirements-dev.in
pytest==5.4.2 # via -r requirements/requirements-dev.in, pytest-cov
pytest-cov==2.10.1 # via -r requirements/requirements-dev.in
pytest==6.1.1 # via -r requirements/requirements-dev.in, pytest-cov
pytz==2020.1 # via tzlocal
six==1.15.0 # via packaging, pip-tools
pyyaml==5.3.1 # via bandit
six==1.15.0 # via bandit, packaging, pip-tools, tox, virtualenv
smmap==3.0.4 # via gitdb
snowballstemmer==2.0.0 # via pydocstyle
stevedore==3.2.2 # via bandit
testfixtures==6.15.0 # via flake8-isort
toml==0.10.1 # via pytest, tox
tox-wheel==0.5.0 # via -r requirements/requirements-dev.in
tox==3.20.1 # via -r requirements/requirements-dev.in, tox-wheel
tzlocal==2.1 # via -r requirements/requirements.in
versioneer==0.18 # via -r requirements/requirements-dev.in
wcwidth==0.1.9 # via pytest
virtualenv==20.0.35 # via tox
werkzeug==1.0.1 # via flask
zipp==3.1.0 # via importlib-metadata
wheel==0.35.1 # via tox-wheel
zipp==3.3.1 # via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
# pip
# setuptools

View File

@@ -6,12 +6,12 @@
#
click==7.1.2 # via flask
flask==1.1.2 # via -r requirements/requirements.in
importlib-metadata==1.6.0 # via markdown
importlib-metadata==2.0.0 # via markdown
itsdangerous==1.1.0 # via flask
jinja2==2.11.2 # via flask
markdown==3.2.2 # via -r requirements/requirements.in
markdown==3.3.2 # via -r requirements/requirements.in
markupsafe==1.1.1 # via jinja2
pytz==2020.1 # via tzlocal
tzlocal==2.1 # via -r requirements/requirements.in
werkzeug==1.0.1 # via flask
zipp==3.1.0 # via importlib-metadata
zipp==3.3.1 # via importlib-metadata

View File

@@ -1,23 +1,3 @@
[coverage:run]
branch = True
omit =
.venv/*
incorporealcms/_version.py
setup.py
tests/*
versioneer.py
source =
incorporealcms
[flake8]
exclude = .git,.venv,__pycache__,versioneer.py,incorporealcms/_version.py
max-line-length = 120
[tool:pytest]
addopts = --cov=. --cov-report=term --cov-report=term-missing
log_cli = 1
log_cli_level = DEBUG
[versioneer]
VCS = git
style = pep440-post

View File

@@ -1,5 +1,6 @@
"""Setuptools configuration."""
import os
from setuptools import find_packages, setup
import versioneer

View File

@@ -10,6 +10,7 @@ HERE = os.path.dirname(os.path.abspath(__file__))
@pytest.fixture
def app():
"""Create the Flask application, with test settings."""
app = create_app(instance_path=os.path.join(HERE, 'instance'))
yield app
@@ -17,4 +18,5 @@ def app():
@pytest.fixture
def client(app):
"""Create a test client based on the test app."""
return app.test_client()

54
tests/functional_tests.py Normal file
View File

@@ -0,0 +1,54 @@
"""Test page requests."""
import re
def test_page_that_exists(client):
"""Test that the app can serve a basic file at the index."""
response = client.get('/')
assert response.status_code == 200
assert b'<h1>test index</h1>' in response.data
def test_page_that_doesnt_exist(client):
"""Test that the app returns 404 for nonsense requests."""
response = client.get('/ohuesthaoeusth')
assert response.status_code == 404
def test_page_with_title_metadata(client):
"""Test that a page with title metadata has its title written."""
response = client.get('/')
assert response.status_code == 200
assert b'<title>Index - incorporeal.org</title>' in response.data
def test_page_without_title_metadata(client):
"""Test that a page without title metadata gets the default title."""
response = client.get('/no-title')
assert response.status_code == 200
assert b'<title>incorporeal.org</title>' in response.data
assert b'<h1>this page doesn\'t have a title!</h1>' in response.data
def test_page_has_modified_timestamp(client):
"""Test that pages have modified timestamps in them."""
response = client.get('/')
assert response.status_code == 200
assert re.search(r'Last modified: ....-..-.. ..:..:.. ...', response.data.decode()) is not None
def test_that_page_request_redirects_to_directory(client):
"""Test that a request to /foo reirects to /foo/, if foo is a directory.
This might be useful in cases where a formerly page-only page has been
converted to a directory with subpages.
"""
response = client.get('/subdir')
assert response.status_code == 301
def test_that_dir_request_does_not_redirect(client):
"""Test that a request to /foo/ serves the index page, if foo is a directory."""
response = client.get('/subdir/')
assert response.status_code == 200
assert b'another page' in response.data

View File

@@ -1,3 +1,5 @@
"""Configure the test application."""
LOGGING = {
'version': 1,
'formatters': {

View File

@@ -24,6 +24,7 @@ def test_title_override():
def test_media_file_access(client):
"""Test that media files are served, and properly."""
response = client.get('/media/favicon.png')
assert response.status_code == 200
assert response.headers['content-type'] == 'image/png'

View File

@@ -1,76 +1,76 @@
"""Test page views and helper methods."""
import re
from incorporealcms.pages import generate_parent_navs, resolve_page_file
"""Unit test helper methods."""
from incorporealcms.pages import generate_parent_navs, is_file_path_actually_dir_path, resolve_page_file
def test_resolve_page_file_dir_to_index():
"""Test that a request to a directory path results in the dir's index.md."""
assert resolve_page_file('foo/') == 'pages/foo/index.md'
def test_resolve_page_file_subdir_to_index():
"""Test that a request to a dir's subdir path results in the subdir's index.md."""
assert resolve_page_file('foo/bar/') == 'pages/foo/bar/index.md'
def test_resolve_page_file_other_requests_fine():
"""Test that a request to non-dir path results in a Markdown file."""
assert resolve_page_file('foo/baz') == 'pages/foo/baz.md'
def test_page_that_exists(client):
response = client.get('/')
assert response.status_code == 200
assert b'<h1>test index</h1>' in response.data
def test_page_that_doesnt_exist(client):
response = client.get('/ohuesthaoeusth')
assert response.status_code == 404
def test_page_with_title_metadata(client):
response = client.get('/')
assert response.status_code == 200
assert b'<title>Index - incorporeal.org</title>' in response.data
def test_page_without_title_metadata(client):
response = client.get('/no-title')
assert response.status_code == 200
assert b'<title>incorporeal.org</title>' in response.data
assert b'<h1>this page doesn\'t have a title!</h1>' in response.data
def test_page_has_modified_timestamp(client):
response = client.get('/')
assert response.status_code == 200
assert re.search(r'Last modified: ....-..-.. ..:..:.. ...', response.data.decode()) is not None
def test_generate_page_navs_index(app):
"""Test that the index page has navs to the root (itself)."""
with app.app_context():
assert generate_parent_navs('/') == [('incorporeal.org', '/')]
def test_generate_page_navs_alternate_index(app):
"""Test that the index page (as a page, not a path) also has navs only to the root (by path)."""
with app.app_context():
assert generate_parent_navs('index') == [('incorporeal.org', '/')]
def test_generate_page_navs_subdir_index(app):
"""Test that dir pages have navs to the root and themselves."""
with app.app_context():
assert generate_parent_navs('subdir/') == [('incorporeal.org', '/'), ('/subdir/', '/subdir/')]
def test_generate_page_navs_subdir_real_page(app):
"""Test that real pages have navs to the root, their parent, and themselves."""
with app.app_context():
assert generate_parent_navs('subdir/page') == [('incorporeal.org', '/'), ('/subdir/', '/subdir/'),
('Page', '/subdir/page')]
def test_generate_page_navs_subdir_with_title_parsing_real_page(app):
"""Test that title metadata is used in the nav text."""
with app.app_context():
assert generate_parent_navs('subdir-with-title/page') == [
('incorporeal.org', '/'),
('SUB!', '/subdir-with-title/'),
('/subdir-with-title/page', '/subdir-with-title/page')
]
def test_is_file_path_actually_dir_path_valid_file_is_yes(app):
"""Test that a file request for what's actually a directory is detected as such."""
with app.app_context():
assert is_file_path_actually_dir_path('/subdir')
def test_is_file_path_actually_dir_path_valid_dir_is_no(app):
"""Test that a directory request is still a directory request."""
with app.app_context():
assert not is_file_path_actually_dir_path('/subdir/')
def test_is_file_path_actually_dir_path_nonsense_file_is_no(app):
"""Test that requests to nonsense file-looking paths aren't treated as dirs."""
with app.app_context():
assert not is_file_path_actually_dir_path('/antphnathpnthapnthsnthax')
def test_is_file_path_actually_dir_path_nonsense_dir_is_no(app):
"""Test that a directory request is a directory request even if the dir doesn't exist."""
with app.app_context():
assert not is_file_path_actually_dir_path('/antphnathpnthapnthsnthax/')

104
tox.ini Normal file
View File

@@ -0,0 +1,104 @@
# tox (https://tox.readthedocs.io/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
envlist = begin,py37,py38,coverage,security,lint,bundle
[testenv]
# build a wheel and test it
wheel = true
wheel_build_env = build
# whitelist commands we need
whitelist_externals = cp
# install everything via requirements-dev.txt, so that developer environment
# is the same as the tox environment (for ease of use/no weird gotchas in
# local dev results vs. tox results) and also to avoid ticky-tacky maintenance
# of "oh this particular env has weird results unless I install foo" --- just
# shotgun blast install everything everywhere
deps =
-rrequirements/requirements-dev.txt
[testenv:build]
# require setuptools when building
deps = setuptools
[testenv:begin]
# clean up potential previous coverage runs
skip_install = true
commands = coverage erase
[testenv:py37]
# run pytest with coverage
commands =
pytest --cov-append --cov={envsitepackagesdir}/incorporealcms/ --cov-branch
[testenv:py38]
# run pytest with coverage
commands =
pytest --cov-append --cov={envsitepackagesdir}/incorporealcms/ --cov-branch
[testenv:coverage]
# report on coverage runs from above
skip_install = true
commands =
coverage report --fail-under=95 --show-missing
[testenv:security]
# run security checks
#
# again it seems the most valuable here to run against the packaged code
commands =
bandit {envsitepackagesdir}/incorporealcms/ -r
[testenv:lint]
# run style checks
commands =
flake8
- flake8 --disable-noqa --ignore= --select=E,W,F,C,D,A,G,B,I,T,M,DUO
[testenv:bundle]
# take extra actions (build sdist, sphinx, whatever) to completely package the app
commands =
cp -r {distdir} .
python setup.py sdist
[coverage:paths]
source =
./
.tox/**/site-packages/
[coverage:run]
branch = True
# redundant with pytest --cov above, but this tricks the coverage.xml report into
# using the full path, otherwise files with the same name in different paths
# get clobbered. maybe appends would fix this, IDK
include =
.tox/**/incorporealcms/
omit =
**/_version.py
[flake8]
enable-extensions = G,M
exclude =
.tox/
versioneer.py
_version.py
instance/
extend-ignore = T101
max-complexity = 10
max-line-length = 120
[isort]
line_length = 120
[pytest]
python_files =
*_tests.py
tests.py
test_*.py