diff --git a/novel_stats/novel_stats.py b/novel_stats/novel_stats.py index efc3aba..6f66893 100755 --- a/novel_stats/novel_stats.py +++ b/novel_stats/novel_stats.py @@ -3,12 +3,13 @@ import collections import sys - +import tempfile CHAPTER_MARKER = '## ' STATUS_MARKER = '[status]: # ' ACT_MARKER = '[act]: # ' -COMMENT_MARKER = '[//]: # ' # Strandard markdown comment marker, supported by pandoc and calibre's ebook-convert +# Standard markdown comment marker, supported by Pandoc and Calibre's ebook-convert. +COMMENT_MARKER = '[//]: # ' def count_words(line): @@ -31,7 +32,8 @@ def main(): if '-pp' in arguments: # -pp flag to allow Markdown Preprocessing primarily to allow multi-file novel formatting # this is implemented using a temporary file created using python's buit-in tempfile library - import MarkdownPP, tempfile + import MarkdownPP + mdfile = tempfile.TemporaryFile(mode='w+') MarkdownPP.MarkdownPP(input=open(filename), output=mdfile, modules=list(MarkdownPP.modules)) mdfile.seek(0) @@ -52,22 +54,24 @@ def main(): word_count_by_act[act_heading] += word_count_by_chapter[chapter_heading] total_word_count += word_count_by_chapter[chapter_heading] - chapter_heading = line[len(CHAPTER_MARKER):].strip('()\n') + chapter_heading = line[len(CHAPTER_MARKER) :].strip('()\n') - word_count_by_chapter[chapter_heading] = count_words(chapter_heading) # Count the words in chapter heading, because the chapter number and title count as words. + # Count the words in chapter heading, because the chapter number and title count as words. + word_count_by_chapter[chapter_heading] = count_words(chapter_heading) status_by_chapter[chapter_heading] = collections.defaultdict(int) current_status = None - elif line.startswith(STATUS_MARKER): # Modified to allow multiple statuses in a single chapter, can swap back and forth. - if current_status == None: - current_status = line[len(STATUS_MARKER):].strip('()\n') + # Modified to allow multiple statuses in a single chapter, can swap back and forth. + elif line.startswith(STATUS_MARKER): + if current_status is None: + current_status = line[len(STATUS_MARKER) :].strip('()\n') status_by_chapter[chapter_heading][current_status] = count_words(chapter_heading) else: - current_status = line[len(STATUS_MARKER):].strip('()\n') + current_status = line[len(STATUS_MARKER) :].strip('()\n') elif line.startswith(ACT_MARKER): - act_heading = line[len(ACT_MARKER):].strip('()\n') + act_heading = line[len(ACT_MARKER) :].strip('()\n') word_count_by_act[act_heading] = count_words(act_heading) - elif line.startswith(COMMENT_MARKER): # don't count the words in a comment + elif line.startswith(COMMENT_MARKER): # Don't count the words in a comment. pass else: line_word_count = count_words(line) @@ -83,7 +87,8 @@ def main(): word_count_by_act[act_heading] += word_count_by_chapter[chapter_heading] total_word_count += word_count_by_chapter[chapter_heading] - if '-c' in arguments or '--chapter' in arguments: # -c or --chapter to give a chapter-by-chapter word count summary + # -c or --chapter to give a chapter-by-chapter word count summary. + if '-c' in arguments or '--chapter' in arguments: for chapter_heading, chapter_word_count in word_count_by_chapter.items(): if chapter_heading is None: continue @@ -102,17 +107,24 @@ def main(): print() - if '-a' in arguments or '--act' in arguments: # -a or --act to give an act-by-act word count summary + # -a or --act to give an act-by-act word count summary. + if '-a' in arguments or '--act' in arguments: for act_heading, act_word_count in word_count_by_act.items(): if act_heading is None: continue - print('act {}: {:,} words (~{}%)'.format(act_heading, act_word_count, act_word_count * 100 // total_word_count)) + print( + 'act {}: {:,} words (~{}%)'.format( + act_heading, act_word_count, act_word_count * 100 // total_word_count + ) + ) print() for status, status_word_count in word_count_by_status.items(): - print(f'{status}: {status_word_count:,} words (~{status_word_count * 100 // total_word_count}%)') + print( + f'{status}: {status_word_count:,} words (~{status_word_count * 100 // total_word_count}%)' + ) print(f'total: {total_word_count:,} words') diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..10572da --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +line-length = 100 +skip-string-normalization = true diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..f6bed47 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,20 @@ +[metadata] +description_file=README.md + +[tool:pytest] +testpaths = tests +addopts = --cov-report term-missing:skip-covered --cov=novel_stats +filterwarnings = + ignore:Coverage disabled.*:pytest.PytestWarning + +[flake8] +ignore = E203,E501,W503 +exclude = *.*/* + +[tool:isort] +force_single_line = False +include_trailing_comma = True +known_first_party = novel_stats +line_length = 100 +multi_line_output = 3 +skip = .tox diff --git a/setup.py b/setup.py index 58108d7..9b7f29b 100644 --- a/setup.py +++ b/setup.py @@ -1,33 +1,27 @@ from setuptools import find_packages, setup -VERSION = '0.1.0' +VERSION = "0.1.0" setup( - name='novel-stats', + name="novel-stats", version=VERSION, - description='Produce word count statistics for novels written in Markdown format.', - author='Dan Helfman', - author_email='witten@torsion.org', - url='https://projects.torsion.org/witten/novel-stats', + description="Produce word count statistics for novels written in Markdown format.", + author="Dan Helfman", + author_email="witten@torsion.org", + url="https://projects.torsion.org/witten/novel-stats", classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Other Audience', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Programming Language :: Python', - 'Topic :: Office/Business', - 'Topic :: Text Processing :: Markup', + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Other Audience", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Programming Language :: Python", + "Topic :: Office/Business", + "Topic :: Text Processing :: Markup", ], - packages=find_packages(exclude=['tests*']), - entry_points={ - 'console_scripts': [ - 'novel-stats = novel_stats.novel_stats:main', - ] - }, + packages=find_packages(exclude=["tests*"]), + entry_points={"console_scripts": ["novel-stats = novel_stats.novel_stats:main",]}, install_requires=(), - extras_require = { - 'multi_file': ['MarkdownPP'], - }, + extras_require={"multi_file": ["MarkdownPP"],}, include_package_data=True, ) diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..bee04f4 --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,18 @@ +appdirs==1.4.4; python_version >= '3.8' +attrs==20.3.0; python_version >= '3.8' +black==19.10b0; python_version >= '3.8' +click==7.1.2; python_version >= '3.8' +coverage==5.3 +flake8==3.8.4 +flexmock==0.10.4 +isort==5.9.1 +mccabe==0.6.1 +pluggy==0.13.1 +pathspec==0.8.1; python_version >= '3.8' +py==1.10.0 +pycodestyle==2.6.0 +pyflakes==2.2.0 +pytest==6.1.2 +pytest-cov==2.10.1 +regex; python_version >= '3.8' +typed-ast==1.4.2; python_version >= '3.8' diff --git a/tests/test_novel_stats.py b/tests/test_novel_stats.py new file mode 100644 index 0000000..e041d58 --- /dev/null +++ b/tests/test_novel_stats.py @@ -0,0 +1,17 @@ +from novel_stats import novel_stats as module + + +def test_count_words_tallies_basic_sentence(): + assert module.count_words('This sentence is five words.') == 5 + + +def test_count_words_em_dash_does_not_split_words(): + assert module.count_words('This is only six—or is it?') == 6 + + +def test_count_words_skips_chapter_marker(): + assert module.count_words('## Chapter 1') == 2 + + +def test_count_words_skips_scene_break(): + assert module.count_words('* * *') == 0 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..a2146c4 --- /dev/null +++ b/tox.ini @@ -0,0 +1,28 @@ +[tox] +envlist = py36,py37,py38,py39 +skip_missing_interpreters = True +skipsdist = True +minversion = 3.14.1 + +[testenv] +usedevelop = True +deps = -rtest_requirements.txt +passenv = COVERAGE_FILE +commands = + pytest {posargs} + py38,py39: black --check . + isort --check-only --settings-path setup.cfg . + flake8 novel_stats tests + +[testenv:black] +commands = + black {posargs} . + +[testenv:test] +commands = + pytest {posargs} + +[testenv:isort] +deps = {[testenv]deps} +commands = + isort --settings-path setup.cfg .