Merge pull request #2747 from avaris/github-actions

Add GitHub Actions workflow
This commit is contained in:
Justin Mayer 2020-05-10 07:16:47 +02:00 committed by GitHub
commit 177bc2262c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 236 additions and 102 deletions

159
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,159 @@
name: build
on: [push, pull_request]
env:
# color output for pytest and tox
PYTEST_ADDOPTS: "--color=yes"
PY_COLORS: 1
jobs:
test:
name: Test - ${{ matrix.config.python }} - ${{ matrix.config.os }}
runs-on: ${{ matrix.config.os }}-latest
strategy:
matrix:
config:
- os: ubuntu
python: 3.5
- os: ubuntu
python: 3.6
- os: ubuntu
python: 3.7
- os: ubuntu
python: 3.8
- os: macos
python: 3.7
- os: windows
python: 3.7
steps:
- uses: actions/checkout@v2
- name: Setup Python ${{ matrix.config.python }}
uses: actions/setup-python@v1.1.1
with:
python-version: ${{ matrix.config.python }}
- name: Set pip cache (Linux)
uses: actions/cache@v1
if: startsWith(runner.os, 'Linux')
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements/*') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Setup pip cache (macOS)
uses: actions/cache@v1
if: startsWith(runner.os, 'macOS')
with:
path: ~/Library/Caches/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements/*') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Setup pip cache (Windows)
uses: actions/cache@v1
if: startsWith(runner.os, 'Windows')
with:
path: ~\AppData\Local\pip\Cache
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements/*') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install locale (Linux)
if: startsWith(runner.os, 'Linux')
run: sudo locale-gen fr_FR.UTF-8 tr_TR.UTF-8
- name: Install pandoc
uses: r-lib/actions/setup-pandoc@v1
with:
pandoc-version: "2.9.2"
- name: Install tox
run: python -m pip install -U pip tox
- name: Info
run: |
echo "===== PYTHON ====="
python --version
echo "===== PANDOC ====="
pandoc --version | head -2
- name: Run tests
run: tox -e py${{ matrix.config.python }}
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v1.1.1
with:
python-version: 3.6
- name: Set pip cache (Linux)
uses: actions/cache@v1
if: startsWith(runner.os, 'Linux')
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements/*') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install tox
run: python -m pip install -U pip tox
- name: Check
run: tox -e flake8
docs:
name: Build docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v1.1.1
with:
python-version: 3.6
- name: Set pip cache (Linux)
uses: actions/cache@v1
if: startsWith(runner.os, 'Linux')
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements/*') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install tox
run: python -m pip install -U pip tox
- name: Check
run: tox -e docs
deploy:
name: Deploy
needs: [test, lint, docs]
runs-on: ubuntu-latest
if: ${{ github.ref=='refs/heads/master' && github.event_name!='pull_request' }}
steps:
- uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v1.1.1
with:
python-version: 3.7
- name: Check release
id: check_release
run: |
python -m pip install pip --upgrade
pip install poetry
pip install githubrelease
pip install --pre autopub
echo "##[set-output name=release;]$(autopub check)"
- name: Publish
if: ${{ steps.check_release.outputs.release=='' }}
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
git remote set-url origin https://$GITHUB_TOKEN@github.com/${{ github.repository }}
autopub prepare
poetry build
autopub commit
autopub githubrelease
poetry publish -u __token__ -p $PYPI_PASSWORD

View file

@ -11,15 +11,15 @@ env:
matrix: matrix:
- TOX_ENV=docs - TOX_ENV=docs
- TOX_ENV=flake8 - TOX_ENV=flake8
- TOX_ENV=py35 - TOX_ENV=py3.5
- TOX_ENV=py36 - TOX_ENV=py3.6
matrix: matrix:
include: include:
- python: 3.7 - python: 3.7
sudo: true sudo: true
dist: xenial dist: xenial
env: env:
- TOX_ENV=py37 - TOX_ENV=py3.7
addons: addons:
apt_packages: apt_packages:
- pandoc - pandoc

View file

@ -61,7 +61,7 @@ html_show_sourcelink = False
def setup(app): def setup(app):
# overrides for wide tables in RTD theme # overrides for wide tables in RTD theme
app.add_stylesheet('theme_overrides.css') # path relative to _static app.add_css_file('theme_overrides.css') # path relative to _static
# -- Options for LaTeX output ------------------------------------------------- # -- Options for LaTeX output -------------------------------------------------

View file

@ -242,7 +242,7 @@ as the name of the metadata field, except in all-lowercase characters.
For example, you could add a field called `FacebookImage` to your article For example, you could add a field called `FacebookImage` to your article
metadata, as shown below: metadata, as shown below:
.. code-block:: markdown .. code-block:: md
Title: I love Python more than music Title: I love Python more than music
Date: 2013-11-06 10:06 Date: 2013-11-06 10:06

View file

@ -696,7 +696,7 @@ def path_metadata(full_path, source_path, settings=None):
# Enforce a trailing slash when checking for parent directories. # Enforce a trailing slash when checking for parent directories.
# This prevents false positives when one file or directory's name # This prevents false positives when one file or directory's name
# is a prefix of another's. # is a prefix of another's.
dirpath = os.path.join(path, '') dirpath = posixize_path(os.path.join(path, ''))
if source_path == path or source_path.startswith(dirpath): if source_path == path or source_path.startswith(dirpath):
metadata.update(meta) metadata.update(meta)

View file

@ -160,6 +160,19 @@ def locale_available(locale_):
return True return True
def can_symlink():
res = True
try:
with temporary_folder() as f:
os.symlink(
f,
os.path.join(f, 'symlink')
)
except OSError:
res = False
return res
def get_settings(**kwargs): def get_settings(**kwargs):
"""Provide tweaked setting dictionaries for testing """Provide tweaked setting dictionaries for testing

View file

@ -1,17 +1,11 @@
import os import os
from shutil import rmtree from shutil import rmtree
from tempfile import mkdtemp from tempfile import mkdtemp
from unittest.mock import MagicMock
from pelican.generators import ArticlesGenerator, PagesGenerator from pelican.generators import ArticlesGenerator, PagesGenerator
from pelican.tests.support import get_context, get_settings, unittest from pelican.tests.support import get_context, get_settings, unittest
try:
from unittest.mock import MagicMock
except ImportError:
try:
from mock import MagicMock
except ImportError:
MagicMock = False
CUR_DIR = os.path.dirname(__file__) CUR_DIR = os.path.dirname(__file__)
CONTENT_DIR = os.path.join(CUR_DIR, 'content') CONTENT_DIR = os.path.join(CUR_DIR, 'content')
@ -131,7 +125,6 @@ class TestCache(unittest.TestCase):
self.assertEqual(uncached_pages, cached_pages) self.assertEqual(uncached_pages, cached_pages)
self.assertEqual(uncached_hidden_pages, cached_hidden_pages) self.assertEqual(uncached_hidden_pages, cached_hidden_pages)
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_article_object_caching(self): def test_article_object_caching(self):
"""Test Article objects caching at the generator level""" """Test Article objects caching at the generator level"""
settings = self._get_cache_enabled_settings() settings = self._get_cache_enabled_settings()
@ -162,7 +155,6 @@ class TestCache(unittest.TestCase):
""" """
self.assertEqual(generator.readers.read_file.call_count, 6) self.assertEqual(generator.readers.read_file.call_count, 6)
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_article_reader_content_caching(self): def test_article_reader_content_caching(self):
"""Test raw article content caching at the reader level""" """Test raw article content caching at the reader level"""
settings = self._get_cache_enabled_settings() settings = self._get_cache_enabled_settings()
@ -185,7 +177,6 @@ class TestCache(unittest.TestCase):
for reader in readers.values(): for reader in readers.values():
self.assertEqual(reader.read.call_count, 0) self.assertEqual(reader.read.call_count, 0)
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_article_ignore_cache(self): def test_article_ignore_cache(self):
"""Test that all the articles are read again when not loading cache """Test that all the articles are read again when not loading cache
@ -212,7 +203,6 @@ class TestCache(unittest.TestCase):
generator.readers.read_file.call_count, generator.readers.read_file.call_count,
orig_call_count) orig_call_count)
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_page_object_caching(self): def test_page_object_caching(self):
"""Test Page objects caching at the generator level""" """Test Page objects caching at the generator level"""
settings = self._get_cache_enabled_settings() settings = self._get_cache_enabled_settings()
@ -238,7 +228,6 @@ class TestCache(unittest.TestCase):
""" """
self.assertEqual(generator.readers.read_file.call_count, 1) self.assertEqual(generator.readers.read_file.call_count, 1)
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_page_reader_content_caching(self): def test_page_reader_content_caching(self):
"""Test raw page content caching at the reader level""" """Test raw page content caching at the reader level"""
settings = self._get_cache_enabled_settings() settings = self._get_cache_enabled_settings()
@ -262,7 +251,6 @@ class TestCache(unittest.TestCase):
for reader in readers.values(): for reader in readers.values():
self.assertEqual(reader.read.call_count, 0) self.assertEqual(reader.read.call_count, 0)
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_page_ignore_cache(self): def test_page_ignore_cache(self):
"""Test that all the pages are read again when not loading cache """Test that all the pages are read again when not loading cache

View file

@ -1,22 +1,17 @@
import locale import locale
import os import os
import sys
from shutil import copy, rmtree from shutil import copy, rmtree
from tempfile import mkdtemp from tempfile import mkdtemp
from unittest.mock import MagicMock
from pelican.generators import (ArticlesGenerator, Generator, PagesGenerator, from pelican.generators import (ArticlesGenerator, Generator, PagesGenerator,
PelicanTemplateNotFound, StaticGenerator, PelicanTemplateNotFound, StaticGenerator,
TemplatePagesGenerator) TemplatePagesGenerator)
from pelican.tests.support import get_context, get_settings, unittest from pelican.tests.support import (can_symlink, get_context, get_settings,
unittest)
from pelican.writers import Writer from pelican.writers import Writer
try:
from unittest.mock import MagicMock
except ImportError:
try:
from mock import MagicMock
except ImportError:
MagicMock = False
CUR_DIR = os.path.dirname(__file__) CUR_DIR = os.path.dirname(__file__)
CONTENT_DIR = os.path.join(CUR_DIR, 'content') CONTENT_DIR = os.path.join(CUR_DIR, 'content')
@ -198,7 +193,6 @@ class TestArticlesGenerator(unittest.TestCase):
return [[article.title, article.status, article.category.name, return [[article.title, article.status, article.category.name,
article.template] for article in articles] article.template] for article in articles]
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_generate_feeds(self): def test_generate_feeds(self):
settings = get_settings() settings = get_settings()
settings['CACHE_PATH'] = self.temp_cache settings['CACHE_PATH'] = self.temp_cache
@ -218,7 +212,6 @@ class TestArticlesGenerator(unittest.TestCase):
generator.generate_feeds(writer) generator.generate_feeds(writer)
self.assertFalse(writer.write_feed.called) self.assertFalse(writer.write_feed.called)
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_generate_feeds_override_url(self): def test_generate_feeds_override_url(self):
settings = get_settings() settings = get_settings()
settings['CACHE_PATH'] = self.temp_cache settings['CACHE_PATH'] = self.temp_cache
@ -334,7 +327,6 @@ class TestArticlesGenerator(unittest.TestCase):
categories_expected = ['default', 'yeah', 'test', 'zhi-dao-shu'] categories_expected = ['default', 'yeah', 'test', 'zhi-dao-shu']
self.assertEqual(sorted(categories), sorted(categories_expected)) self.assertEqual(sorted(categories), sorted(categories_expected))
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_direct_templates_save_as_url_default(self): def test_direct_templates_save_as_url_default(self):
settings = get_settings() settings = get_settings()
@ -352,7 +344,6 @@ class TestArticlesGenerator(unittest.TestCase):
template_name='archives', template_name='archives',
page_name='archives', url="archives.html") page_name='archives', url="archives.html")
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_direct_templates_save_as_url_modified(self): def test_direct_templates_save_as_url_modified(self):
settings = get_settings() settings = get_settings()
@ -373,7 +364,6 @@ class TestArticlesGenerator(unittest.TestCase):
page_name='archives/index', page_name='archives/index',
url="archives/") url="archives/")
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_direct_templates_save_as_false(self): def test_direct_templates_save_as_false(self):
settings = get_settings() settings = get_settings()
@ -398,7 +388,6 @@ class TestArticlesGenerator(unittest.TestCase):
self.assertIn(custom_template, self.articles) self.assertIn(custom_template, self.articles)
self.assertIn(standard_template, self.articles) self.assertIn(standard_template, self.articles)
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_period_in_timeperiod_archive(self): def test_period_in_timeperiod_archive(self):
""" """
Test that the context of a generated period_archive is passed Test that the context of a generated period_archive is passed
@ -1022,7 +1011,6 @@ class TestStaticGenerator(unittest.TestCase):
with open(self.endfile) as f: with open(self.endfile) as f:
self.assertEqual(f.read(), "staticcontent") self.assertEqual(f.read(), "staticcontent")
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_file_update_required_when_dest_does_not_exist(self): def test_file_update_required_when_dest_does_not_exist(self):
staticfile = MagicMock() staticfile = MagicMock()
staticfile.source_path = self.startfile staticfile.source_path = self.startfile
@ -1032,7 +1020,6 @@ class TestStaticGenerator(unittest.TestCase):
update_required = self.generator._file_update_required(staticfile) update_required = self.generator._file_update_required(staticfile)
self.assertTrue(update_required) self.assertTrue(update_required)
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_dest_and_source_mtimes_are_equal(self): def test_dest_and_source_mtimes_are_equal(self):
staticfile = MagicMock() staticfile = MagicMock()
staticfile.source_path = self.startfile staticfile.source_path = self.startfile
@ -1045,7 +1032,6 @@ class TestStaticGenerator(unittest.TestCase):
isnewer = self.generator._source_is_newer(staticfile) isnewer = self.generator._source_is_newer(staticfile)
self.assertFalse(isnewer) self.assertFalse(isnewer)
@unittest.skipUnless(MagicMock, 'Needs Mock module')
def test_source_is_newer(self): def test_source_is_newer(self):
staticfile = MagicMock() staticfile = MagicMock()
staticfile.source_path = self.startfile staticfile.source_path = self.startfile
@ -1097,6 +1083,7 @@ class TestStaticGenerator(unittest.TestCase):
self.generator.generate_output(None) self.generator.generate_output(None)
self.assertTrue(os.path.samefile(self.startfile, self.endfile)) self.assertTrue(os.path.samefile(self.startfile, self.endfile))
@unittest.skipUnless(can_symlink(), 'No symlink privilege')
def test_can_symlink_when_hardlink_not_possible(self): def test_can_symlink_when_hardlink_not_possible(self):
self.settings['STATIC_CREATE_LINKS'] = True self.settings['STATIC_CREATE_LINKS'] = True
with open(self.startfile, "w") as f: with open(self.startfile, "w") as f:
@ -1104,40 +1091,29 @@ class TestStaticGenerator(unittest.TestCase):
os.mkdir(os.path.join(self.temp_output, "static")) os.mkdir(os.path.join(self.temp_output, "static"))
self.generator.fallback_to_symlinks = True self.generator.fallback_to_symlinks = True
self.generator.generate_context() self.generator.generate_context()
try: self.generator.generate_output(None)
self.generator.generate_output(None)
except OSError as e:
# On Windows, possibly others, due to not holding symbolic link
# privilege
self.skipTest(e)
self.assertTrue(os.path.islink(self.endfile)) self.assertTrue(os.path.islink(self.endfile))
@unittest.skipUnless(can_symlink(), 'No symlink privilege')
def test_existing_symlink_is_considered_up_to_date(self): def test_existing_symlink_is_considered_up_to_date(self):
self.settings['STATIC_CREATE_LINKS'] = True self.settings['STATIC_CREATE_LINKS'] = True
with open(self.startfile, "w") as f: with open(self.startfile, "w") as f:
f.write("staticcontent") f.write("staticcontent")
os.mkdir(os.path.join(self.temp_output, "static")) os.mkdir(os.path.join(self.temp_output, "static"))
try: os.symlink(self.startfile, self.endfile)
os.symlink(self.startfile, self.endfile)
except OSError as e:
# On Windows, possibly others
self.skipTest(e)
staticfile = MagicMock() staticfile = MagicMock()
staticfile.source_path = self.startfile staticfile.source_path = self.startfile
staticfile.save_as = self.endfile staticfile.save_as = self.endfile
requires_update = self.generator._file_update_required(staticfile) requires_update = self.generator._file_update_required(staticfile)
self.assertFalse(requires_update) self.assertFalse(requires_update)
@unittest.skipUnless(can_symlink(), 'No symlink privilege')
def test_invalid_symlink_is_overwritten(self): def test_invalid_symlink_is_overwritten(self):
self.settings['STATIC_CREATE_LINKS'] = True self.settings['STATIC_CREATE_LINKS'] = True
with open(self.startfile, "w") as f: with open(self.startfile, "w") as f:
f.write("staticcontent") f.write("staticcontent")
os.mkdir(os.path.join(self.temp_output, "static")) os.mkdir(os.path.join(self.temp_output, "static"))
try: os.symlink("invalid", self.endfile)
os.symlink("invalid", self.endfile)
except OSError as e:
# On Windows, possibly others
self.skipTest(e)
staticfile = MagicMock() staticfile = MagicMock()
staticfile.source_path = self.startfile staticfile.source_path = self.startfile
staticfile.save_as = self.endfile staticfile.save_as = self.endfile
@ -1147,8 +1123,18 @@ class TestStaticGenerator(unittest.TestCase):
self.generator.generate_context() self.generator.generate_context()
self.generator.generate_output(None) self.generator.generate_output(None)
self.assertTrue(os.path.islink(self.endfile)) self.assertTrue(os.path.islink(self.endfile))
self.assertEqual(os.path.realpath(self.endfile),
os.path.realpath(self.startfile)) # os.path.realpath is broken on Windows before python3.8 for symlinks.
# This is a (ugly) workaround.
# see: https://bugs.python.org/issue9949
if os.name == 'nt' and sys.version_info < (3, 8):
def get_real_path(path):
return os.readlink(path) if os.path.islink(path) else path
else:
get_real_path = os.path.realpath
self.assertEqual(get_real_path(self.endfile),
get_real_path(self.startfile))
def test_delete_existing_file_before_mkdir(self): def test_delete_existing_file_before_mkdir(self):
with open(self.startfile, "w") as f: with open(self.startfile, "w") as f:

View file

@ -1,6 +1,7 @@
import locale import locale
import os import os
import re import re
from posixpath import join as posix_join
from pelican.settings import DEFAULT_CONFIG from pelican.settings import DEFAULT_CONFIG
from pelican.tests.support import (mute, skipIfNoExecutable, temporary_folder, from pelican.tests.support import (mute, skipIfNoExecutable, temporary_folder,
@ -448,5 +449,5 @@ class TestWordpressXMLAttachements(unittest.TestCase):
self.assertEqual(1, len(locations)) self.assertEqual(1, len(locations))
directory = locations[0] directory = locations[0]
self.assertTrue( self.assertTrue(
directory.endswith(os.path.join('content', 'article.rst')), directory.endswith(posix_join('content', 'article.rst')),
directory) directory)

View file

@ -1,16 +1,10 @@
import os import os
from unittest.mock import patch
from pelican import readers from pelican import readers
from pelican.tests.support import get_settings, unittest from pelican.tests.support import get_settings, unittest
from pelican.utils import SafeDatetime from pelican.utils import SafeDatetime
try:
from unittest.mock import patch
except ImportError:
try:
from mock import patch
except ImportError:
patch = False
CUR_DIR = os.path.dirname(__file__) CUR_DIR = os.path.dirname(__file__)
CONTENT_PATH = os.path.join(CUR_DIR, 'content') CONTENT_PATH = os.path.join(CUR_DIR, 'content')
@ -125,7 +119,6 @@ class DefaultReaderTest(ReaderTest):
self.assertDictHasSubset(page.metadata, expected) self.assertDictHasSubset(page.metadata, expected)
@unittest.skipUnless(patch, 'Needs Mock module')
def test_find_empty_alt(self): def test_find_empty_alt(self):
with patch('pelican.readers.logger') as log_mock: with patch('pelican.readers.logger') as log_mock:
content = ['<img alt="" src="test-image.png" width="300px" />', content = ['<img alt="" src="test-image.png" width="300px" />',

View file

@ -1,15 +1,8 @@
from unittest.mock import Mock
from pelican.tests.support import unittest from pelican.tests.support import unittest
try:
from unittest.mock import Mock
except ImportError:
try:
from mock import Mock
except ImportError:
Mock = False
@unittest.skipUnless(Mock, 'Needs Mock module')
class Test_abbr_role(unittest.TestCase): class Test_abbr_role(unittest.TestCase):
def call_it(self, text): def call_it(self, text):
from pelican.rstdirectives import abbr_role from pelican.rstdirectives import abbr_role

View file

@ -25,11 +25,10 @@ class TestServer(unittest.TestCase):
os.chdir(self.temp_output) os.chdir(self.temp_output)
def tearDown(self): def tearDown(self):
rmtree(self.temp_output)
os.chdir(self.old_cwd) os.chdir(self.old_cwd)
rmtree(self.temp_output)
def test_get_path_that_exists(self): def test_get_path_that_exists(self):
handler = ComplexHTTPRequestHandler(MockRequest(), ('0.0.0.0', 8888), handler = ComplexHTTPRequestHandler(MockRequest(), ('0.0.0.0', 8888),
self.server) self.server)
handler.base_path = self.temp_output handler.base_path = self.temp_output

View file

@ -136,6 +136,8 @@ class TestSettingsConfiguration(unittest.TestCase):
settings['ARTICLE_DIR'] settings['ARTICLE_DIR']
settings['PAGE_DIR'] settings['PAGE_DIR']
# locale.getdefaultlocale() is broken on Windows
# See: https://bugs.python.org/issue37945
@unittest.skipIf(platform == 'win32', "Doesn't work on Windows") @unittest.skipIf(platform == 'win32', "Doesn't work on Windows")
def test_default_encoding(self): def test_default_encoding(self):
# Test that the default locale is set if not specified in settings # Test that the default locale is set if not specified in settings

View file

@ -757,35 +757,32 @@ class TestDateFormatter(unittest.TestCase):
class TestSanitisedJoin(unittest.TestCase): class TestSanitisedJoin(unittest.TestCase):
@unittest.skipIf(platform == 'win32',
"Different filesystem root on Windows")
def test_detect_parent_breakout(self): def test_detect_parent_breakout(self):
with self.assertRaisesRegex( with self.assertRaisesRegex(
RuntimeError, RuntimeError,
"Attempted to break out of output directory to /foo/test"): "Attempted to break out of output directory to "
"(.*?:)?/foo/test"): # (.*?:)? accounts for Windows root
utils.sanitised_join( utils.sanitised_join(
"/foo/bar", "/foo/bar",
"../test" "../test"
) )
@unittest.skipIf(platform == 'win32',
"Different filesystem root on Windows")
def test_detect_root_breakout(self): def test_detect_root_breakout(self):
with self.assertRaisesRegex( with self.assertRaisesRegex(
RuntimeError, RuntimeError,
"Attempted to break out of output directory to /test"): "Attempted to break out of output directory to "
"(.*?:)?/test"): # (.*?:)? accounts for Windows root
utils.sanitised_join( utils.sanitised_join(
"/foo/bar", "/foo/bar",
"/test" "/test"
) )
@unittest.skipIf(platform == 'win32',
"Different filesystem root on Windows")
def test_pass_deep_subpaths(self): def test_pass_deep_subpaths(self):
self.assertEqual( self.assertEqual(
utils.sanitised_join( utils.sanitised_join(
"/foo/bar", "/foo/bar",
"test" "test"
), ),
os.path.join("/foo/bar", "test") utils.posixize_path(
os.path.abspath(os.path.join("/foo/bar", "test")))
) )

View file

@ -728,8 +728,9 @@ def download_attachments(output_path, urls):
# Generate percent-encoded URL # Generate percent-encoded URL
scheme, netloc, path, query, fragment = urlsplit(url) scheme, netloc, path, query, fragment = urlsplit(url)
path = quote(path) if scheme != 'file':
url = urlunsplit((scheme, netloc, path, query, fragment)) path = quote(path)
url = urlunsplit((scheme, netloc, path, query, fragment))
if not os.path.exists(full_path): if not os.path.exists(full_path):
os.makedirs(full_path) os.makedirs(full_path)

View file

@ -27,8 +27,10 @@ logger = logging.getLogger(__name__)
def sanitised_join(base_directory, *parts): def sanitised_join(base_directory, *parts):
joined = os.path.abspath(os.path.join(base_directory, *parts)) joined = posixize_path(
if not joined.startswith(os.path.abspath(base_directory)): os.path.abspath(os.path.join(base_directory, *parts)))
base = posixize_path(os.path.abspath(base_directory))
if not joined.startswith(base):
raise RuntimeError( raise RuntimeError(
"Attempted to break out of output directory to {}".format( "Attempted to break out of output directory to {}".format(
joined joined
@ -391,10 +393,9 @@ def get_relative_path(path):
def path_to_url(path): def path_to_url(path):
"""Return the URL corresponding to a given path.""" """Return the URL corresponding to a given path."""
if os.sep == '/': if path is not None:
return path path = posixize_path(path)
else: return path
return '/'.join(split_all(path))
def posixize_path(rel_path): def posixize_path(rel_path):

View file

@ -1,5 +1,6 @@
import logging import logging
import os import os
from posixpath import join as posix_join
from urllib.parse import urljoin from urllib.parse import urljoin
from feedgenerator import Atom1Feed, Rss201rev2Feed, get_tag_uri from feedgenerator import Atom1Feed, Rss201rev2Feed, get_tag_uri
@ -25,7 +26,7 @@ class Writer:
# See Content._link_replacer for details # See Content._link_replacer for details
if self.settings['RELATIVE_URLS']: if self.settings['RELATIVE_URLS']:
self.urljoiner = os.path.join self.urljoiner = posix_join
else: else:
self.urljoiner = lambda base, url: urljoin( self.urljoiner = lambda base, url: urljoin(
base if base.endswith('/') else base + '/', url) base if base.endswith('/') else base + '/', url)

View file

@ -1,3 +1,3 @@
sphinx==1.4.9 sphinx
sphinx_rtd_theme sphinx_rtd_theme
livereload livereload

View file

@ -1,6 +1,5 @@
# Tests # Tests
Pygments==2.6.1 Pygments==2.6.1
mock
pytest==5.3.5 pytest==5.3.5
pytest-cov pytest-cov
pytest-xdist pytest-xdist

View file

@ -24,7 +24,7 @@ PRECOMMIT = (
@task @task
def docbuild(c): def docbuild(c):
"""Build documentation""" """Build documentation"""
c.run(f"{VENV_BIN}/sphinx-build docs docs/_build") c.run(f"{VENV_BIN}/sphinx-build -W docs docs/_build")
@task(docbuild) @task(docbuild)

13
tox.ini
View file

@ -1,11 +1,12 @@
[tox] [tox]
envlist = py{35,36,37},docs,flake8 envlist = py{3.5,3.6,3.7,3.8},docs,flake8
[testenv] [testenv]
basepython = basepython =
py35: python3.5 py3.5: python3.5
py36: python3.6 py3.6: python3.6
py37: python3.7 py3.7: python3.7
py3.8: python3.8
passenv = * passenv = *
usedevelop=True usedevelop=True
deps = deps =
@ -13,7 +14,7 @@ deps =
commands = commands =
{envpython} --version {envpython} --version
pytest -sv --cov=pelican pelican pytest -s --cov=pelican pelican
[testenv:docs] [testenv:docs]
basepython = python3.6 basepython = python3.6
@ -27,7 +28,7 @@ commands =
filterwarnings = filterwarnings =
default::DeprecationWarning default::DeprecationWarning
error:.*:Warning:pelican error:.*:Warning:pelican
addopts = -n 2 addopts = -n 2 -r a
[flake8] [flake8]
application-import-names = pelican application-import-names = pelican