├── .coveragerc ├── .editorconfig ├── .gitignore ├── .requirements.diff ├── .travis.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── charts ├── README.md ├── __init__.py ├── charts.py └── images │ ├── basic-100-circo-v1.0.0.png │ ├── basic-100-circo-v2.0.0.png │ ├── basic-100-dot-v0.1.0.png │ ├── basic-100-dot-v1.0.0.png │ ├── basic-100-dot-v2.0.0.png │ ├── basic-100-fdp-v1.0.0.png │ ├── basic-100-fdp-v2.0.0.png │ ├── basic-100-neato-v0.1.0.png │ ├── basic-100-neato-v1.0.0.png │ ├── basic-100-neato-v2.0.0.png │ ├── basic-100-sfdp-v1.0.0.png │ ├── basic-100-sfdp-v2.0.0.png │ ├── basic-100-twopi-v1.0.0.png │ ├── basic-100-twopi-v2.0.0.png │ ├── basic-1000-dot-v0.1.0.png │ ├── basic-1000-dot-v1.0.0.png │ ├── basic-1000-dot-v2.0.0.png │ ├── basic-1000-neato-v0.1.0.png │ ├── basic-circo-v0.1.0.png │ ├── basic-dot-v0.1.0.png │ ├── basic-fdp-v0.1.0.png │ ├── basic-neato-v0.1.0.png │ └── basic-twopi-v0.1.0.png ├── docs ├── Makefile ├── github_docs.py └── source │ ├── _static │ ├── .gitkeep │ ├── basic-1000-dot-v1.0.0.png │ ├── basic-1000-dot-v2.0.0.png │ └── hangman.jpg │ ├── authors.rst │ ├── conf.py │ ├── contributing.rst │ ├── design.rst │ ├── goals.rst │ ├── hangman.rst │ ├── history.rst │ ├── index.rst │ ├── installation.rst │ ├── readme.rst │ ├── readme_call_diagram.rst │ ├── readme_compatibility.rst │ ├── readme_credits.rst │ ├── readme_features.rst │ └── readme_title.rst ├── hangman ├── __init__.py ├── __main__.py ├── _compat.py ├── controller.py ├── model.py ├── utils.py └── view.py ├── requirements.in ├── requirements.txt ├── requirements_dev.in ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_controller.py ├── test_flash_message.py ├── test_hangman.py ├── test_main.py ├── test_view.py └── test_word_bank.py ├── tox.ini └── travis_pypi_setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | ; vi:syntax=ini 2 | ; See https://coverage.readthedocs.org/en/coverage-4.0.3/config.html 3 | 4 | [run] 5 | data_file = .coverage 6 | source = 7 | hangman 8 | 9 | ; debug = config 10 | ; branch = False 11 | ; include = 12 | ; hangman/ 13 | ; omit = 14 | ; tests/ 15 | 16 | [report] 17 | fail_under = 80 18 | show_missing = True 19 | 20 | ; exclude_lines = 21 | ; (?i)#\s*pragma[:\s]?\s*no\s*cover 22 | ; include_errors = False 23 | ; include = 24 | ; hangman/ 25 | ; omit = 26 | ; tests/ 27 | ; skip_covered = False 28 | 29 | [html] 30 | directory = .htmlcov 31 | title = Hangman Coverage Report 32 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | line_length = 120 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | charset = utf-8 12 | end_of_line = lf 13 | 14 | [*.bat] 15 | indent_style = tab 16 | end_of_line = crlf 17 | 18 | [LICENSE] 19 | insert_final_newline = false 20 | 21 | [Makefile] 22 | indent_style = tab 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | .htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # Pycharm 60 | .idea/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | # pip wheels 66 | wheelhouse/ 67 | -------------------------------------------------------------------------------- /.requirements.diff: -------------------------------------------------------------------------------- 1 | diff --git a/requirements_dev.txt b/requirements_dev.txt 2 | index b8e5955..918f60b 100644 3 | --- a/requirements_dev.txt 4 | +++ b/requirements_dev.txt 5 | @@ -12,7 +12,7 @@ cffi==1.4.1 # via cryptography 6 | click==6.2 # via pip-tools 7 | coverage==4.0.3 8 | cryptography==1.1.2 9 | -docutils==0.12 # via sphinx 10 | +docutils==0.12 # via restructuredtext-lint, sphinx 11 | first==2.0.1 # via pip-tools 12 | flake8==2.5.1 13 | idna==2.0 # via cryptography 14 | @@ -33,6 +33,7 @@ pygments==2.0.2 # via sphinx 15 | pytest==2.8.5 16 | pytz==2015.7 # via babel 17 | pyyaml==3.11 18 | +restructuredtext-lint==0.14.0 19 | six==1.10.0 # via cryptography, mock, pip-tools, sphinx 20 | snowballstemmer==1.2.0 # via sphinx 21 | sphinx-rtd-theme==0.1.9 # via sphinx 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | # This file will be regenerated if you run travis_pypi_setup.py 3 | 4 | language: python 5 | 6 | python: 7 | - "3.5" 8 | 9 | install: 10 | - pip install tox coveralls 11 | 12 | env: 13 | - TOXENV=py26 14 | - TOXENV=py27 15 | - TOXENV=py33 16 | - TOXENV=py34 17 | - TOXENV=py35 18 | - TOXENV=pypy 19 | - TOXENV=docs 20 | 21 | # command to run tests, e.g. python setup.py test 22 | script: 23 | - tox -e $TOXENV 24 | - coverage run setup.py test 25 | 26 | after_success: 27 | - coveralls 28 | 29 | # After you create the Github repo and add it to Travis, run the 30 | # travis_pypi_setup.py script to finish PyPI deployment setup 31 | deploy: 32 | provider: pypi 33 | distributions: sdist bdist_wheel 34 | user: bionikspoon 35 | password: 36 | secure: dMZxOD6Wg36X2txHY6/5solsCpkMeoiiC/s/jIJaV0T65tVsRjDTwOzyaV8evTrYmq4ZtvUpQhQnH0A1EtFNKzoZV8wlV2ohdJdYhI4AVI5DWXLNsnVcxQ3RpQT2HZAJ0FwnozIZpN8sntkG0kCO2IYtJEMWH+/SJHsEm5oy+Pk= 37 | on: 38 | tags: true 39 | repo: bionikspoon/python_hangman 40 | condition: $TOXENV == py34 41 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Credits 2 | ======= 3 | 4 | Development Lead 5 | ---------------- 6 | 7 | * Manu Phatak 8 | 9 | Contributors 10 | ------------ 11 | 12 | None yet. Why not be the first? 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. START Source defined in docs/github_docs.py 2 | 3 | 4 | .. This document was procedurally generated by docs/github_docs.py on Friday, December 18, 2015 5 | 6 | 7 | .. END Source defined in docs/github_docs.py 8 | .. START Source defined in docs/github_docs.py 9 | 10 | 11 | .. role:: mod(literal) 12 | .. role:: func(literal) 13 | .. role:: data(literal) 14 | .. role:: const(literal) 15 | .. role:: class(literal) 16 | .. role:: meth(literal) 17 | .. role:: attr(literal) 18 | .. role:: exc(literal) 19 | .. role:: obj(literal) 20 | .. role:: envvar(literal) 21 | 22 | 23 | .. END Source defined in docs/github_docs.py 24 | .. START Source defined in docs/source/contributing.rst 25 | 26 | 27 | Contributing 28 | ============ 29 | 30 | Contributions are welcome, and they are greatly appreciated! Every 31 | little bit helps, and credit will always be given. 32 | 33 | You can contribute in many ways: 34 | 35 | Types of Contributions 36 | ---------------------- 37 | 38 | Report Bugs 39 | ~~~~~~~~~~~ 40 | 41 | Report bugs at https://github.com/bionikspoon/python_hangman/issues. 42 | 43 | If you are reporting a bug, please include: 44 | 45 | * Your operating system name and version. 46 | * Any details about your local setup that might be helpful in troubleshooting. 47 | * Detailed steps to reproduce the bug. 48 | 49 | Fix Bugs 50 | ~~~~~~~~ 51 | 52 | Look through the GitHub issues for bugs. Anything tagged with "bug" 53 | is open to whoever wants to implement it. 54 | 55 | Implement Features 56 | ~~~~~~~~~~~~~~~~~~ 57 | 58 | Look through the GitHub issues for features. Anything tagged with "feature" 59 | is open to whoever wants to implement it. 60 | 61 | Write Documentation 62 | ~~~~~~~~~~~~~~~~~~~ 63 | 64 | python_hangman could always use more documentation, whether as part of the 65 | official python_hangman docs, in docstrings, or even on the web in blog posts, 66 | articles, and such. 67 | 68 | Submit Feedback 69 | ~~~~~~~~~~~~~~~ 70 | 71 | The best way to send feedback is to file an issue at https://github.com/bionikspoon/python_hangman/issues. 72 | 73 | If you are proposing a feature: 74 | 75 | * Explain in detail how it would work. 76 | * Keep the scope as narrow as possible, to make it easier to implement. 77 | * Remember that this is a volunteer-driven project, and that contributions 78 | are welcome :) 79 | 80 | Get Started! 81 | ------------ 82 | 83 | Ready to contribute? Here's how to set up `python_hangman` for local development. 84 | 85 | 1. Fork the `python_hangman` repo on GitHub. 86 | 2. Clone your fork locally 87 | 88 | .. code-block:: shell 89 | 90 | $ git clone git@github.com:your_name_here/python_hangman.git 91 | 92 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development 93 | 94 | .. code-block:: shell 95 | 96 | $ mkvirtualenv python_hangman 97 | $ cd python_hangman/ 98 | $ python setup.py develop 99 | 100 | 4. Create a branch for local development 101 | 102 | .. code-block:: shell 103 | 104 | $ git checkout -b feature/name-of-your-feature 105 | $ git checkout -b hotfix/name-of-your-bugfix 106 | 107 | Now you can make your changes locally. 108 | 109 | 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox 110 | 111 | .. code-block:: shell 112 | 113 | $ flake8 hangman tests 114 | $ python setup.py test 115 | $ tox 116 | 117 | To get flake8 and tox, just pip install them into your virtualenv. 118 | 119 | 6. Commit your changes and push your branch to GitHub 120 | 121 | .. code-block:: shell 122 | 123 | $ git add . 124 | $ git commit -m "Your detailed description of your changes." 125 | $ git push origin name-of-your-bugfix-or-feature 126 | 127 | 7. Submit a pull request through the GitHub website. 128 | 129 | Pull Request Guidelines 130 | ----------------------- 131 | 132 | Before you submit a pull request, check that it meets these guidelines: 133 | 134 | 1. The pull request should include tests. 135 | 2. If the pull request adds functionality, the docs should be updated. Put 136 | your new functionality into a function with a docstring, and add the 137 | feature to the list in README.rst. 138 | 3. The pull request should work for Python 2.6, 2.7, 3.3, 3.4, 3.5, and PyPy. Check 139 | https://travis-ci.org/bionikspoon/python_hangman/pull_requests 140 | and make sure that the tests pass for all supported Python versions. 141 | 142 | Tips 143 | ---- 144 | 145 | To run a subset of tests 146 | 147 | .. code-block:: shell 148 | 149 | $ py.test tests/test_hangman.py 150 | 151 | 152 | .. END Source defined in docs/source/contributing.rst 153 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | History 2 | ======= 3 | 4 | Next Release 5 | ------------ 6 | 7 | * Stay Posted 8 | 9 | 2.2.0 (2015-18-05) 10 | ------------------ 11 | 12 | * Fixed max recursion issue with game loop. 13 | * Updated requirements. 14 | * Removed gratuitous docs -- less is more. 15 | * 2.2.1 Handle ctrl+d EOF to exit. 16 | * 2.2.2 Fix broken coverage report. 17 | 18 | 19 | 2.1.0 (2015-18-05) 20 | ------------------ 21 | 22 | * Updated docs, divided and automated in a more reasonable way. 23 | * renamed the github repo to mirror pypi name. 24 | * 2.1.1 Fix pypi's rst render 25 | 26 | 27 | 2.0.0 (2015-12-05) 28 | ------------------ 29 | 30 | * Establishing a changelog. 31 | * Massive refactoring, explicit MVC structure. 32 | * Code is even more idiomatic! 33 | * Created a `FlashMessage` utility. 34 | * Removed poorly implemented classes in favor of stateless functions. 35 | * Add, Remove support for py35, py32. 36 | * 100% code coverage. (2 untestable, inconsequential lines ignored) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Manu Phatak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-build clean-pyc clean-test clean-docs lint test test-all coverage coverage github docs builddocs servedocs release dist install register requirements 2 | define BROWSER_PYSCRIPT 3 | import os, webbrowser, sys 4 | try: 5 | from urllib import pathname2url 6 | except: 7 | from urllib.request import pathname2url 8 | 9 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 10 | endef 11 | export BROWSER_PYSCRIPT 12 | 13 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 14 | DOCSBUILDDIR = docs/_build 15 | DOCSSOURCEDIR = docs/source 16 | 17 | help: 18 | @echo "clean remove all build, test, coverage and Python artifacts" 19 | @echo "clean-build remove build artifacts" 20 | @echo "clean-pyc remove Python file artifacts" 21 | @echo "clean-test remove test and coverage artifacts" 22 | @echo "clean-docs remove autogenerated docs files" 23 | @echo "lint check style with flake8" 24 | @echo "test run tests quickly with the default Python" 25 | @echo "test-all run tests on every Python version with tox" 26 | @echo "coverage check code coverage quickly with the default Python" 27 | @echo "github generate github's docs (i.e. README)" 28 | @echo "docs generate Sphinx HTML documentation, including API docs" 29 | @echo "servedocs semi-live edit docs" 30 | @echo "release package and upload a release" 31 | @echo "dist package" 32 | @echo "install install the package to the active Python's site-packages" 33 | @echo "register update pypi" 34 | @echo "requirements update and install requirements" 35 | 36 | clean: clean-build clean-pyc clean-test 37 | 38 | clean-build: 39 | rm -fr $(DOCSBUILDDIR)/ 40 | rm -fr dist/ 41 | rm -fr .eggs/ 42 | find . -name '*.egg-info' -exec rm -fr {} + 43 | find . -name '*.egg' -exec rm -fr {} + 44 | 45 | clean-pyc: 46 | find . -name '*.pyc' -exec rm -f {} + 47 | find . -name '*.pyo' -exec rm -f {} + 48 | find . -name '*~' -exec rm -f {} + 49 | find . -name '__pycache__' -exec rm -fr {} + 50 | 51 | clean-test: 52 | rm -fr .tox/ 53 | rm -f .coverage 54 | rm -fr .htmlcov/ 55 | 56 | clean-docs: 57 | rm -f $(DOCSSOURCEDIR)/python_hangman.rst 58 | rm -f $(DOCSSOURCEDIR)/modules.rst 59 | $(MAKE) -C docs clean 60 | 61 | lint: 62 | flake8 hangman tests 63 | 64 | test: lint 65 | python setup.py test 66 | 67 | test-all: lint 68 | tox 69 | 70 | coverage: 71 | coverage run setup.py test 72 | coverage report 73 | coverage html 74 | $(BROWSER) .htmlcov/index.html 75 | $(MAKE) -C docs coverage 76 | 77 | github: 78 | python docs/github_docs.py 79 | rst-lint README.rst 80 | 81 | docs: clean-docs builddocs github 82 | 83 | builddocs: 84 | sphinx-apidoc \ 85 | --private \ 86 | --no-toc \ 87 | --module-first \ 88 | --no-headings \ 89 | --output-dir=$(DOCSSOURCEDIR)/ hangman 90 | $(MAKE) -C docs html 91 | 92 | servedocs: docs 93 | $(BROWSER) $(DOCSBUILDDIR)/html/index.html 94 | watchmedo shell-command \ 95 | --pattern '*.rst;*.py' \ 96 | --command '$(MAKE) builddocs' \ 97 | --ignore-pattern '$(DOCSBUILDDIR)/*;$(DOCSSOURCEDIR)/python_hangman.rst' \ 98 | --ignore-directories \ 99 | --recursive 100 | 101 | release: clean docs 102 | python setup.py sdist upload 103 | python setup.py bdist_wheel upload 104 | 105 | dist: clean docs 106 | python setup.py sdist 107 | python setup.py bdist_wheel 108 | ls -l dist 109 | 110 | install: clean 111 | python setup.py install 112 | 113 | register: 114 | python setup.py register 115 | 116 | requirements: 117 | pip install --quiet --upgrade setuptools pip wheel pip-tools 118 | pip-compile requirements_dev.in > /dev/null 119 | pip-compile requirements.in > /dev/null 120 | pip-sync requirements_dev.txt > /dev/null 121 | pip install --quiet -r requirements.txt 122 | pip wheel --quiet -r requirements_dev.txt 123 | pip wheel --quiet -r requirements.txt 124 | git diff requirements.txt requirements_dev.txt 2>&1 | tee .requirements.diff 125 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. START Source defined in docs/github_docs.py 2 | 3 | 4 | .. This document was procedurally generated by docs/github_docs.py on Friday, December 18, 2015 5 | 6 | 7 | .. END Source defined in docs/github_docs.py 8 | .. START Source defined in docs/github_docs.py 9 | 10 | 11 | .. role:: mod(literal) 12 | .. role:: func(literal) 13 | .. role:: data(literal) 14 | .. role:: const(literal) 15 | .. role:: class(literal) 16 | .. role:: meth(literal) 17 | .. role:: attr(literal) 18 | .. role:: exc(literal) 19 | .. role:: obj(literal) 20 | .. role:: envvar(literal) 21 | 22 | 23 | .. END Source defined in docs/github_docs.py 24 | .. START Source defined in docs/source/readme_title.rst 25 | 26 | ============== 27 | python_hangman 28 | ============== 29 | 30 | .. image:: https://badge.fury.io/py/python_hangman.svg 31 | :target: https://pypi.python.org/pypi/python_hangman/ 32 | :alt: Latest Version 33 | 34 | .. image:: https://img.shields.io/pypi/status/python_hangman.svg 35 | :target: https://pypi.python.org/pypi/python_hangman/ 36 | :alt: Development Status 37 | 38 | .. image:: https://travis-ci.org/bionikspoon/python_hangman.svg?branch=develop 39 | :target: https://travis-ci.org/bionikspoon/python_hangman?branch=develop 40 | :alt: Build Status 41 | 42 | .. image:: https://coveralls.io/repos/bionikspoon/python_hangman/badge.svg?branch=develop 43 | :target: https://coveralls.io/github/bionikspoon/python_hangman?branch=develop&service=github 44 | :alt: Coverage Status 45 | 46 | .. image:: https://readthedocs.org/projects/python-hangman/badge/?version=develop 47 | :target: https://python-hangman.readthedocs.org/en/develop/?badge=develop 48 | :alt: Documentation Status 49 | 50 | 51 | 52 | 53 | A well tested, cli, python version-agnostic, multi-platform hangman game. It's built following a TDD workflow and a MVC design pattern. Each component services a sensibly distinct logical purpose. Python Hangman is a version agnostic, tox tested, travis-backed program! Documented and distributed. 54 | 55 | 56 | .. END Source defined in docs/source/readme_title.rst 57 | .. START Source defined in docs/source/readme_features.rst 58 | 59 | Features 60 | ======== 61 | 62 | - Hangman! 63 | - Documentation: https://python_hangman.readthedocs.org 64 | - Open Source: https://github.com/bionikspoon/python_hangman 65 | - Idiomatic code. 66 | - Thoroughly tested with very high coverage. 67 | - Python version agnostic. 68 | - Demonstrates MVC design out of the scope of web development. 69 | - MIT license 70 | 71 | .. image:: https://cloud.githubusercontent.com/assets/5052422/11611464/00822c5c-9b95-11e5-9fcb-8c10fd9be7df.jpg 72 | :alt: Screenshot 73 | 74 | 75 | .. END Source defined in docs/source/readme_features.rst 76 | .. START Source defined in docs/source/readme_compatibility.rst 77 | 78 | Compatibility 79 | ============= 80 | 81 | .. image:: https://img.shields.io/badge/Python-2.6,_2.7,_3.3,_3.4,_3.5,_pypy-brightgreen.svg 82 | :target: https://pypi.python.org/pypi/python_hangman/ 83 | :alt: Supported Python versions 84 | 85 | 86 | - Python 2.6 87 | - Python 2.7 88 | - Python 3.3 89 | - Python 3.4 90 | - Python 3.5 91 | - PyPy 92 | 93 | 94 | .. END Source defined in docs/source/readme_compatibility.rst 95 | .. START Source defined in docs/source/installation.rst 96 | 97 | 98 | Installation 99 | ============ 100 | 101 | At the command line either via easy_install or pip 102 | 103 | .. code-block:: shell 104 | 105 | $ mkvirtualenv hangman # optional for venv users 106 | $ pip install python_hangman 107 | $ hangman 108 | 109 | 110 | **Uninstall** 111 | 112 | .. code-block:: shell 113 | 114 | $ pip uninstall python_hangman 115 | 116 | 117 | .. END Source defined in docs/source/installation.rst 118 | .. START Source defined in docs/source/goals.rst 119 | 120 | Goals 121 | ===== 122 | 123 | 2.0.0 124 | ----- 125 | 126 | **MVC pattern**. The goal was to explicitly demonstrate an MVC pattern out of the scope of web development. 127 | 128 | **Idiomatic code**. In this overhaul there's a big emphasis on idiomatic code. The code should be describing its' own intention with the clarity your grandmother could read. 129 | 130 | 131 | 1.0.0 132 | ----- 133 | 134 | Learning! This was a Test Driven Development(TDD) exercise. 135 | 136 | Also, explored: 137 | 138 | - Tox, test automation 139 | - Travis CI 140 | - Python version agnostic programming 141 | - Setuptools 142 | - Publishing on pip 143 | - Coverage via coveralls 144 | - Documentation with sphinx and ReadTheDocs 145 | - Cookiecutter development 146 | 147 | 148 | .. END Source defined in docs/source/goals.rst 149 | .. START Source defined in docs/source/design.rst 150 | 151 | Design 152 | ====== 153 | 154 | This game roughly follows the **Model-View-Controller(MVC)** pattern. In the latest overhaul, these roles have been explicitly named: :mod:`hangman.model`, :mod:`hangman.view`, :mod:`hangman.controller`. 155 | 156 | Traditionally in MVC the ``controller`` is the focal point. It tells the ``view`` what information to collect from the user and what to show. It uses that information to communicate with the ``model``--also, the data persistence later--and determine the next step. This Hangman MVC adheres to these principals 157 | 158 | Model 159 | ----- 160 | 161 | The model is very simply the hangman game instance--:class:`hangman.model.Hangman`. It's a class. Every class should have "state" and the methods of that class should manage that state. In this case, the "state" is the current "state of the game". The public API are for managing that state. 162 | 163 | The entirety of the game logic is contained in :class:`hangman.model.Hangman`. You could technically play the game in the python console by instantiating the class, submitting guesses with the method :meth:`hangman.model.Hangman.guess` and printing the game state. 164 | 165 | For example: 166 | 167 | 168 | .. code-block:: python 169 | 170 | >>> from hangman.model import Hangman 171 | >>> game = Hangman(answer='hangman') 172 | >>> game.guess('a') 173 | hangman(status='_A___A_', misses=[], remaining_turns=10) 174 | 175 | >>> game.guess('n').guess('z').guess('e') 176 | hangman(status='_AN__AN', misses=['E', 'Z'], remaining_turns=8) 177 | 178 | >>> game.status 179 | '_AN__AN' 180 | 181 | >>> game.misses 182 | ['E', 'Z'] 183 | 184 | >>> game.remaining_turns 185 | 8 186 | 187 | 188 | View 189 | ---- 190 | 191 | :mod:`hangman.view` is a collection of stateless functions that represent the presentation layer. When called these functions handles printing the art to the console, and collecting input from the user. 192 | 193 | Controller 194 | ---------- 195 | 196 | In this program, the ``controller`` is actually the "game_loop"--:func:`hangman.controller.game_loop`. I still think of it as a ``controller`` because the role it plays--communicating I/O from the view with the model-persistence layer. 197 | 198 | The controller tells the view later what to print and what data to collect. It uses that information update the state of the game (model) and handle game events. 199 | 200 | 201 | .. END Source defined in docs/source/design.rst 202 | .. START Source defined in docs/source/readme_call_diagram.rst 203 | 204 | Call Diagram 205 | ============ 206 | 207 | .. image:: https://cloud.githubusercontent.com/assets/5052422/11611800/bfc9ec20-9ba5-11e5-9b18-95d361e7ba23.png 208 | :alt: Call Diagram 209 | 210 | 211 | .. END Source defined in docs/source/readme_call_diagram.rst 212 | .. START Source defined in docs/source/readme_credits.rst 213 | 214 | Credits 215 | ======= 216 | 217 | Tools used in rendering this package: 218 | 219 | * Cookiecutter_ 220 | * `bionikspoon/cookiecutter-pypackage`_ forked from `audreyr/cookiecutter-pypackage`_ 221 | 222 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 223 | .. _`bionikspoon/cookiecutter-pypackage`: https://github.com/bionikspoon/cookiecutter-pypackage 224 | .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage 225 | 226 | 227 | .. END Source defined in docs/source/readme_credits.rst 228 | -------------------------------------------------------------------------------- /charts/README.md: -------------------------------------------------------------------------------- 1 | # Call Charts 2 | uses `pycallgraph` and Graphviz to generate visual representation of the calls throughout the game. 3 | 4 | It uses the `Mock` library to patch the user input. Currently it's set to run the program 100/10 times, playing 10 games each. 5 | 6 | ## Charts 7 | 8 | 9 | ![dot](images/basic-1000-dot-v2.0.0.png) 10 | 11 | ![neato](images/basic-100-neato-v2.0.0.png) 12 | 13 | ![fdp](images/basic-100-fdp-v2.0.0.png) 14 | 15 | 16 | 17 | ![circo](images/basic-100-circo-v2.0.0.png) 18 | 19 | ![twopi](images/basic-100-twopi-v2.0.0.png) 20 | -------------------------------------------------------------------------------- /charts/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | -------------------------------------------------------------------------------- /charts/charts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import string 3 | import sys 4 | from random import choice 5 | 6 | from mock import patch 7 | from pycallgraph import (PyCallGraph, Config, GlobbingFilter, PyCallGraphException) 8 | from pycallgraph.output import GraphvizOutput 9 | 10 | from hangman import __version__ as version 11 | from hangman.controller import game_loop 12 | 13 | 14 | class LocalConfig: 15 | ITERATIONS = 100 16 | CHUNKS = 10 17 | ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 18 | 19 | 20 | def patch_getchar(): 21 | def random_letter(): 22 | return choice(string.ascii_uppercase) 23 | 24 | return random_letter 25 | 26 | 27 | def patch_confirm(): 28 | while True: 29 | for _ in list(range(LocalConfig.CHUNKS)): 30 | yield True 31 | yield False 32 | 33 | 34 | def graphiz_setup(tool='dot'): 35 | graphviz = GraphvizOutput() 36 | graphviz.output_file = os.path.join(LocalConfig.ROOT, 'charts', 'images', 37 | 'basic-{}-{}-v{}.png'.format(LocalConfig.ITERATIONS, tool, version)) 38 | graphviz.tool = tool 39 | return graphviz 40 | 41 | 42 | def config_setup(): 43 | config = Config() 44 | config.trace_filter = GlobbingFilter(include=['hangman.*']) 45 | return config 46 | 47 | 48 | def main(tool='dot'): 49 | graphviz = graphiz_setup(tool) 50 | config = config_setup() 51 | 52 | # Patch click console prompts. Patch click printing 53 | with patch('click.getchar') as getchar, patch('click.confirm') as confirm, patch('click.echo'), patch( 54 | 'click.secho'), patch('click.clear'): 55 | getchar.side_effect = patch_getchar() 56 | confirm.side_effect = patch_confirm() 57 | 58 | with PyCallGraph(output=graphviz, config=config): 59 | print('Begin: Building <%r> call chart...' % tool) 60 | for i in range(LocalConfig.ITERATIONS // LocalConfig.CHUNKS): 61 | game_loop() 62 | print('%4s trials completed' % ((i + 1) * LocalConfig.CHUNKS)) 63 | print('End: Done building <%r> call chart.' % tool) 64 | 65 | 66 | if __name__ == '__main__': 67 | # add project folder to PATH 68 | sys.path.append(LocalConfig.ROOT) 69 | 70 | LocalConfig.ITERATIONS = 1000 71 | try: 72 | main('dot') 73 | except PyCallGraphException as e: 74 | print('<%r> Failed.' % tool) 75 | print(e) 76 | 77 | LocalConfig.ITERATIONS = 100 78 | for tool in ['dot', 'neato', 'fdp', 'sfdp', 'twopi', 'circo']: 79 | try: 80 | main(tool) 81 | except PyCallGraphException as e: 82 | print('<%r> Failed.' % tool) 83 | print(e) 84 | -------------------------------------------------------------------------------- /charts/images/basic-100-circo-v1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-100-circo-v1.0.0.png -------------------------------------------------------------------------------- /charts/images/basic-100-circo-v2.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-100-circo-v2.0.0.png -------------------------------------------------------------------------------- /charts/images/basic-100-dot-v0.1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-100-dot-v0.1.0.png -------------------------------------------------------------------------------- /charts/images/basic-100-dot-v1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-100-dot-v1.0.0.png -------------------------------------------------------------------------------- /charts/images/basic-100-dot-v2.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-100-dot-v2.0.0.png -------------------------------------------------------------------------------- /charts/images/basic-100-fdp-v1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-100-fdp-v1.0.0.png -------------------------------------------------------------------------------- /charts/images/basic-100-fdp-v2.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-100-fdp-v2.0.0.png -------------------------------------------------------------------------------- /charts/images/basic-100-neato-v0.1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-100-neato-v0.1.0.png -------------------------------------------------------------------------------- /charts/images/basic-100-neato-v1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-100-neato-v1.0.0.png -------------------------------------------------------------------------------- /charts/images/basic-100-neato-v2.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-100-neato-v2.0.0.png -------------------------------------------------------------------------------- /charts/images/basic-100-sfdp-v1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-100-sfdp-v1.0.0.png -------------------------------------------------------------------------------- /charts/images/basic-100-sfdp-v2.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-100-sfdp-v2.0.0.png -------------------------------------------------------------------------------- /charts/images/basic-100-twopi-v1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-100-twopi-v1.0.0.png -------------------------------------------------------------------------------- /charts/images/basic-100-twopi-v2.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-100-twopi-v2.0.0.png -------------------------------------------------------------------------------- /charts/images/basic-1000-dot-v0.1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-1000-dot-v0.1.0.png -------------------------------------------------------------------------------- /charts/images/basic-1000-dot-v1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-1000-dot-v1.0.0.png -------------------------------------------------------------------------------- /charts/images/basic-1000-dot-v2.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-1000-dot-v2.0.0.png -------------------------------------------------------------------------------- /charts/images/basic-1000-neato-v0.1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-1000-neato-v0.1.0.png -------------------------------------------------------------------------------- /charts/images/basic-circo-v0.1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-circo-v0.1.0.png -------------------------------------------------------------------------------- /charts/images/basic-dot-v0.1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-dot-v0.1.0.png -------------------------------------------------------------------------------- /charts/images/basic-fdp-v0.1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-fdp-v0.1.0.png -------------------------------------------------------------------------------- /charts/images/basic-neato-v0.1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-neato-v0.1.0.png -------------------------------------------------------------------------------- /charts/images/basic-twopi-v0.1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/charts/images/basic-twopi-v0.1.0.png -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python_hangman.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python_hangman.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/python_hangman" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python_hangman" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | coverage: 170 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 171 | @echo "Testing of coverage in the sources finished, look at the " \ 172 | "results in $(BUILDDIR)/coverage/python.txt." 173 | 174 | xml: 175 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 176 | @echo 177 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 178 | 179 | pseudoxml: 180 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 181 | @echo 182 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 183 | -------------------------------------------------------------------------------- /docs/github_docs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | """ 4 | Used by ``make readme`` to generate github readme from rst docs. 5 | 6 | Pipeline: routine(`include`, `out_file`) 7 | 8 | * `include` - include files and inline 'heredocs' 9 | * concatenate - flatten documents 10 | * sanitize - modify content 11 | * rule__code_blocks - - re implement rst code-blocks for github highlighting 12 | * rule__everything_else - - various one liners, replace non-standard directives 13 | * write_file(`out_file`) - 14 | * notify(`out_file`) - print a progress bar 15 | """ 16 | from __future__ import print_function 17 | 18 | from datetime import datetime 19 | from functools import partial, reduce 20 | from os.path import dirname, realpath, join, relpath 21 | 22 | 23 | # CONFIG UTILS 24 | # ---------------------------------------------------------------------------- 25 | def path(*args): 26 | return realpath(join(*args)) 27 | 28 | 29 | # CONFIG 30 | # ---------------------------------------------------------------------------- 31 | # PATH FUNCTIONS 32 | DOCS = partial(path, dirname(__file__)) 33 | SOURCE = partial(DOCS, 'source') 34 | PROJECT = partial(DOCS, '..') 35 | 36 | # CONSTANTS 37 | README = PROJECT('README.rst') 38 | CONTRIBUTING = PROJECT('CONTRIBUTING.rst') 39 | TODAY = datetime.now().strftime('%A, %B %d, %Y') # %A, %B %d, %Y -> Friday, December 11, 2015 40 | FILE_HEADER = '.. START Source defined in %s\n\n' 41 | FILE_FOOTER = '\n\n.. END Source defined in %s' 42 | 43 | # SOURCE DEFINITIONS 44 | # ---------------------------------------------------------------------------- 45 | 46 | # comment to redirect contributors 47 | comment_line = """ 48 | .. This document was procedurally generated by %s on %s 49 | """ % (__file__, TODAY) 50 | 51 | # built in rst mechanic to deal with nonstandard roles 52 | role_overrides = """ 53 | .. role:: mod(literal) 54 | .. role:: func(literal) 55 | .. role:: data(literal) 56 | .. role:: const(literal) 57 | .. role:: class(literal) 58 | .. role:: meth(literal) 59 | .. role:: attr(literal) 60 | .. role:: exc(literal) 61 | .. role:: obj(literal) 62 | .. role:: envvar(literal) 63 | """ 64 | 65 | 66 | def include_readme_docs(_=None): 67 | yield read_text(comment_line) 68 | yield read_text(role_overrides) 69 | yield read_source('readme_title.rst') 70 | yield read_source('readme_features.rst') 71 | yield read_source('readme_compatibility.rst') 72 | yield read_source('installation.rst') 73 | yield read_source('goals.rst') 74 | yield read_source('design.rst') 75 | yield read_source('readme_call_diagram.rst') 76 | yield read_source('readme_credits.rst') 77 | 78 | 79 | def include_contributing_docs(_=None): 80 | yield read_text(comment_line) 81 | yield read_text(role_overrides) 82 | yield read_source('contributing.rst') 83 | 84 | 85 | # PRE COMPOSED PARTIALS 86 | # ---------------------------------------------------------------------------- 87 | def read_source(file_name): 88 | return read_file(SOURCE(file_name)) 89 | 90 | 91 | # PROCESS PIPELINE 92 | # ---------------------------------------------------------------------------- 93 | def read_file(file_name): 94 | yield FILE_HEADER % relpath(file_name, PROJECT()) 95 | with open(file_name) as f: 96 | for line in f: 97 | yield line 98 | yield FILE_FOOTER % relpath(file_name, PROJECT()) 99 | 100 | 101 | def read_text(text): 102 | yield FILE_HEADER % relpath(__file__, PROJECT()) 103 | for line in text.splitlines(True): 104 | yield line 105 | yield FILE_FOOTER % relpath(__file__, PROJECT()) 106 | 107 | 108 | def concatenate(sources): 109 | for source in sources: 110 | for line in source: 111 | yield line 112 | yield '\n' 113 | 114 | 115 | def sanitize(lines): 116 | rules = rule__code_blocks, rule__everything_else 117 | return pipeline(rules, lines) 118 | 119 | 120 | def write_file(file_name): 121 | def write_lines(lines): 122 | with open(file_name, 'w') as f: 123 | for line in lines: 124 | yield f.write(line) 125 | 126 | return write_lines 127 | 128 | 129 | def notify(file_name): 130 | # Print messages for start and finish; draw a simple progress bar 131 | def print_notify(lines): 132 | print('Writing', relpath(file_name, PROJECT()), end='') 133 | for i, line in enumerate(lines): 134 | if i % 10 is 0: 135 | print('.', end='') 136 | yield line 137 | print('Done!') 138 | 139 | return print_notify 140 | 141 | 142 | # SANITIZE RULES 143 | # ---------------------------------------------------------------------------- 144 | def rule__code_blocks(lines): 145 | # Replace highlight directive with code blocks 146 | 147 | code_block_language = 'python' 148 | 149 | for line in lines: 150 | # named conditions 151 | is_new_file = line.startswith(FILE_HEADER.replace('%s', '').rstrip()) 152 | is_code_block_shorthand = line.endswith('::\n') and not line.strip().startswith('..') 153 | 154 | # set highlight language and remove directive 155 | if line.startswith('.. highlight:: '): 156 | _, code_block_language = line.rstrip().rsplit(' ', 1) # parse and set language 157 | continue # remove highlight directive 158 | 159 | # reset highlight language to default 160 | if is_new_file: 161 | code_block_language = 'python' 162 | 163 | # write code block directive 164 | if is_code_block_shorthand: 165 | yield line.replace('::\n', '\n') # remove the shorthand 166 | yield '\n.. code-block:: %s\n' % code_block_language # space out new directive 167 | continue 168 | 169 | yield line 170 | 171 | 172 | def rule__everything_else(lines): 173 | # add small rules here, or create a named rule. 174 | 175 | for line in lines: 176 | 177 | # remove orphan directive. 178 | if line.startswith(':orphan:'): 179 | continue 180 | 181 | if line.startswith('.. currentmodule::'): 182 | continue 183 | 184 | yield line 185 | 186 | 187 | # SCRIPT UTILS 188 | # ---------------------------------------------------------------------------- 189 | def pipeline(steps, initial=None): 190 | """ 191 | Chain results from a list of functions. Inverted reduce. 192 | 193 | :param (function) steps: List of function callbacks 194 | :param initial: Starting value for pipeline. 195 | """ 196 | 197 | def apply(result, step): 198 | return step(result) 199 | 200 | return reduce(apply, steps, initial) 201 | 202 | 203 | # RUN SCRIPT 204 | # ---------------------------------------------------------------------------- 205 | if __name__ == '__main__': 206 | def routine(include, out_file): 207 | steps = include, concatenate, sanitize, write_file(out_file), notify(out_file) 208 | list(pipeline(steps)) 209 | 210 | 211 | routine(include_readme_docs, README) 212 | routine(include_contributing_docs, CONTRIBUTING) 213 | -------------------------------------------------------------------------------- /docs/source/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/docs/source/_static/.gitkeep -------------------------------------------------------------------------------- /docs/source/_static/basic-1000-dot-v1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/docs/source/_static/basic-1000-dot-v1.0.0.png -------------------------------------------------------------------------------- /docs/source/_static/basic-1000-dot-v2.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/docs/source/_static/basic-1000-dot-v2.0.0.png -------------------------------------------------------------------------------- /docs/source/_static/hangman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/docs/source/_static/hangman.jpg -------------------------------------------------------------------------------- /docs/source/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | import sys 4 | from os.path import abspath, relpath 5 | import sphinx.environment 6 | 7 | 8 | def _warn_node(func): 9 | def wrapper(self, msg, node): 10 | if msg.startswith('nonlocal image URI found:'): 11 | return 12 | 13 | return func(self, msg, node) 14 | 15 | return wrapper 16 | 17 | 18 | sphinx.environment.BuildEnvironment.warn_node = _warn_node(sphinx.environment.BuildEnvironment.warn_node) 19 | 20 | sys.path.insert(0, abspath(relpath('../', __file__))) 21 | 22 | import hangman 23 | 24 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', ] 25 | 26 | templates_path = ['_templates'] 27 | 28 | source_suffix = '.rst' 29 | 30 | source_encoding = 'utf-8-sig' 31 | 32 | master_doc = 'index' 33 | 34 | # General information about the project. 35 | project = 'python_hangman' 36 | copyright = '2015, Manu Phatak' 37 | author = hangman.__author__ 38 | version = hangman.__version__ 39 | release = hangman.__version__ 40 | 41 | # language = None 42 | # today = '' 43 | # today_fmt = '%B %d, %Y' 44 | exclude_patterns = ['build'] 45 | # default_role = None 46 | # add_function_parentheses = True 47 | # add_module_names = True 48 | # show_authors = False 49 | pygments_style = 'sphinx' 50 | # modindex_common_prefix = [] 51 | # keep_warnings = False 52 | 53 | viewcode_import = True 54 | # -- Options for HTML output ------------------------------------------- 55 | html_theme = 'sphinx_rtd_theme' 56 | # html_theme_options = {} 57 | # html_theme_path = [] 58 | # html_title = None 59 | # html_short_title = None 60 | # html_logo = None 61 | # html_favicon = None 62 | html_static_path = ['_static'] 63 | # html_last_updated_fmt = '%b %d, %Y' 64 | # html_use_smartypants = True 65 | # html_sidebars = {} 66 | # html_additional_pages = {} 67 | # html_domain_indices = True 68 | # html_use_index = True 69 | # html_split_index = False 70 | # html_show_sourcelink = True 71 | # html_show_sphinx = True 72 | # html_show_copyright = True 73 | # html_use_opensearch = '' 74 | # html_file_suffix = None 75 | htmlhelp_basename = 'python_hangmandoc' 76 | 77 | # -- Options for LaTeX output ------------------------------------------ 78 | 79 | latex_elements = {} 80 | # 'papersize': 'letterpaper', 81 | # 'pointsize': '10pt', 82 | # 'preamble': '', 83 | 84 | latex_documents = [( # :off 85 | 'index', 86 | 'python_hangman.tex', 87 | 'python_hangman Documentation', 88 | 'Manu Phatak', 89 | 'manual', 90 | )] # :on 91 | 92 | # latex_logo = None 93 | # latex_use_parts = False 94 | # latex_show_pagerefs = False 95 | # latex_show_urls = False 96 | # latex_appendices = [] 97 | # latex_domain_indices = True 98 | 99 | # -- Options for manual page output ------------------------------------ 100 | 101 | man_pages = [( # :off 102 | 'index', 103 | 'python_hangman', 104 | 'python_hangman Documentation', 105 | ['Manu Phatak'], 106 | 1 107 | )] # :on 108 | # man_show_urls = False 109 | 110 | # -- Options for Texinfo output ---------------------------------------- 111 | 112 | texinfo_documents = [( # :off 113 | 'index', 114 | 'python_hangman', 115 | 'python_hangman Documentation', 116 | 'Manu Phatak', 117 | 'python_hangman', 118 | 'One line description of project.', 119 | 'Miscellaneous' 120 | )] # :on 121 | 122 | # texinfo_appendices = [] 123 | # texinfo_domain_indices = True 124 | # texinfo_show_urls = 'footnote' 125 | # texinfo_no_detailmenu = False 126 | -------------------------------------------------------------------------------- /docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | Contributing 4 | ============ 5 | 6 | Contributions are welcome, and they are greatly appreciated! Every 7 | little bit helps, and credit will always be given. 8 | 9 | You can contribute in many ways: 10 | 11 | Types of Contributions 12 | ---------------------- 13 | 14 | Report Bugs 15 | ~~~~~~~~~~~ 16 | 17 | Report bugs at https://github.com/bionikspoon/python_hangman/issues. 18 | 19 | If you are reporting a bug, please include: 20 | 21 | * Your operating system name and version. 22 | * Any details about your local setup that might be helpful in troubleshooting. 23 | * Detailed steps to reproduce the bug. 24 | 25 | Fix Bugs 26 | ~~~~~~~~ 27 | 28 | Look through the GitHub issues for bugs. Anything tagged with "bug" 29 | is open to whoever wants to implement it. 30 | 31 | Implement Features 32 | ~~~~~~~~~~~~~~~~~~ 33 | 34 | Look through the GitHub issues for features. Anything tagged with "feature" 35 | is open to whoever wants to implement it. 36 | 37 | Write Documentation 38 | ~~~~~~~~~~~~~~~~~~~ 39 | 40 | python_hangman could always use more documentation, whether as part of the 41 | official python_hangman docs, in docstrings, or even on the web in blog posts, 42 | articles, and such. 43 | 44 | Submit Feedback 45 | ~~~~~~~~~~~~~~~ 46 | 47 | The best way to send feedback is to file an issue at https://github.com/bionikspoon/python_hangman/issues. 48 | 49 | If you are proposing a feature: 50 | 51 | * Explain in detail how it would work. 52 | * Keep the scope as narrow as possible, to make it easier to implement. 53 | * Remember that this is a volunteer-driven project, and that contributions 54 | are welcome :) 55 | 56 | Get Started! 57 | ------------ 58 | 59 | Ready to contribute? Here's how to set up `python_hangman` for local development. 60 | 61 | 1. Fork the `python_hangman` repo on GitHub. 62 | 2. Clone your fork locally:: 63 | 64 | $ git clone git@github.com:your_name_here/python_hangman.git 65 | 66 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 67 | 68 | $ mkvirtualenv python_hangman 69 | $ cd python_hangman/ 70 | $ python setup.py develop 71 | 72 | 4. Create a branch for local development:: 73 | 74 | $ git checkout -b feature/name-of-your-feature 75 | $ git checkout -b hotfix/name-of-your-bugfix 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: 80 | 81 | $ flake8 hangman tests 82 | $ python setup.py test 83 | $ tox 84 | 85 | To get flake8 and tox, just pip install them into your virtualenv. 86 | 87 | 6. Commit your changes and push your branch to GitHub:: 88 | 89 | $ git add . 90 | $ git commit -m "Your detailed description of your changes." 91 | $ git push origin name-of-your-bugfix-or-feature 92 | 93 | 7. Submit a pull request through the GitHub website. 94 | 95 | Pull Request Guidelines 96 | ----------------------- 97 | 98 | Before you submit a pull request, check that it meets these guidelines: 99 | 100 | 1. The pull request should include tests. 101 | 2. If the pull request adds functionality, the docs should be updated. Put 102 | your new functionality into a function with a docstring, and add the 103 | feature to the list in README.rst. 104 | 3. The pull request should work for Python 2.6, 2.7, 3.3, 3.4, 3.5, and PyPy. Check 105 | https://travis-ci.org/bionikspoon/python_hangman/pull_requests 106 | and make sure that the tests pass for all supported Python versions. 107 | 108 | Tips 109 | ---- 110 | 111 | To run a subset of tests:: 112 | 113 | $ py.test tests/test_hangman.py 114 | -------------------------------------------------------------------------------- /docs/source/design.rst: -------------------------------------------------------------------------------- 1 | Design 2 | ====== 3 | 4 | This game roughly follows the **Model-View-Controller(MVC)** pattern. In the latest overhaul, these roles have been explicitly named: :mod:`hangman.model`, :mod:`hangman.view`, :mod:`hangman.controller`. 5 | 6 | Traditionally in MVC the ``controller`` is the focal point. It tells the ``view`` what information to collect from the user and what to show. It uses that information to communicate with the ``model``--also, the data persistence later--and determine the next step. This Hangman MVC adheres to these principals 7 | 8 | Model 9 | ----- 10 | 11 | The model is very simply the hangman game instance--:class:`hangman.model.Hangman`. It's a class. Every class should have "state" and the methods of that class should manage that state. In this case, the "state" is the current "state of the game". The public API are for managing that state. 12 | 13 | The entirety of the game logic is contained in :class:`hangman.model.Hangman`. You could technically play the game in the python console by instantiating the class, submitting guesses with the method :meth:`hangman.model.Hangman.guess` and printing the game state. 14 | 15 | For example: 16 | 17 | 18 | .. code-block:: python 19 | 20 | >>> from hangman.model import Hangman 21 | >>> game = Hangman(answer='hangman') 22 | >>> game.guess('a') 23 | hangman(status='_A___A_', misses=[], remaining_turns=10) 24 | 25 | >>> game.guess('n').guess('z').guess('e') 26 | hangman(status='_AN__AN', misses=['E', 'Z'], remaining_turns=8) 27 | 28 | >>> game.status 29 | '_AN__AN' 30 | 31 | >>> game.misses 32 | ['E', 'Z'] 33 | 34 | >>> game.remaining_turns 35 | 8 36 | 37 | 38 | View 39 | ---- 40 | 41 | :mod:`hangman.view` is a collection of stateless functions that represent the presentation layer. When called these functions handles printing the art to the console, and collecting input from the user. 42 | 43 | Controller 44 | ---------- 45 | 46 | In this program, the ``controller`` is actually the "game_loop"--:func:`hangman.controller.game_loop`. I still think of it as a ``controller`` because the role it plays--communicating I/O from the view with the model-persistence layer. 47 | 48 | The controller tells the view later what to print and what data to collect. It uses that information update the state of the game (model) and handle game events. 49 | -------------------------------------------------------------------------------- /docs/source/goals.rst: -------------------------------------------------------------------------------- 1 | Goals 2 | ===== 3 | 4 | 2.0.0 5 | ----- 6 | 7 | **MVC pattern**. The goal was to explicitly demonstrate an MVC pattern out of the scope of web development. 8 | 9 | **Idiomatic code**. In this overhaul there's a big emphasis on idiomatic code. The code should be describing its' own intention with the clarity your grandmother could read. 10 | 11 | 12 | 1.0.0 13 | ----- 14 | 15 | Learning! This was a Test Driven Development(TDD) exercise. 16 | 17 | Also, explored: 18 | 19 | - Tox, test automation 20 | - Travis CI 21 | - Python version agnostic programming 22 | - Setuptools 23 | - Publishing on pip 24 | - Coverage via coveralls 25 | - Documentation with sphinx and ReadTheDocs 26 | - Cookiecutter development 27 | -------------------------------------------------------------------------------- /docs/source/hangman.rst: -------------------------------------------------------------------------------- 1 | hangman package 2 | =============== 3 | 4 | .. automodule:: hangman 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. automodule:: hangman.__main__ 13 | :members: 14 | :undoc-members: 15 | :show-inheritance: 16 | 17 | .. automodule:: hangman.controller 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | .. automodule:: hangman.model 23 | :members: 24 | :undoc-members: 25 | :show-inheritance: 26 | 27 | .. automodule:: hangman.utils 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | 32 | .. automodule:: hangman.view 33 | :members: 34 | :undoc-members: 35 | :show-inheritance: 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/source/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../HISTORY.rst 2 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to python_hangman's documentation! 2 | ========================================== 3 | 4 | Contents: 5 | ========= 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | Readme 11 | Installation 12 | Design 13 | Goals 14 | Contributing 15 | Authors 16 | History 17 | API Docs 18 | 19 | Feedback 20 | ======== 21 | If you have any suggestions or questions about **python_hangman** feel free to email me at bionikspoon@gmail.com. 22 | 23 | If you encounter any errors or problems with **python_hangman**, please let me know! 24 | Open an Issue at the GitHub https://github.com/bionikspoon/python_hangman main repository. 25 | 26 | Indices and tables 27 | ================== 28 | 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | Installation 4 | ============ 5 | 6 | At the command line either via easy_install or pip:: 7 | 8 | $ mkvirtualenv hangman # optional for venv users 9 | $ pip install python_hangman 10 | $ hangman 11 | 12 | 13 | **Uninstall**:: 14 | 15 | $ pip uninstall python_hangman 16 | -------------------------------------------------------------------------------- /docs/source/readme.rst: -------------------------------------------------------------------------------- 1 | .. include:: readme_title.rst 2 | 3 | .. include:: readme_features.rst 4 | 5 | .. include:: readme_compatibility.rst 6 | 7 | .. include:: readme_call_diagram.rst 8 | 9 | .. include:: readme_credits.rst 10 | -------------------------------------------------------------------------------- /docs/source/readme_call_diagram.rst: -------------------------------------------------------------------------------- 1 | Call Diagram 2 | ============ 3 | 4 | .. image:: https://cloud.githubusercontent.com/assets/5052422/11611800/bfc9ec20-9ba5-11e5-9b18-95d361e7ba23.png 5 | :alt: Call Diagram 6 | -------------------------------------------------------------------------------- /docs/source/readme_compatibility.rst: -------------------------------------------------------------------------------- 1 | Compatibility 2 | ============= 3 | 4 | .. image:: https://img.shields.io/badge/Python-2.6,_2.7,_3.3,_3.4,_3.5,_pypy-brightgreen.svg 5 | :target: https://pypi.python.org/pypi/python_hangman/ 6 | :alt: Supported Python versions 7 | 8 | 9 | - Python 2.6 10 | - Python 2.7 11 | - Python 3.3 12 | - Python 3.4 13 | - Python 3.5 14 | - PyPy 15 | -------------------------------------------------------------------------------- /docs/source/readme_credits.rst: -------------------------------------------------------------------------------- 1 | Credits 2 | ======= 3 | 4 | Tools used in rendering this package: 5 | 6 | * Cookiecutter_ 7 | * `bionikspoon/cookiecutter-pypackage`_ forked from `audreyr/cookiecutter-pypackage`_ 8 | 9 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 10 | .. _`bionikspoon/cookiecutter-pypackage`: https://github.com/bionikspoon/cookiecutter-pypackage 11 | .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage 12 | -------------------------------------------------------------------------------- /docs/source/readme_features.rst: -------------------------------------------------------------------------------- 1 | Features 2 | ======== 3 | 4 | - Hangman! 5 | - Documentation: https://python_hangman.readthedocs.org 6 | - Open Source: https://github.com/bionikspoon/python_hangman 7 | - Idiomatic code. 8 | - Thoroughly tested with very high coverage. 9 | - Python version agnostic. 10 | - Demonstrates MVC design out of the scope of web development. 11 | - MIT license 12 | 13 | .. image:: https://cloud.githubusercontent.com/assets/5052422/11611464/00822c5c-9b95-11e5-9fcb-8c10fd9be7df.jpg 14 | :alt: Screenshot 15 | -------------------------------------------------------------------------------- /docs/source/readme_title.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | python_hangman 3 | ============== 4 | 5 | .. image:: https://badge.fury.io/py/python_hangman.svg 6 | :target: https://pypi.python.org/pypi/python_hangman/ 7 | :alt: Latest Version 8 | 9 | .. image:: https://img.shields.io/pypi/status/python_hangman.svg 10 | :target: https://pypi.python.org/pypi/python_hangman/ 11 | :alt: Development Status 12 | 13 | .. image:: https://travis-ci.org/bionikspoon/python_hangman.svg?branch=develop 14 | :target: https://travis-ci.org/bionikspoon/python_hangman?branch=develop 15 | :alt: Build Status 16 | 17 | .. image:: https://coveralls.io/repos/bionikspoon/python_hangman/badge.svg?branch=develop 18 | :target: https://coveralls.io/github/bionikspoon/python_hangman?branch=develop&service=github 19 | :alt: Coverage Status 20 | 21 | .. image:: https://readthedocs.org/projects/python-hangman/badge/?version=develop 22 | :target: https://python-hangman.readthedocs.org/en/develop/?badge=develop 23 | :alt: Documentation Status 24 | 25 | 26 | 27 | 28 | A well tested, cli, python version-agnostic, multi-platform hangman game. It's built following a TDD workflow and a MVC design pattern. Each component services a sensibly distinct logical purpose. Python Hangman is a version agnostic, tox tested, travis-backed program! Documented and distributed. 29 | -------------------------------------------------------------------------------- /hangman/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | """ 4 | ============== 5 | python_hangman 6 | ============== 7 | 8 | A well tested, cli, python version-agnostic, multi-platform hangman game. It's built following a TDD workflow and a 9 | MVC design pattern. Each component services a sensibly distinct logical purpose. Python Hangman is a version 10 | agnostic, tox tested, travis-backed program! Documented and distributed. 11 | 12 | """ 13 | from __future__ import absolute_import 14 | 15 | import logging 16 | 17 | from ._compat import NullHandler 18 | 19 | logging.getLogger(__name__).addHandler(NullHandler()) 20 | 21 | __author__ = 'Manu Phatak' 22 | __email__ = 'bionikspoon@gmail.com' 23 | __version__ = '2.2.2' 24 | -------------------------------------------------------------------------------- /hangman/__main__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | hangman.__main__ 4 | ~~~~~~~~~~~~~~~~ 5 | 6 | Entry point for ``hangman`` command. 7 | """ 8 | from __future__ import absolute_import 9 | 10 | import click 11 | 12 | from . import controller 13 | 14 | 15 | @click.command() 16 | def cli(): 17 | controller.run() 18 | 19 | 20 | if __name__ == '__main__': 21 | cli() # pragma: no cover 22 | -------------------------------------------------------------------------------- /hangman/_compat.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """Python 2to3 compatibility handling.""" 3 | 4 | import logging 5 | 6 | try: 7 | from logging import NullHandler 8 | except ImportError: # pragma: no cover 9 | # Python < 2.7 10 | class NullHandler(logging.Handler): 11 | def emit(self, record): 12 | pass 13 | 14 | # noinspection PyCompatibility 15 | from builtins import zip 16 | 17 | __all__ = ['NullHandler', 'zip'] 18 | -------------------------------------------------------------------------------- /hangman/controller.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | hangman.controller 4 | ~~~~~~~~~~~~~~~~~~ 5 | """ 6 | from __future__ import absolute_import 7 | 8 | from hangman.utils import FlashMessage, GameLost, GameWon, GameOverNotificationComplete 9 | from . import view 10 | from .model import Hangman 11 | 12 | 13 | def game_loop(game=Hangman(), flash=FlashMessage()): 14 | """ 15 | Run a single game. 16 | 17 | :param hangman.model.Hangman game: Hangman game instance. 18 | :param hangman.utils.FlashMessage flash: FlashMessage utility 19 | """ 20 | 21 | while True: 22 | try: 23 | # Draw -> prompt -> guess 24 | view.draw_board(game, message=flash) 25 | letter = view.prompt_guess() 26 | game.guess(letter) 27 | 28 | except GameLost: 29 | flash.game_lost = True 30 | except GameWon: 31 | flash.game_won = True 32 | except ValueError as msg: 33 | flash(msg) 34 | except GameOverNotificationComplete: # raised by view, finished drawing 35 | break 36 | 37 | 38 | # noinspection PyPep8Naming 39 | def run(game=Hangman(), flash=FlashMessage()): 40 | """ 41 | Run ``game_loop`` and handle exiting. 42 | 43 | Logic is separated from game_loop to cleanly avoid python recursion limits. 44 | 45 | :param hangman.model.Hangman game: Hangman game instance. 46 | :param hangman.utils.FlashMessage flash: FlashMessage utility 47 | """ 48 | 49 | # setup, save classes for reuse 50 | GameClass, FlashClass = game.__class__, flash.__class__ 51 | 52 | while True: 53 | try: 54 | game_loop(game=game, flash=flash) 55 | except KeyboardInterrupt: # exit immediately 56 | break 57 | 58 | if not view.prompt_play_again(): 59 | break 60 | 61 | # setup next game 62 | game, flash = GameClass(), FlashClass() 63 | 64 | return view.say_goodbye() 65 | -------------------------------------------------------------------------------- /hangman/model.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | hangman.model 4 | ~~~~~~~~~~~~~ 5 | """ 6 | from __future__ import absolute_import 7 | 8 | import re 9 | from collections import namedtuple 10 | 11 | from .utils import WordBank, GameLost, GameWon 12 | 13 | 14 | class Hangman(object): 15 | """ 16 | The the logic for managing the status of the game and raising key game related events. 17 | 18 | >>> from hangman.model import Hangman 19 | >>> game = Hangman(answer='hangman') 20 | >>> game.guess('a') 21 | hangman(status='_A___A_', misses=[], remaining_turns=10) 22 | 23 | >>> game.guess('n').guess('z').guess('e') 24 | hangman(status='_AN__AN', misses=['E', 'Z'], remaining_turns=8) 25 | 26 | >>> game.status 27 | '_AN__AN' 28 | 29 | >>> game.misses 30 | ['E', 'Z'] 31 | 32 | >>> game.remaining_turns 33 | 8 34 | """ 35 | 36 | # CLASS PROPERTIES 37 | # ------------------------------------------------------------------- 38 | 39 | MAX_TURNS = 10 40 | _re_answer_rules = re.compile('^[A-Z]{1,16}$') 41 | _re_guess_rules = re.compile('^[A-Z]$') 42 | _repr = namedtuple('hangman', ['status', 'misses', 'remaining_turns']) 43 | 44 | # CONSTRUCTOR 45 | # ------------------------------------------------------------------- 46 | def __init__(self, answer=None): 47 | 48 | if not answer: 49 | # Populate answer 50 | answer = WordBank.get() 51 | 52 | # Validate answer. 53 | if not self.is_valid_answer(answer): 54 | raise ValueError("Word must be letters A-Z") 55 | 56 | self.answer = answer.upper() 57 | self._misses = set() 58 | self._hits = set() 59 | 60 | # PUBLIC API 61 | # ------------------------------------------------------------------- 62 | def guess(self, letter): 63 | """Add letter to hits or misses.""" 64 | 65 | # validate input 66 | if not self.is_valid_guess(letter): 67 | raise ValueError('Must be a letter A-Z') 68 | 69 | # add to hits or misses 70 | is_miss = letter.upper() not in self.answer 71 | if is_miss: 72 | self._add_miss(letter) 73 | else: 74 | self._add_hit(letter) 75 | 76 | return self 77 | 78 | # INSTANCE PROPERTIES 79 | # ------------------------------------------------------------------- 80 | @property 81 | def misses(self): 82 | """List of misses.""" 83 | 84 | return sorted(list(self._misses)) 85 | 86 | @misses.setter 87 | def misses(self, letters): 88 | for letter in letters: 89 | self._add_miss(letter) 90 | 91 | @property 92 | def hits(self): 93 | """List of hits.""" 94 | 95 | return sorted(list(self._hits)) 96 | 97 | @hits.setter 98 | def hits(self, letters): 99 | for letter in letters: 100 | self._add_hit(letter) 101 | 102 | @property 103 | def remaining_turns(self): 104 | """Calculate number of turns remaining.""" 105 | 106 | return self.MAX_TURNS - len(self.misses) 107 | 108 | @property 109 | def status(self): 110 | """Build a string representation of status.""" 111 | 112 | hits = self.hits # calculated property 113 | 114 | def fill_in(letter): 115 | """Replace non-hits with `_`.""" 116 | 117 | return letter if letter in hits else '_' 118 | 119 | return ''.join(fill_in(letter) for letter in self.answer) 120 | 121 | # UTILITIES 122 | # ------------------------------------------------------------------- 123 | def _add_miss(self, value): 124 | """Add a letter to misses. Check for game over.""" 125 | 126 | self._misses.add(value.upper()) 127 | if self.remaining_turns <= 0: 128 | raise GameLost 129 | 130 | def _add_hit(self, value): 131 | """Add a letter to hits. Check for game won""" 132 | 133 | self._hits.add(value.upper()) 134 | if self._hits == set(self.answer): 135 | raise GameWon 136 | 137 | def is_valid_answer(self, word): 138 | """Validate answer. Letters only. Max:16""" 139 | 140 | word = str(word).upper() 141 | return not not self._re_answer_rules.search(word) 142 | 143 | def is_valid_guess(self, letter): 144 | """Validate guess. Letters only. Max:1""" 145 | 146 | letter = str(letter).upper() 147 | return not not self._re_guess_rules.search(letter) 148 | 149 | def __repr__(self): 150 | return repr(self._repr(self.status, self.misses, self.remaining_turns)) 151 | -------------------------------------------------------------------------------- /hangman/utils.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | hangman.utils 4 | ~~~~~~~~~~~~~ 5 | 6 | App utilities. 7 | """ 8 | from __future__ import absolute_import 9 | 10 | from random import choice 11 | 12 | __all__ = ['WordBank', 'FlashMessage', 'GameLost', 'GameWon', 'GameOverNotificationComplete'] 13 | 14 | 15 | class WordBank(object): 16 | """Default collection of words to choose from""" 17 | 18 | WORDS = ['ATTEMPT', 'DOLL', 'ELLEN', 'FLOATING', 'PRIDE', 'HEADING', 'FILM', 'KIDS', 'MONKEY', 'LUNGS', 'HABIT', 19 | 'SPIN', 'DISCUSSION', 'OFFICIAL', 'PHILADELPHIA', 'FACING', 'MARTIN', 'NORWAY', 'POLICEMAN', 'TOBACCO', 20 | 'VESSELS', 'TALES', 'VAPOR', 'INDEPENDENT', 'COOKIES', 'WEALTH', 'PENNSYLVANIA', 'EXPLANATION', 'DAMAGE', 21 | 'OCCASIONALLY', 'EXIST', 'SIMPLEST', 'PLATES', 'CANAL', 'NEIGHBORHOOD', 'PALACE', 'ADVICE', 'LABEL', 22 | 'DANNY', 'CLAWS', 'RUSH', 'CHOSE', 'EGYPT', 'POETRY', 'BREEZE', 'WOLF', 'MANUFACTURING', 'OURSELVES', 23 | 'SCARED', 'ARRANGEMENT', 'POSSIBLY', 'PROMISED', 'BRICK', 'ACRES', 'TREATED', 'SELECTION', 'POSITIVE', 24 | 'CONSTANTLY', 'SATISFIED', 'ZOO', 'CUSTOMS', 'UNIVERSITY', 'FIREPLACE', 'SHALLOW', 'INSTANT', 'SALE', 25 | 'PRACTICAL', 'SILLY', 'SATELLITES', 'SHAKING', 'ROCKY', 'SLOPE', 'CASEY', 'REMARKABLE', 'RUBBED', 26 | 'HAPPILY', 'MISSION', 'CAST', 'SHAKE', 'REQUIRE', 'DONKEY', 'EXCHANGE', 'JANUARY', 'MOUNT', 'AUTUMN', 27 | 'SLIP', 'BORDER', 'LEE', 'MELTED', 'TRAP', 'SOLAR', 'RECALL', 'MYSTERIOUS', 'SWUNG', 'CONTRAST', 'TOY', 28 | 'GRABBED', 'AUGUST', 'RELATIONSHIP', 'HUNTER', 'DEPTH', 'FOLKS', 'DEEPLY', 'IMAGE', 'STIFF', 'RHYME', 29 | 'ILLINOIS', 'SPECIES', 'ADULT', 'FINEST', 'THUMB', 'SLIGHT', 'GRANDMOTHER', 'SHOUT', 'HARRY', 30 | 'MATHEMATICS', 'MILL', 'ESSENTIAL', 'TUNE', 'FORT', 'COACH', 'NUTS', 'GARAGE', 'CALM', 'MEMORY', 'SOAP'] 31 | 32 | @classmethod 33 | def set(cls, *values): 34 | """Set word list.""" 35 | 36 | cls.WORDS = list(values) 37 | 38 | @classmethod 39 | def get(cls): 40 | """Get a random word from word list.""" 41 | 42 | return choice(cls.WORDS) 43 | 44 | 45 | class FlashMessage(object): 46 | """Basic "flash message" implementation.""" 47 | 48 | message = '' 49 | game_lost = False 50 | game_won = False 51 | 52 | def __call__(self, message): 53 | """Set message to be flashed.""" 54 | 55 | self.message = str(message) 56 | 57 | def __str__(self): 58 | """Returns and clears the message""" 59 | 60 | message, self.message = self.message, '' 61 | return str(message) 62 | 63 | def __bool__(self): 64 | # Python3 compatibility 65 | return self.__nonzero__() 66 | 67 | def __nonzero__(self): 68 | # Python2 compatibility 69 | return bool(self.message) 70 | 71 | def __eq__(self, other): 72 | return bool(self.message) == other 73 | 74 | def __format__(self, format_spec): 75 | """Format and clear flash message""" 76 | 77 | return format(str(self), format_spec) 78 | 79 | 80 | class GameWon(Exception): 81 | """Raised when answer has been guessed.""" 82 | 83 | 84 | class GameLost(Exception): 85 | """Raised when out of turns.""" 86 | 87 | 88 | class GameOverNotificationComplete(Exception): 89 | """Raised when controller should break game loop.""" 90 | -------------------------------------------------------------------------------- /hangman/view.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | hangman.view 4 | ~~~~~~~~~~~~ 5 | 6 | View layer, printing and prompting. 7 | """ 8 | from __future__ import absolute_import 9 | 10 | import click 11 | 12 | from ._compat import zip 13 | from .utils import FlashMessage, GameOverNotificationComplete 14 | 15 | 16 | # DRAW COMPONENT BLOCK 17 | # ------------------------------------------------------------------- 18 | 19 | def draw_board(game, message=FlashMessage()): 20 | """ 21 | Present the game status with pictures. 22 | 23 | - Clears the screen. 24 | - Flashes any messages. 25 | - Zip the two halves of the picture together. 26 | 27 | .. code-block:: text 28 | 29 | +---------------------------------------------+ 30 | | message 45 x 1 | 31 | +---------------------------------------------+ 32 | | title 45 x 1 | 33 | +----------+----------------------------------+ 34 | | | | 35 | | | | 36 | | | | 37 | | | | 38 | | picture | misses | 39 | | 10 x 10 | 35 x 10 | 40 | | | | 41 | | | | 42 | | | | 43 | | | | 44 | +----------+----------------------------------+ 45 | | hits 45 x 1 | 46 | +---------------------------------------------+ 47 | Dare to pick a letter: 48 | _ 49 | 50 | **Example output:** 51 | 52 | .. code-block:: text 53 | 54 | HANGMAN GAME 55 | _____ 56 | | | 57 | | 58 | | MISSES: 59 | | _ _ _ _ _ _ _ _ _ _ 60 | | 61 | | 62 | ________|_ 63 | _ _ _ _ _ _ _ 64 | Dare to pick a letter: 65 | _ 66 | 67 | 68 | :param hangman.Hangman game: game instance 69 | :param hangman.utils.FlashMessage message: flash message 70 | :raises: hangman.utils.GameOverNotificationComplete 71 | """ 72 | 73 | # setup 74 | click.clear() 75 | partial_picture = build_partial_picture(game.remaining_turns) 76 | partial_misses = build_partial_misses(game.misses) 77 | 78 | # print 79 | print_partial_message(message, game.answer) 80 | print_partial_title() 81 | print_partial_body(partial_picture, partial_misses) 82 | print_partial_hits(game.status) 83 | 84 | # raise to break game loop 85 | if message.game_lost or message.game_won: 86 | raise GameOverNotificationComplete 87 | 88 | 89 | def say_goodbye(): 90 | """Write a goodbye message.""" 91 | 92 | click.secho('Have a nice day!', bold=True, fg='green', blink=True) 93 | 94 | return print_spacer() 95 | 96 | 97 | # PROMPT USER INPUT 98 | # ------------------------------------------------------------------- 99 | def prompt_guess(): 100 | """Get a single letter.""" 101 | 102 | print_spacer() 103 | 104 | click.secho('Dare to pick a letter: ', dim=True, bold=True) 105 | letter = click.getchar() 106 | 107 | # \x03 = ctrl+c, \x04 = ctrl+d 108 | if letter in ['\x03', '\x04']: 109 | raise KeyboardInterrupt 110 | return letter 111 | 112 | 113 | def prompt_play_again(): 114 | """Prompt user to play again.""" 115 | 116 | print_spacer() 117 | return click.confirm('Double or nothings?') 118 | 119 | 120 | # BUILD PARTIAL BLOCKS 121 | # ------------------------------------------------------------------- 122 | 123 | def build_partial_picture(remaining_turns): 124 | """Generator, build the iconic hangman game status.""" 125 | 126 | yield ' _____' 127 | yield ' | |' 128 | if remaining_turns <= 9: 129 | yield ' (_) |' 130 | else: 131 | yield ' |' 132 | 133 | if remaining_turns <= 5: 134 | yield ' \|/ |' 135 | elif remaining_turns <= 6: 136 | yield ' \| |' 137 | elif remaining_turns <= 8: 138 | yield ' | |' 139 | else: 140 | yield ' |' 141 | 142 | if remaining_turns <= 7: 143 | yield ' | |' 144 | else: 145 | yield ' |' 146 | 147 | if remaining_turns <= 4: 148 | yield ' | |' 149 | else: 150 | yield ' |' 151 | 152 | if remaining_turns <= 0: 153 | yield ' _/ \_ |' 154 | elif remaining_turns <= 1: 155 | yield ' _/ \ |' 156 | elif remaining_turns <= 2: 157 | yield ' / \ |' 158 | elif remaining_turns <= 3: 159 | yield ' / |' 160 | else: 161 | yield ' |' 162 | 163 | yield '________|_' 164 | 165 | 166 | def build_partial_misses(game_misses): 167 | """Generator, build game misses block.""" 168 | 169 | misses_block = ' '.join('{0:_<10s}'.format(''.join(game_misses))) 170 | yield '' 171 | yield '' 172 | yield '' 173 | yield '{0:s}{1:s}'.format(' ' * 5, 'MISSES:') 174 | yield '{0:s}{1:s}'.format(' ' * 5, misses_block) 175 | yield '' 176 | yield '' 177 | yield '' 178 | yield '' 179 | yield '' 180 | 181 | 182 | # PRINT PARTIAL BLOCKS 183 | # ------------------------------------------------------------------- 184 | 185 | def print_partial_message(flash, answer): 186 | if flash.game_lost: 187 | message = "YOU LOSE! THE ANSWER IS {0}".format(answer) 188 | return click.secho('{0:45s}'.format(message), bold=True, fg='red') 189 | 190 | if flash.game_won: 191 | message = "YOU ARE SO COOL" 192 | return click.secho('{0:45s}'.format(message), bold=True, fg='cyan') 193 | 194 | if flash.message: 195 | return click.secho('{0:45s}'.format(flash), bold=True, fg='yellow') 196 | 197 | return print_spacer() 198 | 199 | 200 | def print_partial_title(): 201 | return click.secho('{0: ^45s}'.format('HANGMAN GAME'), bold=True, underline=True) 202 | 203 | 204 | def print_partial_body(picture, status): 205 | for line in zip(picture, status): 206 | click.echo('{0:10s}{1:35s}'.format(*line)) 207 | 208 | 209 | def print_partial_hits(game_status): 210 | # Dynamically space hits to fill line 211 | space_between_letters = ' ' if len(game_status) < 45 / 4 else ' ' 212 | formatted_game_status = space_between_letters.join(game_status) 213 | 214 | print_spacer() 215 | return click.echo('{0: ^45s}'.format(formatted_game_status)) 216 | 217 | 218 | def print_spacer(): 219 | """Print empty line""" 220 | return click.echo() 221 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | # PROJECT REQUIREMENTS (Your stuff here) 2 | # ------------------------------------- 3 | click 4 | future 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # Make changes in requirements.in, then run this to update: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | click==6.2 8 | future==0.15.2 9 | -------------------------------------------------------------------------------- /requirements_dev.in: -------------------------------------------------------------------------------- 1 | # GIT UTILITIES 2 | # ------------------------------------- 3 | bumpversion 4 | 5 | # PIP UTILITIES 6 | # ------------------------------------- 7 | wheel 8 | pip-tools 9 | 10 | # LINT UTILITIES 11 | # ------------------------------------- 12 | flake8 13 | 14 | # TEST UTILITIES 15 | # ------------------------------------- 16 | tox 17 | coverage 18 | pytest 19 | mock 20 | 21 | # DOC UTILITIES 22 | # ------------------------------------- 23 | sphinx 24 | watchdog 25 | restructuredtext_lint 26 | 27 | # DIST UTILITIES 28 | # ------------------------------------- 29 | # required by `travis_pypi_setup.py` 30 | cryptography 31 | PyYAML 32 | 33 | 34 | # DEV UTILITIES (Your stuff here) 35 | # ------------------------------------- 36 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # Make changes in requirements_dev.in, then run this to update: 4 | # 5 | # pip-compile requirements_dev.in 6 | # 7 | alabaster==0.7.6 # via sphinx 8 | argh==0.26.1 # via watchdog 9 | babel==2.1.1 # via sphinx 10 | bumpversion==0.5.3 11 | cffi==1.4.1 # via cryptography 12 | click==6.2 # via pip-tools 13 | coverage==4.0.3 14 | cryptography==1.1.2 15 | docutils==0.12 # via restructuredtext-lint, sphinx 16 | first==2.0.1 # via pip-tools 17 | flake8==2.5.1 18 | idna==2.0 # via cryptography 19 | jinja2==2.8 # via sphinx 20 | markupsafe==0.23 # via jinja2 21 | mccabe==0.3.1 # via flake8 22 | mock==1.3.0 23 | pathtools==0.1.2 # via watchdog 24 | pbr==1.8.1 # via mock 25 | pep8==1.5.7 # via flake8 26 | pip-tools==1.4.2 27 | pluggy==0.3.1 # via tox 28 | py==1.4.31 # via pytest, tox 29 | pyasn1==0.1.9 # via cryptography 30 | pycparser==2.14 # via cffi 31 | pyflakes==1.0.0 # via flake8 32 | pygments==2.0.2 # via sphinx 33 | pytest==2.8.5 34 | pytz==2015.7 # via babel 35 | pyyaml==3.11 36 | restructuredtext-lint==0.14.0 37 | six==1.10.0 # via cryptography, mock, pip-tools, sphinx 38 | snowballstemmer==1.2.0 # via sphinx 39 | sphinx-rtd-theme==0.1.9 # via sphinx 40 | sphinx==1.3.3 41 | tox==2.3.1 42 | virtualenv==13.1.2 # via tox 43 | watchdog==0.8.3 44 | wheel==0.26.0 45 | 46 | # The following packages are commented out because they are 47 | # considered to be unsafe in a requirements file: 48 | # setuptools==19.1.1 # via cryptography 49 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.2.2 3 | commit = True 4 | tag = False 5 | tag_name = {new_version} 6 | 7 | [bumpversion:file:setup.py] 8 | 9 | [bumpversion:file:hangman/__init__.py] 10 | 11 | [wheel] 12 | universal = 1 13 | 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | """ 4 | The full documentation is at https://python_hangman.readthedocs.org. 5 | """ 6 | 7 | try: 8 | from setuptools import setup 9 | except ImportError: 10 | from distutils.core import setup 11 | 12 | from setuptools.command.test import test as TestCommand 13 | 14 | 15 | class PyTest(TestCommand): 16 | def finalize_options(self): 17 | TestCommand.finalize_options(self) 18 | self.test_args = [] 19 | self.test_suite = True 20 | 21 | def run_tests(self): 22 | import pytest 23 | import sys 24 | 25 | errno = pytest.main(self.test_args) 26 | sys.exit(errno) 27 | 28 | 29 | with open('README.rst') as readme_file: 30 | readme = readme_file.read() 31 | 32 | with open('HISTORY.rst') as history_file: 33 | history = history_file.read() 34 | 35 | requirements = ['click', 'future'] 36 | 37 | test_requirements = ['pytest', 'mock'] 38 | 39 | setup( # :off 40 | name='python_hangman', 41 | version='2.2.2', 42 | description='Python Hangman TDD/MVC demonstration.', 43 | long_description='\n\n'.join([readme, history]), 44 | author='Manu Phatak', 45 | author_email='bionikspoon@gmail.com', 46 | url='https://github.com/bionikspoon/python_hangman', 47 | packages=['hangman',], 48 | package_dir={'hangman':'hangman'}, 49 | include_package_data=True, 50 | install_requires=requirements, 51 | license='MIT', 52 | zip_safe=False, 53 | use_2to3=True, 54 | cmdclass={'test': PyTest}, 55 | keywords='python_hangman Manu Phatak', 56 | entry_points={'console_scripts': ['hangman = hangman.__main__:cli']}, 57 | classifiers=[ 58 | 'Development Status :: 5 - Production/Stable', 59 | 'Environment :: Console', 60 | 'Intended Audience :: End Users/Desktop', 61 | 'License :: OSI Approved :: MIT License', 62 | 'Natural Language :: English', 63 | 'Operating System :: OS Independent', 64 | 'Programming Language :: Python', 65 | 'Programming Language :: Python :: 2', 66 | 'Programming Language :: Python :: 2.6', 67 | 'Programming Language :: Python :: 2.7', 68 | 'Programming Language :: Python :: 3', 69 | 'Programming Language :: Python :: 3.3', 70 | 'Programming Language :: Python :: 3.4', 71 | 'Programming Language :: Python :: 3.5', 72 | 'Programming Language :: Python :: Implementation :: CPython', 73 | 'Programming Language :: Python :: Implementation :: PyPy', 74 | 'Topic :: Games/Entertainment :: Puzzle Games', 75 | 'Topic :: Terminals', 76 | ], 77 | test_suite='tests', 78 | tests_require=test_requirements 79 | ) # :on 80 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuphatak/python_hangman/f4b990b585b0b270c78221826f3e7ee3390f4ee3/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_controller.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from functools import partial 3 | 4 | from mock import Mock 5 | from pytest import fixture 6 | 7 | 8 | @fixture(autouse=True) 9 | def setup(monkeypatch): 10 | from hangman.utils import GameOverNotificationComplete, FlashMessage 11 | 12 | def draw_board(_, message=FlashMessage()): 13 | if message.game_lost or message.game_won: 14 | raise GameOverNotificationComplete 15 | return 'View draws board' 16 | 17 | monkeypatch.setattr('hangman.view.draw_board', draw_board) 18 | monkeypatch.setattr('hangman.view.say_goodbye', lambda: 'Have a nice day!') 19 | monkeypatch.setattr('hangman.view.prompt_guess', lambda: 'A') 20 | monkeypatch.setattr('hangman.view.prompt_play_again', lambda: False) 21 | 22 | 23 | @fixture(autouse=True) 24 | def game(): 25 | from hangman.model import Hangman 26 | 27 | return Hangman(answer='hangman') 28 | 29 | 30 | @fixture 31 | def flash(): 32 | from hangman.utils import FlashMessage 33 | 34 | return FlashMessage() 35 | 36 | 37 | @fixture 38 | def run(game, flash): 39 | from hangman.controller import run 40 | 41 | return partial(run, game=game, flash=flash) 42 | 43 | 44 | def test_setup(): 45 | from hangman import view 46 | from hangman.model import Hangman 47 | 48 | assert view.draw_board(Hangman()) == 'View draws board' 49 | assert view.say_goodbye() == 'Have a nice day!' 50 | assert view.prompt_guess() == 'A' 51 | assert not view.prompt_play_again() 52 | 53 | 54 | def test_game_lost(game, run, monkeypatch, flash): 55 | monkeypatch.setattr('hangman.view.prompt_guess', lambda: 'O') 56 | game.misses = list('BCDEFIJKL') 57 | 58 | assert run() == 'Have a nice day!' 59 | assert flash.game_lost is True 60 | 61 | 62 | def test_game_won(game, run, flash): 63 | game.hits = list('HNGMN') 64 | 65 | assert run() == 'Have a nice day!' 66 | assert flash.game_won is True 67 | 68 | 69 | def test_value_error(game, run, monkeypatch): 70 | monkeypatch.setattr('hangman.view.prompt_guess', Mock(side_effect=['1', 'A'])) 71 | game.hits = list('HNGMN') 72 | 73 | assert run() == 'Have a nice day!' 74 | 75 | 76 | def test_keyboard_interupt(run, monkeypatch): 77 | monkeypatch.setattr('hangman.view.prompt_guess', Mock(side_effect=KeyboardInterrupt)) 78 | 79 | assert run() == 'Have a nice day!' 80 | 81 | 82 | def test_game_finished(run, monkeypatch): 83 | monkeypatch.setattr('hangman.view.prompt_guess', 84 | Mock(side_effect=['H', 'A', 'N', 'G', 'M', 'N', KeyboardInterrupt])) 85 | monkeypatch.setattr('hangman.view.prompt_play_again', Mock(side_effect=[True, False])) 86 | 87 | assert run() == 'Have a nice day!' 88 | -------------------------------------------------------------------------------- /tests/test_flash_message.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | from pytest import fixture 4 | 5 | from hangman.utils import FlashMessage 6 | 7 | 8 | @fixture 9 | def flash(): 10 | return FlashMessage() 11 | 12 | 13 | def test_is_blank(flash): 14 | assert str(flash) is '' 15 | 16 | 17 | def test_calling_sets_message(flash): 18 | flash('TEST') 19 | assert flash.message is 'TEST' 20 | 21 | 22 | def test_consumption_clears_message(flash): 23 | flash('TEST') 24 | assert str(flash) is 'TEST' 25 | assert str(flash) is '' 26 | 27 | 28 | def test_comparison_does_not_clear_message(flash): 29 | flash('TEST') 30 | if flash: 31 | pass 32 | 33 | assert flash 34 | assert flash == True # noqa 35 | 36 | assert str(flash) is 'TEST' 37 | 38 | 39 | def test_is_false_when_no_message(flash): 40 | assert not flash 41 | assert flash == False # noqa 42 | 43 | 44 | def test_can_be_formatted_when_empty(flash): 45 | assert '{0:45s}'.format(flash) == '{0:45s}'.format('') 46 | 47 | 48 | def test_can_be_formatted_to_string(flash): 49 | flash('TEST') 50 | assert 'Hello {0}'.format(flash) == 'Hello TEST' 51 | 52 | 53 | def test_formatting_consumes_flash_message(flash): 54 | flash('TEST') 55 | 'Hello {0}'.format(flash) 56 | 57 | assert 'Hello {0}'.format(flash) == 'Hello ' 58 | -------------------------------------------------------------------------------- /tests/test_hangman.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | 4 | """ 5 | test_hangman 6 | ---------------------------------- 7 | 8 | Tests for `Hangman` model. 9 | """ 10 | from pytest import fixture, raises 11 | 12 | from hangman.model import Hangman, GameWon, GameLost 13 | 14 | 15 | @fixture 16 | def game(): 17 | return Hangman(answer='hangman') 18 | 19 | 20 | def test_new_game_returns_game_instance_with_answer(game): 21 | assert game.answer == 'HANGMAN' 22 | 23 | 24 | def test_new_game_returns_game_instance_with_misses(game): 25 | assert game.misses == [] 26 | 27 | 28 | def test_new_game_returns_game_instance_with_remaining_turns(game): 29 | assert game.remaining_turns == 10 30 | 31 | 32 | def test_new_game_returns_game_instance_with_status(game): 33 | assert game.status == '_______' 34 | 35 | 36 | def test_answer_validation_rules(): 37 | with raises(ValueError): 38 | Hangman('1234567') 39 | 40 | with raises(ValueError): 41 | Hangman('hangman12') 42 | 43 | with raises(ValueError): 44 | Hangman(1232145678995462313) 45 | 46 | with raises(ValueError): 47 | Hangman('a' * 100) 48 | with raises(ValueError): 49 | Hangman('a' * 17) 50 | 51 | with raises(ValueError): 52 | Hangman('hand-work') 53 | 54 | 55 | def test_guess_miss_removes_1_turn(game): 56 | game.guess('e') 57 | assert game.remaining_turns == 9 58 | 59 | 60 | def test_guess_miss_updates_misses(game): 61 | game.guess('e') 62 | assert game.misses == ['E'] 63 | 64 | 65 | def test_guess_miss_does_not_change_status(game): 66 | expected = game.status 67 | game.guess('e') 68 | assert game.status == expected == '_______' 69 | 70 | 71 | def test_guess_validation_must_be_a_single_letter_number(game): 72 | with raises(ValueError): 73 | game.guess(1) 74 | 75 | with raises(ValueError): 76 | game.guess('EE') 77 | 78 | with raises(ValueError): 79 | game.guess('') 80 | 81 | 82 | def test_guess_miss_duplicate_is_ignored(game): 83 | game.guess('e') 84 | game.guess('e') 85 | assert game.remaining_turns == 9 86 | 87 | 88 | def test_guess_hit_updates_status(game): 89 | game.guess('a') 90 | assert game.status == '_A___A_' 91 | 92 | game.guess('H') 93 | assert game.status == 'HA___A_' 94 | 95 | 96 | def test_guess_hit_leaves_remaining_turns_and_misses_untouched(game): 97 | expected_misses = game.misses 98 | expected_remaining_turns = game.remaining_turns 99 | 100 | game.guess('a') 101 | 102 | assert expected_misses == game.misses 103 | assert expected_remaining_turns == game.remaining_turns 104 | 105 | 106 | def test_game_winning_guess(game): 107 | game.guess('h') 108 | game.guess('a') 109 | game.guess('n') 110 | game.guess('g') 111 | 112 | with raises(GameWon): 113 | game.guess('m') 114 | 115 | assert game.status == 'HANGMAN' 116 | 117 | 118 | def test_setting_hits_can_raise_game_won(game): 119 | with raises(GameWon): 120 | game.hits = list('HANGMAN') 121 | 122 | 123 | def test_game_losing_guess(game): 124 | game.guess('b') 125 | game.guess('c') 126 | game.guess('d') 127 | game.guess('e') 128 | game.guess('f') 129 | game.guess('i') 130 | game.guess('j') 131 | game.guess('k') 132 | game.guess('l') 133 | 134 | with raises(GameLost): 135 | game.guess('o') 136 | assert game.status == '_______' 137 | assert game.remaining_turns == 0 138 | 139 | 140 | def test_setting_misses_can_raise_game_over(game): 141 | with raises(GameLost): 142 | game.misses = list('BCDEFIJKLO') 143 | 144 | 145 | def test_game_populates_answer_if_not_provided(): 146 | from hangman.utils import WordBank 147 | 148 | WordBank.set('TEST') 149 | 150 | _game = Hangman() 151 | assert _game.answer == 'TEST' 152 | 153 | 154 | def test_game_repr(game): 155 | expected = "hangman(status='_______', misses=[], remaining_turns=10)" 156 | assert repr(game) == expected 157 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | from mock import Mock 4 | from pytest import fixture 5 | 6 | 7 | @fixture(autouse=True) 8 | def setup(monkeypatch, game_loop): 9 | monkeypatch.setattr('hangman.controller.game_loop', game_loop) 10 | 11 | 12 | @fixture 13 | def game_loop(): 14 | return Mock() 15 | 16 | 17 | @fixture 18 | def runner(): 19 | from click.testing import CliRunner 20 | 21 | return CliRunner() 22 | 23 | 24 | def test_click_starts_game_loop(game_loop, runner): 25 | from hangman.__main__ import cli 26 | 27 | result = runner.invoke(cli) 28 | assert result.exit_code == 0 29 | assert game_loop.call_count == 1 30 | -------------------------------------------------------------------------------- /tests/test_view.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from textwrap import dedent 3 | 4 | from mock import Mock 5 | from pytest import fixture, raises, mark 6 | 7 | from hangman import view 8 | 9 | 10 | @fixture(autouse=True) 11 | def setup(monkeypatch): 12 | monkeypatch.setattr('click.getchar', lambda: 'A') 13 | monkeypatch.setattr('click.confirm', lambda _: True) 14 | monkeypatch.setattr('click.clear', lambda: None) 15 | 16 | 17 | @fixture 18 | def patch_echo(monkeypatch): 19 | noop = lambda *__, **_: None # noqa 20 | 21 | monkeypatch.setattr('click.secho', noop) 22 | monkeypatch.setattr('click.echo', noop) 23 | 24 | 25 | @fixture 26 | def game(): 27 | from hangman.model import Hangman 28 | 29 | mock_game = Mock(spec=Hangman) 30 | mock_game.misses = [] 31 | mock_game.status = '_______' 32 | mock_game.answer = 'HANGMAN' 33 | mock_game.remaining_turns = 10 34 | return mock_game 35 | 36 | 37 | @fixture 38 | def flash(): 39 | from hangman.utils import FlashMessage 40 | 41 | return FlashMessage() 42 | 43 | 44 | def test_picture_10_turns(): 45 | remaining_turns = 10 46 | 47 | actual = '\n'.join(view.build_partial_picture(remaining_turns)) 48 | expected = dedent(""" 49 | _____ 50 | | | 51 | | 52 | | 53 | | 54 | | 55 | | 56 | ________|_"""[1:]) 57 | assert actual.splitlines() == expected.splitlines() 58 | assert actual == expected 59 | 60 | 61 | def test_picture_9_turns(): 62 | remaining_turns = 9 63 | 64 | actual = '\n'.join(view.build_partial_picture(remaining_turns)) 65 | expected = dedent(""" 66 | _____ 67 | | | 68 | (_) | 69 | | 70 | | 71 | | 72 | | 73 | ________|_"""[1:]) 74 | assert actual.splitlines() == expected.splitlines() 75 | assert actual == expected 76 | 77 | 78 | def test_picture_8_turns(): 79 | remaining_turns = 8 80 | 81 | actual = '\n'.join(view.build_partial_picture(remaining_turns)) 82 | expected = dedent(""" 83 | _____ 84 | | | 85 | (_) | 86 | | | 87 | | 88 | | 89 | | 90 | ________|_"""[1:]) 91 | assert actual.splitlines() == expected.splitlines() 92 | assert actual == expected 93 | 94 | 95 | def test_picture_7_turns(): 96 | remaining_turns = 7 97 | 98 | actual = '\n'.join(view.build_partial_picture(remaining_turns)) 99 | expected = dedent(""" 100 | _____ 101 | | | 102 | (_) | 103 | | | 104 | | | 105 | | 106 | | 107 | ________|_"""[1:]) 108 | assert actual.splitlines() == expected.splitlines() 109 | assert actual == expected 110 | 111 | 112 | def test_picture_6_turns(): 113 | remaining_turns = 6 114 | 115 | actual = '\n'.join(view.build_partial_picture(remaining_turns)) 116 | expected = dedent(""" 117 | _____ 118 | | | 119 | (_) | 120 | \| | 121 | | | 122 | | 123 | | 124 | ________|_"""[1:]) 125 | assert actual.splitlines() == expected.splitlines() 126 | assert actual == expected 127 | 128 | 129 | def test_picture_5_turns(): 130 | remaining_turns = 5 131 | 132 | actual = '\n'.join(view.build_partial_picture(remaining_turns)) 133 | expected = dedent(""" 134 | _____ 135 | | | 136 | (_) | 137 | \|/ | 138 | | | 139 | | 140 | | 141 | ________|_"""[1:]) 142 | assert actual.splitlines() == expected.splitlines() 143 | assert actual == expected 144 | 145 | 146 | def test_picture_4_turns(): 147 | remaining_turns = 4 148 | 149 | actual = '\n'.join(view.build_partial_picture(remaining_turns)) 150 | expected = dedent(""" 151 | _____ 152 | | | 153 | (_) | 154 | \|/ | 155 | | | 156 | | | 157 | | 158 | ________|_"""[1:]) 159 | assert actual.splitlines() == expected.splitlines() 160 | assert actual == expected 161 | 162 | 163 | def test_picture_3_turns(): 164 | remaining_turns = 3 165 | 166 | actual = '\n'.join(view.build_partial_picture(remaining_turns)) 167 | expected = dedent(""" 168 | _____ 169 | | | 170 | (_) | 171 | \|/ | 172 | | | 173 | | | 174 | / | 175 | ________|_"""[1:]) 176 | assert actual.splitlines() == expected.splitlines() 177 | assert actual == expected 178 | 179 | 180 | def test_picture_2_turns(): 181 | remaining_turns = 2 182 | 183 | actual = '\n'.join(view.build_partial_picture(remaining_turns)) 184 | expected = dedent(""" 185 | _____ 186 | | | 187 | (_) | 188 | \|/ | 189 | | | 190 | | | 191 | / \ | 192 | ________|_"""[1:]) 193 | assert actual.splitlines() == expected.splitlines() 194 | assert actual == expected 195 | 196 | 197 | def test_picture_1_turns(): 198 | remaining_turns = 1 199 | 200 | actual = '\n'.join(view.build_partial_picture(remaining_turns)) 201 | expected = dedent(""" 202 | _____ 203 | | | 204 | (_) | 205 | \|/ | 206 | | | 207 | | | 208 | _/ \ | 209 | ________|_"""[1:]) 210 | assert actual.splitlines() == expected.splitlines() 211 | assert actual == expected 212 | 213 | 214 | def test_picture_0_turns(): 215 | remaining_turns = 0 216 | 217 | actual = '\n'.join(view.build_partial_picture(remaining_turns)) 218 | expected = dedent(""" 219 | _____ 220 | | | 221 | (_) | 222 | \|/ | 223 | | | 224 | | | 225 | _/ \_ | 226 | ________|_"""[1:]) 227 | assert actual.splitlines() == expected.splitlines() 228 | assert actual == expected 229 | 230 | 231 | def test_status_0_misses_full(): 232 | misses = [] 233 | actual = list(view.build_partial_misses(misses)) 234 | expected = ['', '', '', ' MISSES:', ' _ _ _ _ _ _ _ _ _ _', '', '', '', '', ''] 235 | 236 | assert actual == expected 237 | 238 | 239 | def test_status_2_misses(): 240 | misses = ['A', 'E'] 241 | 242 | actual = list(view.build_partial_misses(misses)) 243 | expected = ['', '', '', ' MISSES:', ' A E _ _ _ _ _ _ _ _', '', '', '', '', ''] 244 | 245 | assert set(actual[4].split(' ')) == set(expected[4].split(' ')) 246 | 247 | 248 | def test_status_10_misses(): 249 | misses = list('QWERTYASDF') 250 | 251 | actual = list(view.build_partial_misses(misses)) 252 | expected = ['', '', '', ' MISSES:', ' A E D F Q S R T W Y', '', '', '', '', ''] 253 | 254 | assert set(actual[4].split(' ')) == set(expected[4].split(' ')) 255 | 256 | 257 | def test_draw_board(game, capsys): 258 | expected_list = dedent(""" 259 | HANGMAN GAME 260 | _____ 261 | | | 262 | | 263 | | MISSES: 264 | | _ _ _ _ _ _ _ _ _ _ 265 | | 266 | | 267 | ________|_ 268 | 269 | _ _ _ _ _ _ _ 270 | """).split('\n') 271 | view.draw_board(game) 272 | out, err = capsys.readouterr() 273 | actual_list = out.split('\n') 274 | for actual_list, expected in zip(actual_list, expected_list): 275 | assert actual_list.rstrip() == expected.rstrip() 276 | assert err == '' 277 | 278 | 279 | def test_flash_message(game, capsys, flash): 280 | message = 'This test is a success' 281 | flash(message) 282 | view.draw_board(game, message=flash) 283 | out, err = capsys.readouterr() 284 | assert out.split('\n')[0] == '{0: <45}'.format(message) 285 | assert err == '' 286 | 287 | 288 | def test_flash_message_handles_error_objects(game, capsys, flash): 289 | message = 'This test is a success' 290 | 291 | try: 292 | raise ValueError(message) 293 | except ValueError as e: 294 | flash(e) 295 | view.draw_board(game, message=flash) 296 | 297 | out, err = capsys.readouterr() 298 | assert out.split('\n')[0] == '{0: <45}'.format(message) 299 | assert err == '' 300 | 301 | 302 | @mark.usefixtures('patch_echo') 303 | def test_prompting_for_a_guess(): 304 | actual = view.prompt_guess() 305 | assert actual == 'A' 306 | 307 | 308 | @mark.usefixtures('patch_echo') 309 | def test_keyboard_interrupt(monkeypatch): 310 | monkeypatch.setattr('click.getchar', lambda: '\x03') 311 | 312 | with raises(KeyboardInterrupt): 313 | view.prompt_guess() 314 | 315 | 316 | @mark.usefixtures('patch_echo') 317 | def test_prompt_play_again_method_true(): 318 | assert view.prompt_play_again() is True 319 | 320 | 321 | def test_say_goodbye_method(capsys): 322 | view.say_goodbye() 323 | out, err = capsys.readouterr() 324 | assert out == 'Have a nice day!\n\n' 325 | assert err == '' 326 | 327 | 328 | def test_game_won(capsys, game, flash): 329 | from hangman.utils import GameOverNotificationComplete 330 | 331 | expected = 'YOU ARE SO COOL' 332 | flash.game_won = True 333 | 334 | with raises(GameOverNotificationComplete): 335 | view.draw_board(game, message=flash) 336 | out, err = capsys.readouterr() 337 | 338 | assert out.startswith(expected) 339 | 340 | 341 | def test_game_lost(capsys, game, flash): 342 | from hangman.utils import GameOverNotificationComplete 343 | 344 | expected = "YOU LOSE! THE ANSWER IS HANGMAN" 345 | flash.game_lost = True 346 | flash.game_answer = 'HANGMAN' 347 | 348 | with raises(GameOverNotificationComplete): 349 | view.draw_board(game, message=flash) 350 | out, err = capsys.readouterr() 351 | 352 | assert out.startswith(expected) 353 | -------------------------------------------------------------------------------- /tests/test_word_bank.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from pytest import fixture 3 | 4 | 5 | @fixture 6 | def mock_word_bank_get(): 7 | from hangman.utils import WordBank 8 | 9 | WordBank.set('TEST') 10 | return WordBank.get 11 | 12 | 13 | def test_dictionary_returns_random_choice(mock_word_bank_get): 14 | assert mock_word_bank_get() == 'TEST' 15 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26, py27, py33, py34, py35, pypy, docs 3 | basepython = python2.7 4 | 5 | [testenv] 6 | commands = 7 | {envpython} setup.py test 8 | deps =-rrequirements.txt 9 | setenv = 10 | PYTHONPATH = {toxinidir}:{toxinidir}/hangman 11 | PYTHONWARNINGS = all 12 | LC_ALL = C.UTF-8 13 | LANG = C.UTF-8 14 | 15 | [testenv:docs] 16 | whitelist_externals = make 17 | changedir = docs/ 18 | deps =-rrequirements_dev.txt 19 | 20 | commands = 21 | make linkcheck 22 | make html 23 | make doctest 24 | make coverage 25 | 26 | [flake8] 27 | show-source = True 28 | max-line-length = 120 29 | -------------------------------------------------------------------------------- /travis_pypi_setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | """ 4 | Update encrypted deploy password in Travis config file 5 | """ 6 | 7 | from __future__ import print_function 8 | import base64 9 | import json 10 | import os 11 | from getpass import getpass 12 | import yaml 13 | from cryptography.hazmat.primitives.serialization import load_pem_public_key 14 | from cryptography.hazmat.backends import default_backend 15 | from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 16 | 17 | try: 18 | from urllib import urlopen 19 | except ImportError: 20 | # noinspection PyCompatibility 21 | from urllib.request import urlopen 22 | 23 | GITHUB_REPO = 'bionikspoon/python_hangman' 24 | TRAVIS_CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), '.travis.yml') 25 | 26 | 27 | def load_key(pubkey): 28 | """Load public RSA key, with work-around for keys using 29 | incorrect header/footer format. 30 | 31 | Read more about RSA encryption with cryptography: 32 | https://cryptography.io/latest/hazmat/primitives/asymmetric/rsa/ 33 | """ 34 | try: 35 | return load_pem_public_key(pubkey.encode(), default_backend()) 36 | except ValueError: 37 | # workaround for https://github.com/travis-ci/travis-api/issues/196 38 | pubkey = pubkey.replace('BEGIN RSA', 'BEGIN').replace('END RSA', 'END') 39 | return load_pem_public_key(pubkey.encode(), default_backend()) 40 | 41 | 42 | def encrypt(pubkey, password): 43 | """Encrypt password using given RSA public key and encode it with base64. 44 | 45 | The encrypted password can only be decrypted by someone with the 46 | private key (in this case, only Travis). 47 | """ 48 | key = load_key(pubkey) 49 | encrypted_password = key.encrypt(password, PKCS1v15()) 50 | return base64.b64encode(encrypted_password) 51 | 52 | 53 | def fetch_public_key(repo): 54 | """Download RSA public key Travis will use for this repo. 55 | 56 | Travis API docs: http://docs.travis-ci.com/api/#repository-keys 57 | """ 58 | keyurl = 'https://api.travis-ci.org/repos/{0}/key'.format(repo) 59 | data = json.loads(urlopen(keyurl).read().decode()) 60 | if 'key' not in data: 61 | errmsg = "Could not find public key for repo: {}.\n".format(repo) 62 | errmsg += "Have you already added your GitHub repo to Travis?" 63 | raise ValueError(errmsg) 64 | return data['key'] 65 | 66 | 67 | def prepend_line(filepath, line): 68 | """Rewrite a file adding a line to its beginning. 69 | """ 70 | with open(filepath) as f: 71 | lines = f.readlines() 72 | 73 | lines.insert(0, line) 74 | 75 | with open(filepath, 'w') as f: 76 | f.writelines(lines) 77 | 78 | 79 | def load_yaml_config(filepath): 80 | with open(filepath) as f: 81 | return yaml.load(f) 82 | 83 | 84 | def save_yaml_config(filepath, config): 85 | with open(filepath, 'w') as f: 86 | yaml.dump(config, f, default_flow_style=False) 87 | 88 | 89 | def update_travis_deploy_password(encrypted_password): 90 | """Update the deploy section of the .travis.yml file 91 | to use the given encrypted password. 92 | """ 93 | config = load_yaml_config(TRAVIS_CONFIG_FILE) 94 | 95 | config['deploy']['password'] = dict(secure=encrypted_password) 96 | 97 | save_yaml_config(TRAVIS_CONFIG_FILE, config) 98 | 99 | line = ('# This file was autogenerated and will overwrite' 100 | ' each time you run travis_pypi_setup.py\n') 101 | prepend_line(TRAVIS_CONFIG_FILE, line) 102 | 103 | 104 | def main(args): 105 | public_key = fetch_public_key(args.repo) 106 | password = args.password or getpass('PyPI password: ') 107 | update_travis_deploy_password(encrypt(public_key, password.encode())) 108 | print("Wrote encrypted password to .travis.yml -- you're ready to deploy") 109 | 110 | 111 | if '__main__' == __name__: 112 | import argparse 113 | 114 | parser = argparse.ArgumentParser(description=__doc__) 115 | parser.add_argument('--repo', default=GITHUB_REPO, help='GitHub repo (default: %s)' % GITHUB_REPO) 116 | parser.add_argument('--password', help='PyPI password (will prompt if not provided)') 117 | 118 | args = parser.parse_args() 119 | main(args) 120 | --------------------------------------------------------------------------------