From 6cefbfb20dcd50beb6aa0e6d1aa61a2e8091b048 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sat, 27 May 2017 16:37:10 +0100 Subject: [PATCH 01/35] Start adding basic tests --- circle.yml | 1 + dev-requirements.txt | 1 + setup.py | 2 +- tests/__init__.py | 5 +++++ tests/test_things.py | 6 ++++++ 5 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_things.py diff --git a/circle.yml b/circle.yml index 9ee76d2..99a6f04 100644 --- a/circle.yml +++ b/circle.yml @@ -24,3 +24,4 @@ test: - bandit -r md_pdf/ - mdp --update-csl - cd test-files/ && mdp -vvv + - nose2 diff --git a/dev-requirements.txt b/dev-requirements.txt index 6364219..bae884c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,4 @@ bandit==1.4.0 flake8==3.3.0 +nose2==0.6.5 safety==0.5.1 diff --git a/setup.py b/setup.py index f0d671a..8e4f270 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup( "word-count==0.1.0" ], setup_requires=['setuptools_scm'], - packages=find_packages(), + packages=find_packages(exclude=['tests']), include_package_data=True, zip_safe=False, entry_points=""" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..056c322 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,5 @@ +from unittest import TestCase + + +class BaseTestCase(TestCase): + pass diff --git a/tests/test_things.py b/tests/test_things.py new file mode 100644 index 0000000..a6617b9 --- /dev/null +++ b/tests/test_things.py @@ -0,0 +1,6 @@ +from tests import BaseTestCase + + +class ExampleTestCase(BaseTestCase): + def test_thing(self): + self.assertEqual(1, 1) From 8ce8a6f9407477cf023f2857ec1fa51ad07659ac Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sat, 27 May 2017 16:43:23 +0100 Subject: [PATCH 02/35] Delete templates after test --- tests/__init__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index 056c322..d04989a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,20 @@ from unittest import TestCase +import os +from md_pdf.consts import TEMPLATES_DIR class BaseTestCase(TestCase): - pass + def deleteTemplates(self): + for template in [ + 'header.html', + 'footer.html', + 'cover.html', + 'toc-xml', + ]: + try: + os.remove(os.path.join(TEMPLATES_DIR, template)) + except OSError: + pass + + def tearDown(self): + self.deleteTemplates() From 9db3b3133d9f17f44c024b439ab9f882a0b834dd Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sat, 27 May 2017 20:04:36 +0100 Subject: [PATCH 03/35] Add test runner script --- circle.yml | 2 +- dev-requirements.txt | 1 - scripts/run-tests.sh | 4 ++++ 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100755 scripts/run-tests.sh diff --git a/circle.yml b/circle.yml index 99a6f04..9b9f7b6 100644 --- a/circle.yml +++ b/circle.yml @@ -24,4 +24,4 @@ test: - bandit -r md_pdf/ - mdp --update-csl - cd test-files/ && mdp -vvv - - nose2 + - scripts/run-tests.sh diff --git a/dev-requirements.txt b/dev-requirements.txt index bae884c..6364219 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,3 @@ bandit==1.4.0 flake8==3.3.0 -nose2==0.6.5 safety==0.5.1 diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh new file mode 100755 index 0000000..8eacbe1 --- /dev/null +++ b/scripts/run-tests.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + + +python3 -m unittest -v From a46f304c555fddd59edbb0ffc10b7e3a1442ddba Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sat, 27 May 2017 20:32:50 +0100 Subject: [PATCH 04/35] Test config reader --- md_pdf/config/read.py | 4 ++-- tests/__init__.py | 4 ++-- tests/test_config.py | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 tests/test_config.py diff --git a/md_pdf/config/read.py b/md_pdf/config/read.py index 89633a3..046064b 100644 --- a/md_pdf/config/read.py +++ b/md_pdf/config/read.py @@ -4,9 +4,9 @@ from md_pdf.consts import CONFIG_FILE from md_pdf.exceptions import ConfigValidationException -def load_config(): +def load_config(location=CONFIG_FILE): try: - with open(os.path.join(CONFIG_FILE)) as f: + with open(os.path.join(location)) as f: return yaml.safe_load(f) except FileNotFoundError: raise ConfigValidationException("Can't find config file at {}".format(CONFIG_FILE)) diff --git a/tests/__init__.py b/tests/__init__.py index d04989a..ecff5d9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,9 +1,9 @@ -from unittest import TestCase +import unittest import os from md_pdf.consts import TEMPLATES_DIR -class BaseTestCase(TestCase): +class BaseTestCase(unittest.TestCase): def deleteTemplates(self): for template in [ 'header.html', diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..cc66a4d --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,15 @@ +from tests import BaseTestCase +from md_pdf.config import read, validate +from md_pdf.exceptions import ConfigValidationException +import os + + +class ReadConfigTestCase(BaseTestCase): + def test_reads_config(self): + config = read.load_config(os.path.abspath('test-files/mdp.yml')) + self.assertIsInstance(config, dict) + + def test_throws_at_missing_config(self): + with self.assertRaises(ConfigValidationException): + read.load_config(os.path.abspath('non-existant')) + From a4d766034a4b2772b8e76078f1d28e9331117b71 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sat, 27 May 2017 22:06:55 +0100 Subject: [PATCH 05/35] Add tests for config validator --- md_pdf/config/validate.py | 15 ++- scripts/run-tests.sh | 2 +- tests/test_config.py | 232 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 242 insertions(+), 7 deletions(-) diff --git a/md_pdf/config/validate.py b/md_pdf/config/validate.py index e90f5ba..cc97a4b 100644 --- a/md_pdf/config/validate.py +++ b/md_pdf/config/validate.py @@ -36,6 +36,10 @@ def test_input(config): abs_input = os.path.abspath(config['input']) if len(glob.glob(abs_input)) == 0: raise ConfigValidationException("No files found at {}".format(abs_input)) + for file in glob.iglob(abs_input): + if not os.path.isfile(file): + raise ConfigValidationException("Input must be a glob of files") + def validate_bibliography(config): @@ -49,9 +53,8 @@ def validate_bibliography(config): abs_bibliography = os.path.abspath(config['bibliography']['references']) if not os.path.isfile(abs_bibliography): raise ConfigValidationException("Invalid bibliography path: '{}'".format(abs_bibliography)) - if 'csl' in config['bibliography']: - if not os.path.isfile(os.path.join(CSL_DIR, "{}.csl".format(config['bibliography']['csl']))): - raise ConfigValidationException("Could not find CSL '{}'".format(config.bibliography.csl)) + if not os.path.isfile(os.path.join(CSL_DIR, "{}.csl".format(config['bibliography']['csl']))): + raise ConfigValidationException("Could not find CSL '{}'".format(config['bibliography']['csl'])) def validate_context(config): @@ -63,11 +66,11 @@ def validate_context(config): non_str_keys = [key for key in config['context'].keys() if type(key) != str] if non_str_keys: - raise ConfigValidationException("Context keys must be strings. Non-strings: {}".format(", ".join(non_str_keys))) + raise ConfigValidationException("Context keys must be strings. Non-strings: {}".format(non_str_keys)) invalid_values = [value for value in config['context'].values() if type(value) in [list, dict]] if invalid_values: - raise ConfigValidationException("Context keys must be plain. Invalid values: {}".format(", ".join(invalid_values))) + raise ConfigValidationException("Context keys must be plain. Invalid values: {}".format(invalid_values)) def validate_toc(config): @@ -92,7 +95,7 @@ def validate_submission_date(config): try: parser.parse(config['submission_date']) except ValueError: - raise ConfigValidationException("Invalid Submission Date format") + raise ConfigValidationException("Invalid Submission Date format {}".format(config['submission_date'])) def validate_config(config): diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 8eacbe1..dd95d21 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -python3 -m unittest -v +python3 -m unittest -v $@ diff --git a/tests/test_config.py b/tests/test_config.py index cc66a4d..b6e34d2 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,7 +1,11 @@ from tests import BaseTestCase from md_pdf.config import read, validate from md_pdf.exceptions import ConfigValidationException +from md_pdf.consts import CSL_DIR +from md_pdf.utils import remove_dir import os +import datetime +from unittest import skipIf class ReadConfigTestCase(BaseTestCase): @@ -13,3 +17,231 @@ class ReadConfigTestCase(BaseTestCase): with self.assertRaises(ConfigValidationException): read.load_config(os.path.abspath('non-existant')) + +class ConfigValidatorBaseTestCase(BaseTestCase): + def setUp(self): + self.BASE_VALID_CONFIG = { + 'title': 'test title', + 'input': 'test-files/*.md', + 'output_formats': [ + 'html', 'pdf' + ], + 'output_dir': 'out/', + + } + validate.validate_config(self.BASE_VALID_CONFIG) + + +class ValidateSubmissionDateTestCase(ConfigValidatorBaseTestCase): + def test_transparent_to_datetime(self): + self.BASE_VALID_CONFIG['submission_date'] = datetime.datetime.now() + validate.validate_config(self.BASE_VALID_CONFIG) + + def test_transparent_to_date(self): + self.BASE_VALID_CONFIG['submission_date'] = datetime.datetime.now().date() + validate.validate_config(self.BASE_VALID_CONFIG) + + def test_transparent_to_time(self): + self.BASE_VALID_CONFIG['submission_date'] = datetime.datetime.now().time() + validate.validate_config(self.BASE_VALID_CONFIG) + + def test_valid_date_format(self): + for date in [ + '2017-01-01', + '01-01-2017', + '1st jan 2017', + '1 january 2017' + ]: + self.BASE_VALID_CONFIG['submission_date'] = date + validate.validate_config(self.BASE_VALID_CONFIG) + + def test_invalid_date_format(self): + for date in [ + 'nothing', + '31-02-2017', + '01-2017-01', + '1st smarch 2017' + ]: + self.BASE_VALID_CONFIG['submission_date'] = date + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + + +class ValidateWordcountTestCase(ConfigValidatorBaseTestCase): + def test_boolean_values_only(self): + self.BASE_VALID_CONFIG['show_word_count'] = True + validate.validate_config(self.BASE_VALID_CONFIG) + self.BASE_VALID_CONFIG['show_word_count'] = False + validate.validate_config(self.BASE_VALID_CONFIG) + + def test_invalid_values(self): + for value in [ + 'True', + 'False', + 0, + 1 + ]: + self.BASE_VALID_CONFIG['show_word_count'] = value + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + + + +class ValidateTOCTestCase(ConfigValidatorBaseTestCase): + def test_boolean_values_only(self): + self.BASE_VALID_CONFIG['toc'] = True + validate.validate_config(self.BASE_VALID_CONFIG) + self.BASE_VALID_CONFIG['toc'] = False + validate.validate_config(self.BASE_VALID_CONFIG) + + def test_invalid_values(self): + for value in [ + 'True', + 'False', + 0, + 1 + ]: + self.BASE_VALID_CONFIG['toc'] = value + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + + +class ValidateContextTestCase(ConfigValidatorBaseTestCase): + def test_should_be_dict(self): + for value in [ + [], + 'dict', + 1 + ]: + self.BASE_VALID_CONFIG['context'] = value + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + self.BASE_VALID_CONFIG['context'] = {} + validate.validate_config(self.BASE_VALID_CONFIG) + + def test_non_string_keys(self): + self.BASE_VALID_CONFIG['context'] = { + 1: 'test' + } + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + + def test_string_keys(self): + self.BASE_VALID_CONFIG['context'] = { + '1': 'test' + } + validate.validate_config(self.BASE_VALID_CONFIG) + + def test_valid_values(self): + for value in [ + 'test', + 1 + ]: + self.BASE_VALID_CONFIG['context'] = { + 'test': value + } + validate.validate_config(self.BASE_VALID_CONFIG) + + def test_invalid_values(self): + for value in [ + [], + {} + ]: + self.BASE_VALID_CONFIG['context'] = { + 'test': value + } + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + + +class ValidateBibliographyTestCase(ConfigValidatorBaseTestCase): + def test_contains_all_keys(self): + self.BASE_VALID_CONFIG['bibliography'] = {} + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + self.BASE_VALID_CONFIG['bibliography'] = { + 'references': 'test' + } + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + self.BASE_VALID_CONFIG['bibliography'] = { + 'csl': 'test' + } + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + + def test_valid_references(self): + self.BASE_VALID_CONFIG['bibliography'] = { + 'references': 'non-existant', + 'csl': 'chicago-author-date' + } + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + self.BASE_VALID_CONFIG['bibliography']['references'] = 'test-files/bib.yaml' + validate.validate_config(self.BASE_VALID_CONFIG) + + @skipIf(not os.path.isdir(CSL_DIR), 'Missing CSL Files') + def test_valid_csl(self): + self.BASE_VALID_CONFIG['bibliography'] = { + 'references': 'test-files/bib.yaml', + 'csl': 'nothing' + } + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + self.BASE_VALID_CONFIG['bibliography']['csl'] = 'chicago-author-date' + validate.validate_config(self.BASE_VALID_CONFIG) + + +class ValidateInputTestCase(ConfigValidatorBaseTestCase): + def test_no_matches(self): + self.BASE_VALID_CONFIG['input'] = 'test-files/*.mp4' + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + + def test_invalid_glob(self): + self.BASE_VALID_CONFIG['input'] = 'test-files/' + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + + +class ValidateOutputTestCase(ConfigValidatorBaseTestCase): + def tearDown(self): + super().tearDown() + remove_dir('test-files/test') + + def test_creates_output_dir(self): + self.assertFalse(os.path.isdir('test-files/test')) + self.BASE_VALID_CONFIG['output_dir'] = 'test-files/test' + validate.validate_config(self.BASE_VALID_CONFIG) + self.assertTrue(os.path.isdir('test-files/test')) + + def test_valid_output_formats(self): + for format in [ + 'html', + 'pdf' + ]: + self.BASE_VALID_CONFIG['output_formats'] = [format] + validate.validate_config(self.BASE_VALID_CONFIG) + + def test_invalid_output_formats(self): + for format in [ + 'text', + 'foo' + ]: + self.BASE_VALID_CONFIG['output_formats'] = [format] + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + + def test_part_invalid_format(self): + self.BASE_VALID_CONFIG['output_formats'] = ['html', 'foo'] + with self.assertRaises(ConfigValidationException): + validate.validate_config(self.BASE_VALID_CONFIG) + + +class ValidateRequiredKeysTestCase(ConfigValidatorBaseTestCase): + def test_required_keys(self): + for key in validate.REQUIRED_KEYS: + base_config = self.BASE_VALID_CONFIG.copy() + del base_config[key] + with self.assertRaises(ConfigValidationException): + validate.validate_config(base_config) From a7117a8f5cdf42734742101ceb9fb6941e278d8d Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sat, 27 May 2017 22:10:06 +0100 Subject: [PATCH 06/35] Override tests --- circle.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 9b9f7b6..95dc18c 100644 --- a/circle.yml +++ b/circle.yml @@ -18,7 +18,8 @@ dependencies: test: - post: + override: + - npm test - flake8 md_pdf/ --ignore=E128,E501 - safety check - bandit -r md_pdf/ From dbe4e8ae7c13d911cb306761095a5cec2f2d34c3 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sat, 27 May 2017 22:27:47 +0100 Subject: [PATCH 07/35] Verbose download of CSL --- circle.yml | 2 +- md_pdf/csl.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/circle.yml b/circle.yml index 95dc18c..259dbb0 100644 --- a/circle.yml +++ b/circle.yml @@ -23,6 +23,6 @@ test: - flake8 md_pdf/ --ignore=E128,E501 - safety check - bandit -r md_pdf/ - - mdp --update-csl + - mdp --update-csl -vvv - cd test-files/ && mdp -vvv - scripts/run-tests.sh diff --git a/md_pdf/csl.py b/md_pdf/csl.py index 1d45830..4368341 100644 --- a/md_pdf/csl.py +++ b/md_pdf/csl.py @@ -39,7 +39,7 @@ def download_csl(): with open(download_location, 'rb') as downloaded_file: with zipfile.ZipFile(downloaded_file) as csl_zip: member_list = csl_zip.namelist() - logger.info("Extracting CSL...") + logger.info("Extracting CSL to {}".format(ASSET_DIR)) bar.start(max_value=len(member_list)) for i, member in enumerate(member_list): From 0c9b1324b080408b96f39dc720470c8d9b257201 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sat, 27 May 2017 22:34:01 +0100 Subject: [PATCH 08/35] Better error message when missing CSL --- md_pdf/config/validate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/md_pdf/config/validate.py b/md_pdf/config/validate.py index cc97a4b..edc3569 100644 --- a/md_pdf/config/validate.py +++ b/md_pdf/config/validate.py @@ -54,7 +54,7 @@ def validate_bibliography(config): if not os.path.isfile(abs_bibliography): raise ConfigValidationException("Invalid bibliography path: '{}'".format(abs_bibliography)) if not os.path.isfile(os.path.join(CSL_DIR, "{}.csl".format(config['bibliography']['csl']))): - raise ConfigValidationException("Could not find CSL '{}'".format(config['bibliography']['csl'])) + raise ConfigValidationException("Could not find CSL '{}' in {}".format(config['bibliography']['csl'], CSL_DIR)) def validate_context(config): From 2f11a3c93e31444bd9c0a806831eb3f8db12b0ba Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sun, 28 May 2017 11:40:17 +0100 Subject: [PATCH 09/35] Fix variable name --- md_pdf/csl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/md_pdf/csl.py b/md_pdf/csl.py index a7b3836..d922855 100644 --- a/md_pdf/csl.py +++ b/md_pdf/csl.py @@ -39,7 +39,7 @@ def download_csl(): with open(download_location, 'rb') as downloaded_file: with zipfile.ZipFile(downloaded_file) as csl_zip: member_list = csl_zip.namelist() - logger.info("Extracting CSL to {}".format(ASSET_DIR)) + logger.info("Extracting CSL to {}".format(ASSETS_DIR)) bar.start(max_value=len(member_list)) for i, member in enumerate(member_list): From 16b60022054f7f74bed2862fd8b9f79af5b66be5 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sun, 28 May 2017 11:53:58 +0100 Subject: [PATCH 10/35] Fix lint --- md_pdf/config/validate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/md_pdf/config/validate.py b/md_pdf/config/validate.py index edc3569..299bb1d 100644 --- a/md_pdf/config/validate.py +++ b/md_pdf/config/validate.py @@ -41,7 +41,6 @@ def test_input(config): raise ConfigValidationException("Input must be a glob of files") - def validate_bibliography(config): if 'bibliography' not in config: return From c6100c04c6e4cf2a79908308feb9826d51a1a5cf Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sun, 28 May 2017 12:33:34 +0100 Subject: [PATCH 11/35] Test html output --- md_pdf/build/__init__.py | 2 +- md_pdf/build/pandoc.py | 2 +- tests/test_pandoc.py | 26 ++++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/test_pandoc.py diff --git a/md_pdf/build/__init__.py b/md_pdf/build/__init__.py index 27075f1..177216b 100644 --- a/md_pdf/build/__init__.py +++ b/md_pdf/build/__init__.py @@ -15,7 +15,7 @@ def build(config): logger.debug("Starting Build...") start_time = time.time() data = read_files(os.path.abspath(config['input'])) - doc = build_document(data, config.get('bibliography'), config.get('context')) + doc = build_document(data, config.get('bibliography')) parsed_template = parse_template(doc, config) if 'html' in config['output_formats']: output_html(parsed_template, os.path.abspath(config['output_dir'])) diff --git a/md_pdf/build/pandoc.py b/md_pdf/build/pandoc.py index 7bd86a2..1c7ab2d 100644 --- a/md_pdf/build/pandoc.py +++ b/md_pdf/build/pandoc.py @@ -12,7 +12,7 @@ def output_html(html, out_dir): f.write(html) -def build_document(files_content, bibliography, context): +def build_document(files_content, bibliography): args = [ '-s', ] diff --git a/tests/test_pandoc.py b/tests/test_pandoc.py new file mode 100644 index 0000000..e29e281 --- /dev/null +++ b/tests/test_pandoc.py @@ -0,0 +1,26 @@ +from tests import BaseTestCase +from md_pdf.build.pandoc import output_html, build_document +from md_pdf.utils import remove_dir +import os + +class OutputHTMLTestCase(BaseTestCase): + output_dir = 'test-output' + + def setUp(self): + super().setUp() + os.makedirs(self.output_dir) + + def tearDown(self): + super().tearDown() + remove_dir(self.output_dir) + + def test_outputs_file(self): + self.assertFalse(os.path.isfile(os.path.join(self.output_dir, 'output.html'))) + output_html('test', self.output_dir) + self.assertTrue(os.path.isfile(os.path.join(self.output_dir, 'output.html'))) + + def test_outputs_correct_data(self): + output_html('test', self.output_dir) + with open(os.path.join(self.output_dir, 'output.html')) as f: + self.assertEqual(f.read(), 'test') + From 112d22b6ed44178ec4fb648b1132d4d35512a3d5 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sun, 28 May 2017 13:21:22 +0100 Subject: [PATCH 12/35] Add basic pandoc build tests --- tests/test_pandoc.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_pandoc.py b/tests/test_pandoc.py index e29e281..531e6a2 100644 --- a/tests/test_pandoc.py +++ b/tests/test_pandoc.py @@ -3,6 +3,7 @@ from md_pdf.build.pandoc import output_html, build_document from md_pdf.utils import remove_dir import os + class OutputHTMLTestCase(BaseTestCase): output_dir = 'test-output' @@ -24,3 +25,24 @@ class OutputHTMLTestCase(BaseTestCase): with open(os.path.join(self.output_dir, 'output.html')) as f: self.assertEqual(f.read(), 'test') + +class BuildDocumentTestCase(BaseTestCase): + def test_parses_markdown(self): + doc = build_document('# test', None) + self.assertIn('

test

', doc) + + def test_bibliography(self): + bibliography = { + 'references': 'test-files/bib.yaml', + 'csl': 'chicago-author-date' + } + with open('test-files/2-pandoc.md') as f: + test_content = f.read() + doc = build_document(test_content, bibliography) + self.assertIn( + 'Doe (2005, 2006, 30; see also Doe and Roe 2007) says blah.', + doc + ) + self.assertIn('Doe, John. 2005.', doc) + self.assertIn('
', doc) + From b7669c53ffc793f09045213e893f4d69eee7e4e0 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sun, 28 May 2017 14:06:44 +0100 Subject: [PATCH 13/35] More tests for pandoc --- tests/test_pandoc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_pandoc.py b/tests/test_pandoc.py index 531e6a2..e25d85f 100644 --- a/tests/test_pandoc.py +++ b/tests/test_pandoc.py @@ -31,6 +31,10 @@ class BuildDocumentTestCase(BaseTestCase): doc = build_document('# test', None) self.assertIn('

test

', doc) + def converts_nothing_if_plain(self): + doc = build_document('test', None) + self.assertIn('test', doc) + def test_bibliography(self): bibliography = { 'references': 'test-files/bib.yaml', From 1b34aa2b0d46b5fd8d14cbe05f5b67f12371380c Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sun, 28 May 2017 14:11:35 +0100 Subject: [PATCH 14/35] better file removal --- tests/__init__.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index ecff5d9..397c12c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,20 +1,25 @@ import unittest import os -from md_pdf.consts import TEMPLATES_DIR +from md_pdf.consts import TEMPLATES_DIR, STATIC_DIR class BaseTestCase(unittest.TestCase): + def removeFile(self, file): + try: + os.remove(file) + except OSError: + pass + def deleteTemplates(self): for template in [ 'header.html', 'footer.html', 'cover.html', - 'toc-xml', + 'toc.xsl', ]: - try: - os.remove(os.path.join(TEMPLATES_DIR, template)) - except OSError: - pass + self.removeFile(os.path.join(TEMPLATES_DIR, template)) def tearDown(self): self.deleteTemplates() + self.removeFile(os.path.join(STATIC_DIR, 'style.css')) + From cf210cd908a2858fe1276e09719c02ae6b53f596 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sun, 28 May 2017 14:12:22 +0100 Subject: [PATCH 15/35] Remove basic tests --- tests/test_things.py | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 tests/test_things.py diff --git a/tests/test_things.py b/tests/test_things.py deleted file mode 100644 index a6617b9..0000000 --- a/tests/test_things.py +++ /dev/null @@ -1,6 +0,0 @@ -from tests import BaseTestCase - - -class ExampleTestCase(BaseTestCase): - def test_thing(self): - self.assertEqual(1, 1) From e9cc191d9b295b424068a8c051e4a84cbe8f31f9 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sun, 28 May 2017 15:08:39 +0100 Subject: [PATCH 16/35] test consts --- dev-requirements.txt | 2 + tests/test_consts.py | 105 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 tests/test_consts.py diff --git a/dev-requirements.txt b/dev-requirements.txt index 6364219..6b74877 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,5 @@ bandit==1.4.0 flake8==3.3.0 +freezegun==0.3.9 +requests==2.16.5 safety==0.5.1 diff --git a/tests/test_consts.py b/tests/test_consts.py new file mode 100644 index 0000000..7bbbe9b --- /dev/null +++ b/tests/test_consts.py @@ -0,0 +1,105 @@ +from tests import BaseTestCase +from md_pdf import consts +from unittest import skipIf +import os +import requests +import datetime +from freezegun import freeze_time + + +class ConstsTestCase(BaseTestCase): + def setUp(self): + super().setUp() + self.this_dir = os.path.dirname(__file__) + self.project_root = os.path.normpath(os.path.join(self.this_dir, '..')) + + def test_project_dir(self): + self.assertEqual( + consts.PROJECT_DIR, + os.path.normpath(os.path.join(self.this_dir, '..', 'md_pdf')) + ) + self.assertIn(consts.WORKING_DIR, consts.PROJECT_DIR) + + def test_working_dir(self): + self.assertEqual(consts.WORKING_DIR, self.project_root) + + @skipIf('APPDATA' not in os.environ, 'not on windows') + def test_windows_asset_dir(self): + self.assertIn(os.environ['APPDATA'], consts.ASSETS_DIR) + + @skipIf('HOME' not in os.environ, 'not on windows') + def test_asset_dir(self): + self.assertEqual(consts.ASSETS_DIR, os.path.expanduser('~/.mdp')) + + def test_csl_dir(self): + self.assertIn(consts.ASSETS_DIR, consts.CSL_DIR) + self.assertIn('csl', consts.CSL_DIR) + + def test_templates_dir(self): + self.assertIn(consts.ASSETS_DIR, consts.TEMPLATES_DIR) + self.assertIn('templates', consts.TEMPLATES_DIR) + + def test_static_dir(self): + self.assertIn(consts.ASSETS_DIR, consts.STATIC_DIR) + self.assertIn('static', consts.STATIC_DIR) + + def test_internal_asset_dir(self): + self.assertIn(consts.PROJECT_DIR, consts.INTERNAL_ASSETS_DIR) + self.assertIn('assets', consts.INTERNAL_ASSETS_DIR) + + def test_internal_static_dir(self): + self.assertIn(consts.PROJECT_DIR, consts.INTERNAL_STATIC_DIR) + self.assertIn('static', consts.INTERNAL_STATIC_DIR) + + def test_internal_templates_dir(self): + self.assertIn(consts.PROJECT_DIR, consts.INTERNAL_TEMPLATES_DIR) + self.assertIn('templates', consts.INTERNAL_TEMPLATES_DIR) + + def test_config_file(self): + self.assertIn(consts.WORKING_DIR, consts.CONFIG_FILE) + self.assertIn('mdp.yml', consts.CONFIG_FILE) + + def test_csl_download_link(self): + self.assertIn('https://github.com', consts.CSL_DOWNLOAD_LINK) + self.assertTrue(consts.CSL_DOWNLOAD_LINK.endswith('master.zip')) + + def test_csl_accessible(self): + response = requests.head(consts.CSL_DOWNLOAD_LINK) + if response.status_code == 302: + response = requests.head(response.headers['Location']) + self.assertEqual(response.status_code, 200) + headers = response.headers + self.assertEqual(headers['Content-Type'], 'application/zip') + self.assertEqual(headers['Content-Disposition'], 'attachment; filename=styles-master.zip') + + @freeze_time('2017-01-01') + def test_date_format(self): + now = datetime.datetime.now() + self.assertEqual( + now.strftime(consts.DATE_FORMAT), + '01 January 2017' + ) + + @freeze_time('2017-01-01T12:34') + def test_time_format(self): + now = datetime.datetime.now() + self.assertEqual( + now.strftime(consts.TIME_FORMAT), + '12:34' + ) + + @freeze_time('2017-01-01T12:34') + def test_time_format(self): + now = datetime.datetime.now() + self.assertEqual( + now.strftime(consts.DATETIME_FORMAT), + '01 January 2017 12:34' + ) + + def test_dirs_existt(self): + self.assertTrue(os.path.isdir(consts.ASSETS_DIR)) + self.assertTrue(os.path.isdir(consts.TEMPLATES_DIR)) + self.assertTrue(os.path.isdir(consts.STATIC_DIR)) + + + From 3e7759c52d8470c890c52ec62ee74903ac802a4d Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sun, 28 May 2017 15:14:30 +0100 Subject: [PATCH 17/35] Remove non-determanistic test --- tests/test_pandoc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_pandoc.py b/tests/test_pandoc.py index e25d85f..4725bbc 100644 --- a/tests/test_pandoc.py +++ b/tests/test_pandoc.py @@ -48,5 +48,4 @@ class BuildDocumentTestCase(BaseTestCase): doc ) self.assertIn('Doe, John. 2005.', doc) - self.assertIn('
', doc) From e71650cd3c5d3702e9ca9e8204428d05d5f93c5b Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sun, 28 May 2017 15:51:52 +0100 Subject: [PATCH 18/35] Test hard extra config --- tests/test_context.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/test_context.py diff --git a/tests/test_context.py b/tests/test_context.py new file mode 100644 index 0000000..69d9ee4 --- /dev/null +++ b/tests/test_context.py @@ -0,0 +1,22 @@ +from tests import BaseTestCase +from md_pdf.build.context import get_context, EXTRA_CONTEXT +from md_pdf import consts +from datetime import datetime +from md_pdf import __version__ + + +class ExtraContextTestCase(BaseTestCase): + def test_directories(self): + self.assertEqual(EXTRA_CONTEXT['templates_dir'], consts.TEMPLATES_DIR) + self.assertEqual(EXTRA_CONTEXT['static_dir'], consts.STATIC_DIR) + self.assertEqual(EXTRA_CONTEXT['internal_templates_dir'], consts.INTERNAL_TEMPLATES_DIR) + self.assertEqual(EXTRA_CONTEXT['internal_static_dir'], consts.INTERNAL_STATIC_DIR) + + def test_datetimes(self): + now = datetime.now() + self.assertEqual(EXTRA_CONTEXT['date'], now.strftime(consts.DATE_FORMAT)) + self.assertEqual(EXTRA_CONTEXT['time'], now.strftime(consts.TIME_FORMAT)) + self.assertEqual(EXTRA_CONTEXT['datetime'], now.strftime(consts.DATETIME_FORMAT)) + + def test_version(self): + self.assertEqual(EXTRA_CONTEXT['mdp_version'], __version__) From 538685e5b287b3e79a7bab04592b33ec7266a548 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sun, 28 May 2017 16:58:58 +0100 Subject: [PATCH 19/35] Test config generator --- md_pdf/build/context.py | 7 ++++-- md_pdf/utils.py | 2 ++ tests/__init__.py | 12 +++++++++ tests/test_config.py | 10 +------- tests/test_context.py | 55 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 11 deletions(-) diff --git a/md_pdf/build/context.py b/md_pdf/build/context.py index 61506ff..2e2f018 100644 --- a/md_pdf/build/context.py +++ b/md_pdf/build/context.py @@ -21,8 +21,11 @@ EXTRA_CONTEXT = { def get_context(config, content): config = config.copy() - context = config['context'].copy() - del config['context'] + if 'context' in config: + context = config['context'].copy() + del config['context'] + else: + context = {} context = dict( config, **EXTRA_CONTEXT, diff --git a/md_pdf/utils.py b/md_pdf/utils.py index 101e639..4c39d48 100644 --- a/md_pdf/utils.py +++ b/md_pdf/utils.py @@ -25,6 +25,8 @@ def safe_list_get(l, idx, default): def get_plain_text(content): soup = BeautifulSoup(content, 'html.parser') body = soup.find('body') + if body is None: + return content try: body.find('h1', class_='references-title').extract() body.find('div', class_='references').extract() diff --git a/tests/__init__.py b/tests/__init__.py index 397c12c..5fe62ed 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -4,6 +4,18 @@ from md_pdf.consts import TEMPLATES_DIR, STATIC_DIR class BaseTestCase(unittest.TestCase): + def setUp(self): + super().setUp() + self.BASE_VALID_CONFIG = { + 'title': 'test title', + 'input': 'test-files/*.md', + 'output_formats': [ + 'html', 'pdf' + ], + 'output_dir': 'out/', + + } + def removeFile(self, file): try: os.remove(file) diff --git a/tests/test_config.py b/tests/test_config.py index b6e34d2..15181ff 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -20,15 +20,7 @@ class ReadConfigTestCase(BaseTestCase): class ConfigValidatorBaseTestCase(BaseTestCase): def setUp(self): - self.BASE_VALID_CONFIG = { - 'title': 'test title', - 'input': 'test-files/*.md', - 'output_formats': [ - 'html', 'pdf' - ], - 'output_dir': 'out/', - - } + super().setUp() validate.validate_config(self.BASE_VALID_CONFIG) diff --git a/tests/test_context.py b/tests/test_context.py index 69d9ee4..4a1722c 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -3,6 +3,7 @@ from md_pdf.build.context import get_context, EXTRA_CONTEXT from md_pdf import consts from datetime import datetime from md_pdf import __version__ +import os class ExtraContextTestCase(BaseTestCase): @@ -20,3 +21,57 @@ class ExtraContextTestCase(BaseTestCase): def test_version(self): self.assertEqual(EXTRA_CONTEXT['mdp_version'], __version__) + + +class ContextTestCase(BaseTestCase): + def test_context_contains_extra(self): + context = get_context(self.BASE_VALID_CONFIG, 'test') + for key in EXTRA_CONTEXT.keys(): + self.assertIn(key, context) + + def test_context_contains_context(self): + config = dict(self.BASE_VALID_CONFIG, **{ + 'context': { + '1': '2' + } + }) + context = get_context(config, 'test') + self.assertEqual(context['1'], '2') + self.assertNotIn('context', context) + + def test_context_contains_config(self): + context = get_context(self.BASE_VALID_CONFIG, 'test') + for key in self.BASE_VALID_CONFIG.keys(): + self.assertIn(key, context) + + def test_has_output_dir(self): + context = get_context(self.BASE_VALID_CONFIG, 'test') + self.assertEqual(context['output_dir'], os.path.abspath(self.BASE_VALID_CONFIG['output_dir'])) + + def test_word_count(self): + config = dict(self.BASE_VALID_CONFIG, **{ + 'show_word_count': True + }) + context = get_context(config, 'testy test test') + self.assertEqual(context['word_count'], 3) + + def test_transparent_datetime_for_submission_date(self): + for value in [ + datetime.now().date(), + datetime.now().time(), + datetime.now() + ]: + config = dict(self.BASE_VALID_CONFIG, **{ + 'submission_date': value + }) + context = get_context(config, 'test') + self.assertEqual(context['submission_date'], value.strftime(consts.DATE_FORMAT)) + + def test_date_format(self): + config = dict(self.BASE_VALID_CONFIG, **{ + 'submission_date': '2017-01-01' + }) + context = get_context(config, 'test') + self.assertEqual(context['submission_date'], '01 January 2017') + + From 58e825c43b4fa41eb6eb6d3309b5663ba909065b Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sun, 28 May 2017 22:12:22 +0100 Subject: [PATCH 20/35] Add tests for content parser --- md_pdf/build/content.py | 6 +++-- tests/__init__.py | 14 ++++++---- tests/test_parser.py | 57 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 tests/test_parser.py diff --git a/md_pdf/build/content.py b/md_pdf/build/content.py index 27e03a3..4286747 100644 --- a/md_pdf/build/content.py +++ b/md_pdf/build/content.py @@ -19,7 +19,7 @@ def fix_references_title(content, config): return soup.prettify() -def add_base_tag(doc, config): +def make_images_relative(doc, config): logger.debug("Adding Base Tag...") soup = BeautifulSoup(doc, 'html.parser') for img in soup.findAll('img'): @@ -32,6 +32,8 @@ def add_base_tag(doc, config): def add_body_class(doc, config): logger.debug("Adding Body Class...") soup = BeautifulSoup(doc, 'html.parser') + if not soup.body: + return doc soup.body['class'] = 'content' return soup.prettify() @@ -47,7 +49,7 @@ def parse_template(doc, config): parsed_doc = doc for parser in [ fix_references_title, - add_base_tag, + make_images_relative, add_body_class, ]: parsed_doc = parser(parsed_doc, config) diff --git a/tests/__init__.py b/tests/__init__.py index 5fe62ed..7679ddc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,7 @@ import unittest import os from md_pdf.consts import TEMPLATES_DIR, STATIC_DIR +from bs4 import BeautifulSoup class BaseTestCase(unittest.TestCase): @@ -16,22 +17,25 @@ class BaseTestCase(unittest.TestCase): } - def removeFile(self, file): + def parse_html(self, html): + return BeautifulSoup(html, 'html.parser') + + def remove_file(self, file): try: os.remove(file) except OSError: pass - def deleteTemplates(self): + def delete_templates(self): for template in [ 'header.html', 'footer.html', 'cover.html', 'toc.xsl', ]: - self.removeFile(os.path.join(TEMPLATES_DIR, template)) + self.remove_file(os.path.join(TEMPLATES_DIR, template)) def tearDown(self): - self.deleteTemplates() - self.removeFile(os.path.join(STATIC_DIR, 'style.css')) + self.delete_templates() + self.remove_file(os.path.join(STATIC_DIR, 'style.css')) diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..6182204 --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,57 @@ +from tests import BaseTestCase +from md_pdf.build import content +import os + + +class FixReferencesTitleTestCase(BaseTestCase): + def test_adds_reference_title(self): + html = '
' + output = content.fix_references_title(html, self.BASE_VALID_CONFIG) + self.assertIn('references-title', output) + self.assertIn('References', output) + + def test_doesnt_modify_if_no_references(self): + html = 'test text' + output = content.fix_references_title(html, self.BASE_VALID_CONFIG) + self.assertNotIn('references-title', output) + self.assertNotIn('References', output) + + +class RelativeImageTestCase(BaseTestCase): + def test_makes_image_relative(self): + html = '' + output = self.parse_html(content.make_images_relative(html, self.BASE_VALID_CONFIG)) + self.assertEqual(output.find('img').attrs['src'], os.path.abspath('test-files/test-image.png')) + + def test_leaves_remote_images(self): + html = '' + output = self.parse_html(content.make_images_relative(html, self.BASE_VALID_CONFIG)) + self.assertEqual(output.find('img').attrs['src'], 'http://example.com/image.png') + + +class AddBodyClassTestCase(BaseTestCase): + def test_adds_class(self): + html = '' + output = self.parse_html(content.add_body_class(html, self.BASE_VALID_CONFIG)) + self.assertEqual(output.body.attrs['class'], ['content']) + + def test_doesnt_change(self): + html = 'test content' + output = content.add_body_class(html, self.BASE_VALID_CONFIG) + self.assertEqual(output, html) + + +class RenderTemplateTestCase(BaseTestCase): + def test_renders_template(self): + html = 'test {{ test }}' + output = content.render_template(html, dict(self.BASE_VALID_CONFIG, **{ + 'test': 'content' + })) + self.assertEqual(output, 'test content') + + def test_changes_nothing(self): + html = 'test test' + output = content.render_template(html, dict(self.BASE_VALID_CONFIG, **{ + 'test': 'content' + })) + self.assertEqual(output, html) From f38e7a53dfc68e21612fac5c6a4da2b496f97858 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sun, 28 May 2017 22:19:45 +0100 Subject: [PATCH 21/35] Create wrapper for config extension --- tests/__init__.py | 6 ++++++ tests/test_context.py | 8 ++++---- tests/test_parser.py | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 7679ddc..14e6b78 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -26,6 +26,12 @@ class BaseTestCase(unittest.TestCase): except OSError: pass + def extend_config(self, *args): + base_config = self.BASE_VALID_CONFIG.copy() + for arg in args: + base_config = dict(base_config, **arg) + return base_config + def delete_templates(self): for template in [ 'header.html', diff --git a/tests/test_context.py b/tests/test_context.py index 4a1722c..b5eb9ad 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -30,7 +30,7 @@ class ContextTestCase(BaseTestCase): self.assertIn(key, context) def test_context_contains_context(self): - config = dict(self.BASE_VALID_CONFIG, **{ + config = self.extend_config({ 'context': { '1': '2' } @@ -49,7 +49,7 @@ class ContextTestCase(BaseTestCase): self.assertEqual(context['output_dir'], os.path.abspath(self.BASE_VALID_CONFIG['output_dir'])) def test_word_count(self): - config = dict(self.BASE_VALID_CONFIG, **{ + config = self.extend_config({ 'show_word_count': True }) context = get_context(config, 'testy test test') @@ -61,14 +61,14 @@ class ContextTestCase(BaseTestCase): datetime.now().time(), datetime.now() ]: - config = dict(self.BASE_VALID_CONFIG, **{ + config = self.extend_config({ 'submission_date': value }) context = get_context(config, 'test') self.assertEqual(context['submission_date'], value.strftime(consts.DATE_FORMAT)) def test_date_format(self): - config = dict(self.BASE_VALID_CONFIG, **{ + config = self.extend_config({ 'submission_date': '2017-01-01' }) context = get_context(config, 'test') diff --git a/tests/test_parser.py b/tests/test_parser.py index 6182204..ec28f5f 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -44,14 +44,14 @@ class AddBodyClassTestCase(BaseTestCase): class RenderTemplateTestCase(BaseTestCase): def test_renders_template(self): html = 'test {{ test }}' - output = content.render_template(html, dict(self.BASE_VALID_CONFIG, **{ + output = content.render_template(html, self.extend_config({ 'test': 'content' })) self.assertEqual(output, 'test content') def test_changes_nothing(self): html = 'test test' - output = content.render_template(html, dict(self.BASE_VALID_CONFIG, **{ + output = content.render_template(html, self.extend_config({ 'test': 'content' })) self.assertEqual(output, html) From 44504e2584bc7a2e810513f41a0b028087d96f71 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Tue, 30 May 2017 17:17:37 +0100 Subject: [PATCH 22/35] Throw script error if tests fail --- scripts/run-tests.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index dd95d21..9319521 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set -e python3 -m unittest -v $@ From e7c6e9357164eabd5ca6c73dc98d7ef5cdafd14a Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Thu, 1 Jun 2017 20:51:07 +0100 Subject: [PATCH 23/35] Try and catch exceptions from pdf render --- md_pdf/build/pdf.py | 7 ++++++- md_pdf/exceptions.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/md_pdf/build/pdf.py b/md_pdf/build/pdf.py index b498b5d..b8d3451 100644 --- a/md_pdf/build/pdf.py +++ b/md_pdf/build/pdf.py @@ -1,6 +1,7 @@ import pdfkit from md_pdf.consts import TEMPLATES_DIR, STATIC_DIR from md_pdf.build.templates import FILE_NAME_FORMAT +from md_pdf.exceptions import PDFRenderException import os import logging @@ -44,7 +45,7 @@ def export_pdf(content, config): PDF_OPTIONS['margin-right'] = config['context'].get('margin_horizontal', DEFAULT_MARGIN_HORIZONTAL) logger.info("Rendering PDF...") - return pdfkit.from_string( + render_ok = pdfkit.from_string( content, os.path.join(os.path.abspath(config['output_dir']), 'output.pdf'), options=PDF_OPTIONS, @@ -52,3 +53,7 @@ def export_pdf(content, config): toc=TOC_OPTIONS if config['toc'] else {}, cover_first=True ) + if not render_ok: + raise PDFRenderException('Failed to render PDF. ' + render_ok) + return PDF_OPTIONS # mostly for testing + diff --git a/md_pdf/exceptions.py b/md_pdf/exceptions.py index a11a747..86dc882 100644 --- a/md_pdf/exceptions.py +++ b/md_pdf/exceptions.py @@ -8,3 +8,7 @@ class PrematureExit(BaseException): class ConfigValidationException(BaseException): pass + + +class PDFRenderException(BaseException): + pass From 8b914d8ae2391610419fdbf19423c21824521d4e Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Thu, 1 Jun 2017 22:04:21 +0100 Subject: [PATCH 24/35] Test PDF renderer --- md_pdf/build/pdf.py | 36 +++++++++--------- tests/__init__.py | 17 +++++++++ tests/test_pdf.py | 90 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 17 deletions(-) create mode 100644 tests/test_pdf.py diff --git a/md_pdf/build/pdf.py b/md_pdf/build/pdf.py index 1802710..7ad086b 100644 --- a/md_pdf/build/pdf.py +++ b/md_pdf/build/pdf.py @@ -13,8 +13,8 @@ DEFAULT_MARGIN_VERTICAL = '1.5cm' DEFAULT_MARGIN_HORIZONTAL = '2.5cm' STYLE_FILE = os.path.join(STATIC_DIR, 'style.css') -HEADER_FILE = os.path.join(TEMPLATES_DIR, 'header.html') -FOOTER_FILE = os.path.join(TEMPLATES_DIR, 'footer.html') +HEADER_FILE = FILE_NAME_FORMAT.format('header') +FOOTER_FILE = FILE_NAME_FORMAT.format('footer') TOC_OPTIONS = { 'xsl-style-sheet': os.path.join(TEMPLATES_DIR, 'toc.xsl') @@ -37,23 +37,25 @@ def export_pdf(content, config): if logger.getEffectiveLevel() > logging.DEBUG: PDF_OPTIONS['quiet'] = "" PDF_OPTIONS['title'] = config.get('title', 'Output') - PDF_OPTIONS['replace'] = [(key, str(value)) for key, value in config['context'].items()] + context = config.get('context', {}) - PDF_OPTIONS['margin-top'] = config['context'].get('margin_vertical', DEFAULT_MARGIN_VERTICAL) - PDF_OPTIONS['margin-bottom'] = config['context'].get('margin_vertical', DEFAULT_MARGIN_VERTICAL) - PDF_OPTIONS['margin-left'] = config['context'].get('margin_horizontal', DEFAULT_MARGIN_HORIZONTAL) - PDF_OPTIONS['margin-right'] = config['context'].get('margin_horizontal', DEFAULT_MARGIN_HORIZONTAL) + PDF_OPTIONS['replace'] = [(key, str(value)) for key, value in context.items()] + PDF_OPTIONS['margin-top'] = context.get('margin_vertical', DEFAULT_MARGIN_VERTICAL) + PDF_OPTIONS['margin-bottom'] = context.get('margin_vertical', DEFAULT_MARGIN_VERTICAL) + PDF_OPTIONS['margin-left'] = context.get('margin_horizontal', DEFAULT_MARGIN_HORIZONTAL) + PDF_OPTIONS['margin-right'] = context.get('margin_horizontal', DEFAULT_MARGIN_HORIZONTAL) logger.info("Rendering PDF...") - render_ok = pdfkit.from_string( - content, - os.path.join(os.path.abspath(config['output_dir']), 'output.pdf'), - options=PDF_OPTIONS, - cover=FILE_NAME_FORMAT.format('cover'), - toc=TOC_OPTIONS if config.get('toc') else {}, - cover_first=True - ) - if not render_ok: - raise PDFRenderException('Failed to render PDF. ' + render_ok) + try: + pdfkit.from_string( + content, + os.path.join(os.path.abspath(config['output_dir']), 'output.pdf'), + options=PDF_OPTIONS, + cover=FILE_NAME_FORMAT.format('cover'), + toc=TOC_OPTIONS if config.get('toc') else {}, + cover_first=True + ) + except OSError as e: + raise PDFRenderException('Failed to render PDF. ' + str(e)) return PDF_OPTIONS # mostly for testing diff --git a/tests/__init__.py b/tests/__init__.py index 14e6b78..fe1f9b2 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,7 @@ import unittest import os from md_pdf.consts import TEMPLATES_DIR, STATIC_DIR +from md_pdf.build.templates import FILE_NAME_FORMAT from bs4 import BeautifulSoup @@ -26,6 +27,17 @@ class BaseTestCase(unittest.TestCase): except OSError: pass + def touch_file(self, file): + open(file, 'w').close() + + def create_fake_templates(self): + for template in [ + 'header', + 'footer', + 'cover' + ]: + self.touch_file(FILE_NAME_FORMAT.format(template)) + def extend_config(self, *args): base_config = self.BASE_VALID_CONFIG.copy() for arg in args: @@ -45,3 +57,8 @@ class BaseTestCase(unittest.TestCase): self.delete_templates() self.remove_file(os.path.join(STATIC_DIR, 'style.css')) + def call_to_args(self, call): + args = tuple(call.call_args)[0] + kwargs = tuple(call.call_args)[1] + return args, kwargs + diff --git a/tests/test_pdf.py b/tests/test_pdf.py new file mode 100644 index 0000000..43199e5 --- /dev/null +++ b/tests/test_pdf.py @@ -0,0 +1,90 @@ +from tests import BaseTestCase +import os +from md_pdf.build.pdf import export_pdf, TOC_OPTIONS, DEFAULT_MARGIN_VERTICAL, DEFAULT_MARGIN_HORIZONTAL +from unittest.mock import patch +from md_pdf.build.templates import FILE_NAME_FORMAT +from md_pdf.exceptions import PDFRenderException +import pdfkit + + +class PDFRendererTestCase(BaseTestCase): + def setUp(self): + super().setUp() + self.content = 'test content' + self.output_file_path = os.path.join(self.BASE_VALID_CONFIG['output_dir'], 'output.pdf') + self.assertFalse(os.path.isfile(self.output_file_path)) + self.create_fake_templates() + + def tearDown(self): + super().tearDown() + self.remove_file(self.output_file_path) + + def test_renders(self): + export_pdf(self.content, self.BASE_VALID_CONFIG) + + def test_title(self): + context = export_pdf(self.content, self.BASE_VALID_CONFIG) + self.assertEqual(context['title'], self.BASE_VALID_CONFIG['title']) + + def test_replace_context(self): + self.BASE_VALID_CONFIG['context'] = { + '1': 2, + '2': '1' + } + context = export_pdf(self.content, self.BASE_VALID_CONFIG) + self.assertEqual(context['replace'], [ + ('1', '2'), + ('2', '1'), + ]) + + def test_default_margins(self): + context = export_pdf(self.content, self.BASE_VALID_CONFIG) + self.assertEqual(context['margin-top'], DEFAULT_MARGIN_VERTICAL) + self.assertEqual(context['margin-bottom'], DEFAULT_MARGIN_VERTICAL) + self.assertEqual(context['margin-left'], DEFAULT_MARGIN_HORIZONTAL) + self.assertEqual(context['margin-right'], DEFAULT_MARGIN_HORIZONTAL) + + def test_override_margin(self): + self.BASE_VALID_CONFIG['context'] = { + 'margin_vertical': '1cm', + 'margin_horizontal': '2cm' + } + context = export_pdf(self.content, self.BASE_VALID_CONFIG) + self.assertEqual(context['margin-top'], '1cm') + self.assertEqual(context['margin-bottom'], '1cm') + self.assertEqual(context['margin-left'], '2cm') + self.assertEqual(context['margin-right'], '2cm') + + @patch.object(pdfkit, 'from_string') + def test_kit_call(self, pdf_render): + context = export_pdf(self.content, self.BASE_VALID_CONFIG) + self.assertTrue(pdf_render.called) + args, kwargs = self.call_to_args(pdf_render) + self.assertEqual(args[0], self.content) + self.assertIn(self.output_file_path, args[1]) + self.assertEqual(kwargs['options'], context) + self.assertTrue(kwargs['cover_first']) + self.assertEqual(kwargs['cover'], FILE_NAME_FORMAT.format('cover')) + self.assertEqual(kwargs['toc'], {}) + + @patch.object(pdfkit, 'from_string') + def test_toc(self, pdf_render): + self.BASE_VALID_CONFIG['toc'] = True + export_pdf(self.content, self.BASE_VALID_CONFIG) + args, kwargs = self.call_to_args(pdf_render) + self.assertEqual(kwargs['toc'], TOC_OPTIONS) + + def test_fails_if_missing_templates(self): + self.remove_file(FILE_NAME_FORMAT.format('cover')) + with self.assertRaises(PDFRenderException): + export_pdf(self.content, self.BASE_VALID_CONFIG) + + def test_files_exist(self): + context = export_pdf(self.content, self.BASE_VALID_CONFIG) + self.assertTrue(os.path.isfile(context['header-html'])) + self.assertTrue(os.path.isfile(context['footer-html'])) + self.assertEqual(context['header-html'], FILE_NAME_FORMAT.format('header')) + self.assertEqual(context['footer-html'], FILE_NAME_FORMAT.format('footer')) + + + From c563129d5de3bfb83b9824dbf32d3af611372d31 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Thu, 1 Jun 2017 22:06:08 +0100 Subject: [PATCH 25/35] Actually test the file is output --- tests/test_pdf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_pdf.py b/tests/test_pdf.py index 43199e5..ac2e9c7 100644 --- a/tests/test_pdf.py +++ b/tests/test_pdf.py @@ -21,6 +21,7 @@ class PDFRendererTestCase(BaseTestCase): def test_renders(self): export_pdf(self.content, self.BASE_VALID_CONFIG) + self.assertTrue(os.path.isfile(self.output_file_path)) def test_title(self): context = export_pdf(self.content, self.BASE_VALID_CONFIG) From bc9005e8b79c5e867a7edf97a7896a3ccda15a2d Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Thu, 1 Jun 2017 22:14:28 +0100 Subject: [PATCH 26/35] remove trailing blank lines --- md_pdf/build/pdf.py | 1 - tests/test_pdf.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/md_pdf/build/pdf.py b/md_pdf/build/pdf.py index 7ad086b..40200a0 100644 --- a/md_pdf/build/pdf.py +++ b/md_pdf/build/pdf.py @@ -58,4 +58,3 @@ def export_pdf(content, config): except OSError as e: raise PDFRenderException('Failed to render PDF. ' + str(e)) return PDF_OPTIONS # mostly for testing - diff --git a/tests/test_pdf.py b/tests/test_pdf.py index ac2e9c7..597d4f7 100644 --- a/tests/test_pdf.py +++ b/tests/test_pdf.py @@ -86,6 +86,3 @@ class PDFRendererTestCase(BaseTestCase): self.assertTrue(os.path.isfile(context['footer-html'])) self.assertEqual(context['header-html'], FILE_NAME_FORMAT.format('header')) self.assertEqual(context['footer-html'], FILE_NAME_FORMAT.format('footer')) - - - From 56184e25c407935db6899743fba307a09ecec137 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sat, 3 Jun 2017 22:50:16 +0100 Subject: [PATCH 27/35] Dont hit network in tests --- dev-requirements.txt | 1 - tests/test_consts.py | 19 ++++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 6b74877..217ed69 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,4 @@ bandit==1.4.0 flake8==3.3.0 freezegun==0.3.9 -requests==2.16.5 safety==0.5.1 diff --git a/tests/test_consts.py b/tests/test_consts.py index 7bbbe9b..f31f696 100644 --- a/tests/test_consts.py +++ b/tests/test_consts.py @@ -2,7 +2,7 @@ from tests import BaseTestCase from md_pdf import consts from unittest import skipIf import os -import requests +from urllib.parse import urlparse import datetime from freezegun import freeze_time @@ -60,17 +60,10 @@ class ConstsTestCase(BaseTestCase): self.assertIn('mdp.yml', consts.CONFIG_FILE) def test_csl_download_link(self): - self.assertIn('https://github.com', consts.CSL_DOWNLOAD_LINK) - self.assertTrue(consts.CSL_DOWNLOAD_LINK.endswith('master.zip')) - - def test_csl_accessible(self): - response = requests.head(consts.CSL_DOWNLOAD_LINK) - if response.status_code == 302: - response = requests.head(response.headers['Location']) - self.assertEqual(response.status_code, 200) - headers = response.headers - self.assertEqual(headers['Content-Type'], 'application/zip') - self.assertEqual(headers['Content-Disposition'], 'attachment; filename=styles-master.zip') + url = urlparse(consts.CSL_DOWNLOAD_LINK) + self.assertEqual(url.netloc, 'github.com') + self.assertTrue(url.path.endswith('master.zip')) + self.assertIn('citation-style-language/styles', url.path) @freeze_time('2017-01-01') def test_date_format(self): @@ -96,7 +89,7 @@ class ConstsTestCase(BaseTestCase): '01 January 2017 12:34' ) - def test_dirs_existt(self): + def test_dirs_exist(self): self.assertTrue(os.path.isdir(consts.ASSETS_DIR)) self.assertTrue(os.path.isdir(consts.TEMPLATES_DIR)) self.assertTrue(os.path.isdir(consts.STATIC_DIR)) From c5400915de1ec071867bcaec01efd6ff35688279 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Thu, 8 Jun 2017 10:04:09 +0100 Subject: [PATCH 28/35] Add type checking --- .gitignore | 1 + circle.yml | 1 + dev-requirements.txt | 1 + 3 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b9bab86..f57529d 100644 --- a/.gitignore +++ b/.gitignore @@ -245,3 +245,4 @@ ENV/ # End of https://www.gitignore.io/api/node,linux,python,jetbrains,archlinuxpackages out/ +.mypy_cache diff --git a/circle.yml b/circle.yml index 259dbb0..55fc5f3 100644 --- a/circle.yml +++ b/circle.yml @@ -21,6 +21,7 @@ test: override: - npm test - flake8 md_pdf/ --ignore=E128,E501 + - mypy --ignore-missing-imports md_pdf - safety check - bandit -r md_pdf/ - mdp --update-csl -vvv diff --git a/dev-requirements.txt b/dev-requirements.txt index 217ed69..e6a2a71 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,5 @@ bandit==1.4.0 flake8==3.3.0 freezegun==0.3.9 +mypy==0.511 safety==0.5.1 From f45f551bdf63d6742f88a350f72f64259fa9f499 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Thu, 8 Jun 2017 10:09:52 +0100 Subject: [PATCH 29/35] Collect coverage --- dev-requirements.txt | 1 + scripts/run-tests.sh | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index e6a2a71..97fd9cd 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,5 @@ bandit==1.4.0 +coverage==4.4.1 flake8==3.3.0 freezegun==0.3.9 mypy==0.511 diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 9319521..01f5863 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -2,4 +2,7 @@ set -e -python3 -m unittest -v $@ +coverage run --source=md_pdf -m unittest -v $@ + +coverage report +coverage html From 392703e3cbe5bb6c446d20e654451ff4fba6c820 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Thu, 8 Jun 2017 10:53:30 +0100 Subject: [PATCH 30/35] Test args --- md_pdf/args.py | 4 ++-- tests/test_args.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 tests/test_args.py diff --git a/md_pdf/args.py b/md_pdf/args.py index 3b42416..a576fc5 100644 --- a/md_pdf/args.py +++ b/md_pdf/args.py @@ -2,10 +2,10 @@ import argparse from md_pdf import __version__ -def parse_args(): +def parse_args(args=None): parser = argparse.ArgumentParser() parser.add_argument("-v", "--verbose", help="Set verbosity level (repeat argument)", action="count", default=0) parser.add_argument("--update-csl", help="Update CSL files", action="store_true") parser.add_argument("--version", action="version", version="%(prog)s {}".format(__version__)) parser.add_help = True - return parser.parse_args() + return parser.parse_args(args=args) diff --git a/tests/test_args.py b/tests/test_args.py new file mode 100644 index 0000000..359eba0 --- /dev/null +++ b/tests/test_args.py @@ -0,0 +1,22 @@ +from tests import BaseTestCase +from md_pdf.args import parse_args + + +class ArgParserTestCase(BaseTestCase): + def test_allows_no_args(self): + args = parse_args([]) + self.assertFalse(args.update_csl) + self.assertEqual(args.verbose, 0) + + def test_adds_verbosity(self): + args = parse_args(['-v']) + self.assertEqual(args.verbose, 1) + + def test_chains_verbosity(self): + for i in range(1, 10): + args = parse_args(['-' + ('v' * i)]) + self.assertEqual(args.verbose, i) + + def test_csl_update(self): + args = parse_args(['--update-csl']) + self.assertTrue(args.update_csl) From ddb51ceee467895a536a84cc63016f82908fba36 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Thu, 8 Jun 2017 12:26:15 +0100 Subject: [PATCH 31/35] Test file reader --- md_pdf/config/read.py | 2 +- tests/test_pandoc.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/md_pdf/config/read.py b/md_pdf/config/read.py index 046064b..20ad7d2 100644 --- a/md_pdf/config/read.py +++ b/md_pdf/config/read.py @@ -6,7 +6,7 @@ from md_pdf.exceptions import ConfigValidationException def load_config(location=CONFIG_FILE): try: - with open(os.path.join(location)) as f: + with open(location) as f: return yaml.safe_load(f) except FileNotFoundError: raise ConfigValidationException("Can't find config file at {}".format(CONFIG_FILE)) diff --git a/tests/test_pandoc.py b/tests/test_pandoc.py index 4725bbc..df933a7 100644 --- a/tests/test_pandoc.py +++ b/tests/test_pandoc.py @@ -1,7 +1,23 @@ from tests import BaseTestCase from md_pdf.build.pandoc import output_html, build_document +from md_pdf.build.md import read_files from md_pdf.utils import remove_dir import os +import glob + + +class ReadFileTestCase(BaseTestCase): + file_glob = 'test-files/*.md' + + def test_reads_files(self): + files = read_files(self.file_glob) + self.assertNotEqual(files, '') + + def test_contains_all_files(self): + files = read_files(self.file_glob) + for file in glob.iglob(self.file_glob): + with open(file) as f: + self.assertIn(f.read(), files) class OutputHTMLTestCase(BaseTestCase): From ebdfa67f3cf371c556d520cba3ee1f15ec566c88 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Fri, 9 Jun 2017 22:33:40 +0100 Subject: [PATCH 32/35] remove unused import --- md_pdf/config/read.py | 1 - 1 file changed, 1 deletion(-) diff --git a/md_pdf/config/read.py b/md_pdf/config/read.py index 20ad7d2..27eb42e 100644 --- a/md_pdf/config/read.py +++ b/md_pdf/config/read.py @@ -1,5 +1,4 @@ import yaml -import os from md_pdf.consts import CONFIG_FILE from md_pdf.exceptions import ConfigValidationException From bbfc3cac4cac193f40680a900127ab632586c74c Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Fri, 9 Jun 2017 23:07:30 +0100 Subject: [PATCH 33/35] Add type annotations to most places --- md_pdf/build/__init__.py | 2 +- md_pdf/build/context.py | 10 +++++----- md_pdf/build/jinja.py | 2 +- md_pdf/build/md.py | 5 +++-- md_pdf/build/pandoc.py | 4 ++-- md_pdf/build/pdf.py | 2 +- md_pdf/build/templates.py | 4 ++-- md_pdf/config/read.py | 2 +- md_pdf/utils.py | 8 +++++--- 9 files changed, 21 insertions(+), 18 deletions(-) diff --git a/md_pdf/build/__init__.py b/md_pdf/build/__init__.py index 177216b..6b94e00 100644 --- a/md_pdf/build/__init__.py +++ b/md_pdf/build/__init__.py @@ -11,7 +11,7 @@ import time logger = logging.getLogger(__file__) -def build(config): +def build(config: dict): logger.debug("Starting Build...") start_time = time.time() data = read_files(os.path.abspath(config['input'])) diff --git a/md_pdf/build/context.py b/md_pdf/build/context.py index 2e2f018..958caec 100644 --- a/md_pdf/build/context.py +++ b/md_pdf/build/context.py @@ -19,14 +19,14 @@ EXTRA_CONTEXT = { } -def get_context(config, content): +def get_context(config: dict, content: str) -> dict: config = config.copy() if 'context' in config: context = config['context'].copy() del config['context'] else: context = {} - context = dict( + merged_context = dict( config, **EXTRA_CONTEXT, **context, @@ -35,11 +35,11 @@ def get_context(config, content): } ) if config.get('show_word_count'): - context['word_count'] = word_count(get_plain_text(content)) + merged_context['word_count'] = word_count(get_plain_text(content)) if config.get('submission_date'): if type(config['submission_date']) in [datetime.date, datetime.datetime, datetime.time]: submission_date = config['submission_date'] else: submission_date = parser.parse(config['submission_date']) - context['submission_date'] = submission_date.strftime(DATE_FORMAT) - return context + merged_context['submission_date'] = submission_date.strftime(DATE_FORMAT) + return merged_context diff --git a/md_pdf/build/jinja.py b/md_pdf/build/jinja.py index d5f0687..2b068de 100644 --- a/md_pdf/build/jinja.py +++ b/md_pdf/build/jinja.py @@ -1,7 +1,7 @@ from jinja2 import Environment -def render_content(content, context): +def render_content(content: str, context: dict) -> str: env = Environment( autoescape=True, trim_blocks=True, diff --git a/md_pdf/build/md.py b/md_pdf/build/md.py index 93ba6a0..d08729d 100644 --- a/md_pdf/build/md.py +++ b/md_pdf/build/md.py @@ -1,12 +1,13 @@ import glob +from typing import Generator, List -def get_files_content(filenames): +def get_files_content(filenames: List[str]) -> Generator[str, None, None]: for filename in filenames: with open(filename) as f: yield f.read() -def read_files(files_glob): +def read_files(files_glob: str) -> str: filenames = sorted(glob.glob(files_glob)) return '\n'.join(list(get_files_content(filenames))) diff --git a/md_pdf/build/pandoc.py b/md_pdf/build/pandoc.py index 1c7ab2d..7fd4362 100644 --- a/md_pdf/build/pandoc.py +++ b/md_pdf/build/pandoc.py @@ -6,13 +6,13 @@ import logging logger = logging.getLogger(__file__) -def output_html(html, out_dir): +def output_html(html: str, out_dir: str): logger.info("Outputting HTML...") with open(os.path.join(out_dir, 'output.html'), 'w') as f: f.write(html) -def build_document(files_content, bibliography): +def build_document(files_content: str, bibliography: dict) -> str: args = [ '-s', ] diff --git a/md_pdf/build/pdf.py b/md_pdf/build/pdf.py index 40200a0..9fe4612 100644 --- a/md_pdf/build/pdf.py +++ b/md_pdf/build/pdf.py @@ -33,7 +33,7 @@ PDF_OPTIONS = { } -def export_pdf(content, config): +def export_pdf(content: str, config: dict) -> dict: if logger.getEffectiveLevel() > logging.DEBUG: PDF_OPTIONS['quiet'] = "" PDF_OPTIONS['title'] = config.get('title', 'Output') diff --git a/md_pdf/build/templates.py b/md_pdf/build/templates.py index e0cf1f0..e62966c 100644 --- a/md_pdf/build/templates.py +++ b/md_pdf/build/templates.py @@ -11,7 +11,7 @@ FILE_NAME_FORMAT = os.path.join(TEMPLATES_DIR, "{}.html") TEMPLATE_FORMAT = os.path.join(INTERNAL_TEMPLATES_DIR, "{}-template.html") -def render_page(input_file, output_file, context): +def render_page(input_file: str, output_file: str, context: dict) -> str: logger.debug("Rendering {}...".format(os.path.splitext(os.path.basename(output_file))[0].title())) with open(input_file) as f: content = render_content(f.read(), context) @@ -20,7 +20,7 @@ def render_page(input_file, output_file, context): return content -def render_templates(config, content): +def render_templates(config: dict, content: str): context = get_context(config, content) for template in [ 'cover', diff --git a/md_pdf/config/read.py b/md_pdf/config/read.py index 27eb42e..6950fe1 100644 --- a/md_pdf/config/read.py +++ b/md_pdf/config/read.py @@ -3,7 +3,7 @@ from md_pdf.consts import CONFIG_FILE from md_pdf.exceptions import ConfigValidationException -def load_config(location=CONFIG_FILE): +def load_config(location: str=CONFIG_FILE) -> str: try: with open(location) as f: return yaml.safe_load(f) diff --git a/md_pdf/utils.py b/md_pdf/utils.py index 4c39d48..33dc229 100644 --- a/md_pdf/utils.py +++ b/md_pdf/utils.py @@ -2,11 +2,13 @@ import shutil import os import logging from bs4 import BeautifulSoup +from typing import List + logger = logging.getLogger(__file__) -def remove_dir(dir): +def remove_dir(dir: str): logger.debug("Removing directory {}.".format(dir)) try: shutil.rmtree(dir) @@ -15,14 +17,14 @@ def remove_dir(dir): pass -def safe_list_get(l, idx, default): +def safe_list_get(l: List, idx: int, default): try: return l[idx] except IndexError: return default -def get_plain_text(content): +def get_plain_text(content: str) -> str: soup = BeautifulSoup(content, 'html.parser') body = soup.find('body') if body is None: From 270b7195c763f6a2cd1f6c96fcfb40842b00cda4 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sat, 10 Jun 2017 10:35:57 +0100 Subject: [PATCH 34/35] Types for renderer --- md_pdf/build/jinja.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/md_pdf/build/jinja.py b/md_pdf/build/jinja.py index 2b068de..d5f0687 100644 --- a/md_pdf/build/jinja.py +++ b/md_pdf/build/jinja.py @@ -1,7 +1,7 @@ from jinja2 import Environment -def render_content(content: str, context: dict) -> str: +def render_content(content, context): env = Environment( autoescape=True, trim_blocks=True, From f0c377d6b8100c2dc73782250345a263dba71c59 Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Sat, 10 Jun 2017 11:17:33 +0100 Subject: [PATCH 35/35] Add dedicated tests for jinja templates --- md_pdf/build/jinja.py | 1 - tests/test_jinja.py | 27 +++++++++++++++++++++++++++ tests/test_parser.py | 7 ------- 3 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 tests/test_jinja.py diff --git a/md_pdf/build/jinja.py b/md_pdf/build/jinja.py index d5f0687..7d1e115 100644 --- a/md_pdf/build/jinja.py +++ b/md_pdf/build/jinja.py @@ -10,7 +10,6 @@ def render_content(content, context): 'jinja2.ext.with_', 'jinja2.ext.loopcontrols' ] - ) template = env.from_string(content) return template.render(**context) diff --git a/tests/test_jinja.py b/tests/test_jinja.py new file mode 100644 index 0000000..2c4af8f --- /dev/null +++ b/tests/test_jinja.py @@ -0,0 +1,27 @@ +from tests import BaseTestCase +from md_pdf.build.jinja import render_content + + +class ContentRendererTestCase(BaseTestCase): + def test_renders_template(self): + html = 'test {{ test }}' + output = render_content(html, self.extend_config({ + 'test': 'content' + })) + self.assertEqual(output, 'test content') + + def test_changes_nothing(self): + html = 'test test' + output = render_content(html, self.extend_config({ + 'test': 'content' + })) + self.assertEqual(output, html) + + def test_with_block(self): + html = """ + {% with test = 'test' %} + {{ test }} thing + {% endwith %} + """ + output = render_content(html, self.BASE_VALID_CONFIG) + self.assertIn('test thing', output) diff --git a/tests/test_parser.py b/tests/test_parser.py index ec28f5f..41acc67 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -48,10 +48,3 @@ class RenderTemplateTestCase(BaseTestCase): 'test': 'content' })) self.assertEqual(output, 'test content') - - def test_changes_nothing(self): - html = 'test test' - output = content.render_template(html, self.extend_config({ - 'test': 'content' - })) - self.assertEqual(output, html)