├── .github └── workflows │ └── build_deploy.yml ├── .gitignore ├── Makefile ├── README.md ├── _static ├── academis.png ├── custom.css ├── favicon.ico └── header-alt.jpg ├── citable_code.md ├── coding_style.md ├── conf.py ├── documenting.md ├── editors.md ├── environment_variables.md ├── exercises.md ├── github_issues.md ├── good_software.md ├── images ├── Figure_test_driven_development_3.png ├── InputProcessingOutput.odp ├── cloud.png ├── cloud.svg ├── cover.jpg ├── cover.png ├── cover.svg ├── cover │ ├── software_engineering_making_of.svg │ ├── software_engineering_title_04.svg │ └── software_engineering_title_seq.svg ├── cover_small.jpg ├── crc.png ├── decomposing_stories.png ├── github_issue.png ├── github_issue_comment.png ├── introspection.png ├── io_def1.png ├── io_def2.png ├── io_def3.png ├── legacy_graph_simple.png ├── legacy_graph_simple.svg ├── mind_map.png ├── pair_user.png ├── pbis.png ├── program_publish_prove.png ├── roadmap │ ├── python_roadmap.md │ ├── python_roadmap_DE.png │ ├── python_roadmap_DE.svg │ └── python_roadmap_EN.svg ├── softdev.svg ├── software_qa.png ├── software_quality.svg ├── starmap.png ├── starmap.svg ├── toolbox.png ├── userstory.png ├── warning_signs.png └── waterfall.png ├── impostor.md ├── index.rst ├── interface.md ├── legacy_code.md ├── loc.md ├── programming_language_exercise.md ├── project_checklist.rst ├── project_management.md ├── project_templates.md ├── refactoring ├── LICENSE ├── README.md ├── solution │ ├── 01-extract-module │ │ ├── space_game.py │ │ ├── test_space_game.py │ │ └── text_en.py │ ├── 02-extract-function │ │ ├── space_game.py │ │ ├── test_space_game.py │ │ └── text_en.py │ ├── 03-extract-and-modify │ │ ├── space_game.py │ │ ├── test_space_game.py │ │ └── text_en.py │ ├── 04-extract-data-structure │ │ ├── space_game.py │ │ ├── test_space_game.py │ │ └── text_en.py │ ├── 05-extract-class │ │ ├── puzzles.py │ │ ├── space_game.py │ │ ├── test_space_game.py │ │ └── text_en.py │ ├── 06-another-class │ │ ├── puzzles.py │ │ ├── space_game.py │ │ ├── test_space_game.py │ │ └── text_en.py │ └── 07-oop-decouple-game-logic │ │ ├── puzzles.py │ │ ├── space_game.py │ │ ├── test_space_game.py │ │ └── text_en.py ├── space_game.py └── test_space_game.py ├── requirements.txt ├── tech_debt.md ├── user_stories.md └── writing_code.md /.github/workflows/build_deploy.yml: -------------------------------------------------------------------------------- 1 | 2 | name: deploy software engineering 3 | 4 | on: 5 | push: 6 | branches: [ master ] 7 | 8 | jobs: 9 | 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: checkout repo 16 | uses: actions/checkout@v1 17 | 18 | - name: build static html 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install -r requirements.txt 22 | make html 23 | 24 | - name: copy to academis server 25 | uses: appleboy/scp-action@master 26 | with: 27 | host: ${{ secrets.ACADEMIS_HOST }} 28 | username: ${{ secrets.ACADEMIS_USERNAME }} 29 | port: 22 30 | key: ${{ secrets.SSH_PRIVATE_KEY }} 31 | source: build/html/* 32 | target: /www/academis/software_engineering 33 | rm: true 34 | strip_components: 2 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Python Software Development 3 | 4 | ### What this guide is about? 5 | 6 | This guide is for you if you are writing programs with more than 500 lines. 7 | 8 | You know how to write Python code, but have realized that creating a piece of software is more complex. You are facing questions like: 9 | 10 | * How to clean up my code? 11 | * How to make sure my program works? 12 | * How to install my program on multiple computers? 13 | * How to keep the program running over time? 14 | * How to deliver the program to other people? 15 | 16 | Below you find development tools and techniques that help you to write programs that get the job done and don't fall apart. 17 | 18 | ---- 19 | 20 | ## Getting Started 21 | 22 | * [Start with a Prototype](prototype.md) 23 | * [Set up a Git Repository](version_control.md) 24 | * [Create a Folder Structure](folders.md) 25 | * [Create Issues on GitHub](github_issues.md) 26 | 27 | ---- 28 | 29 | ## Planning and Design 30 | 31 | * [Define an Interface](interface.md) 32 | * [Class Diagrams](class_diagram.md) 33 | * [User Stories](user_stories.md) 34 | * [CRC Cards](crc_cards.md) 35 | 36 | ---- 37 | 38 | ## Packaging and Maintenance 39 | 40 | * [Virtual Environments](virtualenv.md) 41 | * [Installing packages with pip](pip.md) 42 | * [Create a pip-installable Package](pip_setup.md) 43 | * [Continuous Integration](continuous_integration.md) 44 | 45 | ---- 46 | 47 | ## Coding Strategies 48 | 49 | * [Coding Strategies](writing_code.md) 50 | * [Debugging](debugging.md) 51 | * [PEP8 Code Style](coding_style.md) 52 | * [Refactoring](refactoring/README.md) 53 | * [Code Reviews](code_reviews.md) 54 | 55 | ---- 56 | 57 | ## Advanced Stuff 58 | 59 | * [Counting Lines of Code](loc.md) 60 | * [Technical Debt](tech_debt.md) 61 | * [Project Templates](project_templates.md) 62 | * [Project Management](project_management.md) 63 | * [How to work with legacy code?](legacy_code.md) 64 | * [Documentation Tools](documenting.md) 65 | * [Citable Code](citable_code.md) 66 | 67 | ---- 68 | 69 | ## Other Things 70 | 71 | * [Editors](editors.md) 72 | * [Environment Variables](environment_variables.md) 73 | * [How to recognize good scientific software?](good_software.md) 74 | * [Impostor Syndrome](impostor.md) 75 | * [Exercise: Programming Languages](programming_language_exercise.md) 76 | * [Exercises](exercises.md) 77 | 78 | ---- 79 | 80 | ## Contact 81 | 82 | We are two Python software engineers who decided to write down our experience resulting from our Python projects in life science, web development and teaching. 83 | 84 | ### License 85 | 86 | *© 2020 [Kristian Rother](http://github.com/krother) and [Magdalena Rother](http://github.com/lenarother)* 87 | 88 | This text is released under the conditions of the Creative Commons Attribution Share-alike License 4.0. 89 | -------------------------------------------------------------------------------- /_static/academis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/_static/academis.png -------------------------------------------------------------------------------- /_static/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main-font: Lato; 3 | --secondary-color: #3C6F1F; 4 | --icon-frame-color: #80b940; 5 | --icon-body-color: #b3d789; 6 | --hover-color: #1b4403; 7 | --link-color:#3C6F1F; 8 | --footer-link-color: #efefef; 9 | --line-color: rgba(0, 128, 0, 0.298); 10 | --main-text-color: #000000; 11 | } 12 | 13 | /******************* VARIABLES END *******************/ 14 | 15 | 16 | /************************************** NEW CODE **************************************/ 17 | 18 | html { 19 | height: 100%; 20 | width: 100%; 21 | scroll-behavior: smooth; 22 | -webkit-box-sizing: border-box; 23 | -moz-box-sizing: border-box; 24 | box-sizing: border-box; 25 | } 26 | body { 27 | height: 100%; 28 | width: 100%; 29 | overflow-x: hidden; 30 | color: var(--main-text-color); 31 | font-family: var(--main-font); 32 | font-size: 1.4rem; 33 | background: url(header-alt.jpg) no-repeat; 34 | padding-top: 5em; 35 | } 36 | 37 | div.body { 38 | background: none; 39 | background-color: #00000000; 40 | } 41 | 42 | div.body h1 { 43 | font-size: 200%; 44 | } 45 | div.body h2 { 46 | font-size: 170%; 47 | } 48 | -------------------------------------------------------------------------------- /_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/_static/favicon.ico -------------------------------------------------------------------------------- /_static/header-alt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/_static/header-alt.jpg -------------------------------------------------------------------------------- /citable_code.md: -------------------------------------------------------------------------------- 1 | 2 | # Citable Code 3 | 4 | For scientists, getting credit for software is often essential. 5 | Here are a few links to start with: 6 | 7 | * [GitHub guide: Making Your Code Citable](https://guides.github.com/activities/citable-code/) 8 | * [Zenodo](http://zenodo.org/) 9 | * [figshare](https://figshare.com/) - citable data 10 | * [Software Sustainability Institute](https://www.software.ac.uk) 11 | * [Journal of Open Source Software](http://joss.theoj.org) 12 | * [Journal of Open Research Software](http://openresearchsoftware.metajnl.com/) 13 | * [Publishing with persistent identifiers](https://speakerdeck.com/mfenner/publication-and-citation-of-scientific-software-with-persistent-identifiers) - slides by Martin Fenner 14 | -------------------------------------------------------------------------------- /coding_style.md: -------------------------------------------------------------------------------- 1 | # PEP8 Code Style 2 | 3 | As a programmer, you probably need to read code more often than to write. Naturally, every programmer is interested in readable code. Your own code, of course, is always readable. Or is it? 4 | 5 | Fortunately, there a gold standard you can refer to. Python has a standard style guide for code, known as [PEP8](https://www.python.org/dev/peps/pep-0008). Adhering to PEP8 is good, because it makes your code readable for you and for others. 6 | 7 | ---- 8 | 9 | ## pylint 10 | 11 | The **pylint** tool checks whether your code conforms to the PEP8 coding guidelines. `pylint` is a powerful tool to analyze your code for readability and style. 12 | 13 | Install it with 14 | 15 | pip install pylint 16 | 17 | Then you can analyze any Python file: 18 | 19 | pylint my_program.py 20 | 21 | Or all the files in a folder: 22 | 23 | pyling *.py 24 | 25 | ---- 26 | 27 | ### The output of pylint 28 | 29 | In the output of `pylint`, there are two sections to pay attention to: 30 | 31 | * warning messages 32 | * a code score at the very end 33 | 34 | At the top of the output from **pylint**, you find a section with warning messages. Each warning contains the line number the warning refers to: 35 | 36 | W:117,12:Template.prepare_identifiers: Unused variable 'x' 37 | C: 32,0: Line too long (88/80) 38 | C:134,16:Renumerator.get_identifiers_list: Operator not preceded by a space 39 | C: 1,0: Missing docstring 40 | C:114,8:Renumerator.prepare_identifiers: Invalid name "fn" (should match [a-z_][a-z0-9_]{2,30}$) 41 | 42 | These warnings point you to the following issues: 43 | 44 | #### Bugs and dead code 45 | 46 | W:117,12:Template.prepare_identifiers: Unused variable 'x' 47 | 48 | This message indicates that line 117 either won't work or that the code has not been used at all. 49 | 50 | #### Coding style 51 | 52 | C: 32,0: Line too long (88/80) 53 | C:134,16:Renumerator.get_identifiers_list: Operator not preceded by a space 54 | 55 | Style issues regarding spaces, indentation and line lengths raised by pylint affect readability and are generally easy to fix. 56 | 57 | #### Docstrings 58 | 59 | C: 1,0: Missing docstring 60 | 61 | Functions and classes without docstrings are more difficult to understand. If you get a lot of docstring warnings your code may be hard to understand for someone else. 62 | 63 | #### Variable names 64 | 65 | C:114,8:Renumerator.prepare_identifiers: Invalid name "fn" (should match [a-z_][a-z0-9_]{2,30}$) 66 | 67 | Descriptive variable names are a big plus for code readability. Of course, it does not help much to replace **l** by **data_list** in order to satisfy pylint. But the name **fragment** tells you a lot more than **fn**. 68 | 69 | #### Code modularization 70 | 71 | Pylint helps to analyze modularization by printing warning messages: 72 | 73 | R: 19,0:Renumerator: Too many public methods (30/20) 74 | R: 32,4:Renumerator.letter_generator: Method could be a function 75 | R: 45,0:RNAResidue: Too many instance attributes (11/7) 76 | R:328,0:NucleotidePattern: Too few public methods (1/2) 77 | 78 | Warnings about the number of classes / methods / functions indicate that the structure of the code needs improvement. These messages require some interpretation; don't try to fix all of them by force. 79 | 80 | If you see a few warnings like these, don't worry. Only if you see them repeatedly, it may help readability to divide the code into units of more reasonable size. 81 | 82 | To assess modularization of a program as a whole, pylint is not the right tool. 83 | 84 | #### Code score 85 | 86 | At the end of the pylint output you find a score of up to 10 points: 87 | 88 | Your code has been rated at 8.18/10 89 | 90 | When you have fixed some of the issues, re-run pylint and see your score improve. The score directly measures your success and makes working with pylint very rewarding. 91 | You should generally aim to fix all the style issues so that your score becomes 10.0. 92 | You don't need to fix every issue though. You may choose to ignore types of warnings that your team is not committed to. 93 | 94 | #### Ignore warnings 95 | 96 | If you want to run `pylint` in a Continuous Integration system (e.g. in GitHub Actions), it must finish without warnings. 97 | Otherwise the CI will treat the style check as failed. 98 | A good practice is to disable some types of warnings (those you and your team agree not to adhere to). 99 | 100 | To ignore PEP8 warnings, create a file `.pylintrc` in your project directory. `pylint` finds it automatically. There you can list the types of warnings you would like to disable: 101 | 102 | [pylint] 103 | disable=C0103,C0111,line-too-long,too-few-public-methods 104 | 105 | You can refer to the disabled messages either by their name or by a code. Both are in the `pylint` output. 106 | 107 | 108 | ## Some PEP8 guidelines 109 | 110 | - Indent blocks using four spaces (no tabs) 111 | - keep lines less than 80 characters long 112 | - separate functions with two blank lines 113 | - separate logical chunks of long functions with a single blank line 114 | - write constants in `UPPER_CASE` 115 | - write other variable and function names in `snake_case` 116 | - write classes in `CamelCase` 117 | - every function, class and module has a docstring 118 | 119 | PEP8 is a *guideline*, not a lawbook. 120 | 121 | ---- 122 | 123 | ## Also see: 124 | 125 | * [How to write Pythonic Code](https://github.com/PyLadiesBerlin/materials/tree/master/12_how_to_write_pythonic_code) 126 | * [Black](https://github.com/psf/black) - a program that converts your code to conform with PEP8 127 | * [isort](https://github.com/timothycrosley/isort) - sorts your imports 128 | -------------------------------------------------------------------------------- /conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'Sofware Engineering' 10 | copyright = '2023, Kristian Rother' 11 | author = 'Kristian Rother' 12 | release = '1.0' 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | 17 | extensions = [ 18 | 'sphinx_design', 19 | 'sphinx_copybutton', 20 | 'sphinx.ext.todo', 21 | 'myst_parser', 22 | ] 23 | 24 | templates_path = ['_templates'] 25 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 26 | 27 | language = 'ls' 28 | 29 | # -- Options for HTML output ------------------------------------------------- 30 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 31 | 32 | html_theme = 'alabaster' 33 | html_theme_path = ['themes'] 34 | html_static_path = ['_static'] 35 | #html_logo = "_static/banner_wide.svg" 36 | html_favicon = "_static/favicon.ico" 37 | 38 | html_sidebars = { 39 | '**': [ 40 | 'about.html', 41 | 'localtoc.html', 42 | 'searchbox.html', 43 | ] 44 | } 45 | html_theme_options = { 46 | 'logo': 'academis.png', 47 | 'github_user': 'krother', 48 | 'github_repo': 'software-engineering-python', 49 | 'show_relbar_top' : True, 50 | 'show_relbar_bottom' : True, 51 | } 52 | -------------------------------------------------------------------------------- /documenting.md: -------------------------------------------------------------------------------- 1 | 2 | # Documentation Tools 3 | 4 | Although it sounds like a boring task at first, I like documenting software. I like writing about both my own programs and those of other people. Here is why: 5 | 6 | * First, it makes the software a lot more usable - bad documentation is a good way to keep your users out. 7 | * Second, it makes you think about the program from a new angle, helping you understand more deeply what it does. 8 | * Third, as long as you are writing in your native tongue, it is not really difficult, even if you are a beginner. 9 | * Fourth, the first person who is going to benefit from good documentation is **yourself** – in a couple of weeks or months. 10 | 11 | That said, there are a number of good Python tools to build and maintain documentation. For this article, you will find my favourite selection: 12 | 13 | ## Sphinx 14 | 15 | [Sphinx](http://sphinx-doc.org/) is the most well-known documentation tool for Python. It uses files in the [reStructuredText](http://docutils.sourceforge.net/rst.html) markup format to create **HTML websites** and **PDF documents**. Sphinx is what many big Python libraries and Python itself use for their documentation. 16 | 17 | Running Sphinx could look like this: 18 | 19 | sphinx-build html 20 | 21 | Sphinx has its strengths in: 22 | 23 | * building documents with cross-references 24 | * integrating docstrings 25 | * running tests from code examples (**doctests**) to see if your documentation is up to date 26 | 27 | Sometimes I find the layout of the generated websites difficult to change, but the available templates are very good. In conclusion, I would recommend Sphinx for documentation that consists of 20+ pages. For smaller projects it may feel a bit heavy. 28 | 29 | If you like to know more, check out this **[Talk by Eric Holscher](https://www.youtube.com/watch?v=hM4I58TA72g)** 30 | 31 | ---- 32 | 33 | ## Mkdocs 34 | 35 | [Mkdocs](http://www.mkdocs.org/) is a Python documentation tool using **Markdown** as a markup language. [Markdown](http://daringfireball.net/projects/markdown/basics) is almost ridiculously simple (see an [interactive tutorial](http://markdowntutorial.com)). With Mkdocs you can compile a static HTML website from a folder with Markdown files. There are many templates to choose from and you can create your own easily. 36 | 37 | A very cool feature is that you can run a local documentation server with 38 | 39 | mkdocs serve 40 | 41 | and the local website is automatically updated as you edit the Markdown documents. 42 | 43 | Personally, I find Mkdocs much easier to get started with than Sphinx, but you have less control over things. Even changing the order of the table of contents requires an effort. 44 | 45 | ---- 46 | 47 | ## GitHub Pages 48 | 49 | [Github](https://github.com/) offers a neat mechanism to create your own pages at zero cost. It renders ReST and Markdown documents (e.g. README files). You can configure GitHub pages from the **Settings** tab of your repository. There is nothing Python-specific about GitHub pages, so it is totally up to you to make sure your documentation works. 50 | 51 | Personally, I find the templates not that easy to edit. But GitHub pages are a great option for publishing a web page that goes beyond a README file. It is also a great tool to set up your first personal web page. 52 | 53 | ---- 54 | 55 | ## Readthedocs 56 | 57 | [Readthedocs](https://readthedocs.org/) is a website hosting documentation for many programming projects. It can handle both the **Sphinx** and **Mkdocs** formats (ReST and Markdown, respectively). The nice thing about it is that you can connect Readthedocs to your Github or Bitbucket repository, so that every time you push new code to the repository, the documentation gets updated as well. As long as the repository is public, no additional cost is involved. 58 | 59 | My personal opinion: Great, go for it! 60 | 61 | ---- 62 | 63 | ## Conclusion 64 | 65 | Which of these tools is best depends a lot on who you are writing for, what kind of documentation you are writing (tutorial, full reference, cookbook or all three combined), and what it will be read with. In any case, you have a lot of options to cover some of the white space between the README file and a 100-page manual. 66 | -------------------------------------------------------------------------------- /editors.md: -------------------------------------------------------------------------------- 1 | 2 | # Editors 3 | 4 | **The editor is the main tool of a programmer. Learn to use one of them well.** 5 | 6 | Here we list the most common Python editors. 7 | 8 | | editor | description | 9 | |--------|---------------| 10 | | VS Code | powerful editor with many plugins, maintained by Microsoft | 11 | | PyCharm | lots of functionality for writing big programs | 12 | | Spyder | Anaconda IDE with interactive debugger | 13 | | IDLE | default basic Python editor | 14 | | IPython | powerful interactive environment | 15 | | Jupyter | great for integrating output, text and diagrams | 16 | | JupyterLab | like Jupyter but slightly different interface | 17 | | Notepad++ | good general-purpose text editor on Windows | 18 | | Vim | works through SSH and other terminals | 19 | 20 | ---- 21 | 22 | ## VS Code 23 | 24 | A modern general-purpose text editor. There are many plugins for Python and other languages available. It has great integration for git and Docker. 25 | 26 | ---- 27 | 28 | ## PyCharm 29 | 30 | PyCharm is probably the most luxurious IDE for Python. It contains tons of functions that cover most of what the other editors offer. This makes PyCharm a great choice for bigger Python projects, although it has a bit of a learning curve. 31 | 32 | ---- 33 | 34 | ## Spyder 35 | 36 | **Spyder** is part of the **Anaconda** Python distribution. It is a small IDE mostly for data analysis, similar to RStudio. It automatically highlights Syntax errors, contains a variable explorer, debugging functionality and other useful things. 37 | 38 | ---- 39 | 40 | ## IDLE 41 | 42 | The standard editor distributed with Python. IDLE is easy to use but very basic. 43 | IDLE is not useful for bigger programs. 44 | 45 | ---- 46 | 47 | ## IPython 48 | 49 | IPython is a better interactive Python command line. It incorporates tab-completion, interactive help and running regular shell commands. 50 | 51 | IPython adds `%`-magic commands like `%time` and `%hist` that are available in most of the other editors. This is why you find IPython listed here. 52 | 53 | IPython is very useful to try out a few lines of code quickly, but it does not really count as an editor. 54 | 55 | ---- 56 | 57 | ## Jupyter and Jupyter Lab 58 | 59 | Interactive environment for the web browser. A Jupyter notebook contains Python code, text, images and any output from your program (including plots!). It is a great tool for exploratory data analysis. 60 | 61 | Jupyter Lab offers a slightly different interface, but does the same things under the hood. 62 | 63 | ---- 64 | 65 | ## Notepad++ 66 | 67 | If you must use a text editor on Windows to edit files, use **Notepad++**. **DO NOT USE THE WINDOWS NOTEPAD!** 68 | 69 | ---- 70 | 71 | ## Vim 72 | 73 | To use Vim, you need to learn a lot of keyboard shortcuts. Its unique advantage is that it is the only editor in this collection that you can use through an SSH connection on a remote machine. 74 | -------------------------------------------------------------------------------- /environment_variables.md: -------------------------------------------------------------------------------- 1 | 2 | # Environment Variables 3 | 4 | Environment Variables are like Python variables, but for the entire operating system. They are useful to transport short pieces of information from one program to another. 5 | In software engineering, you will see environment variables used ubiquitously for things like: 6 | 7 | * paths 8 | * language settings 9 | * passwords 10 | * server names 11 | * switching debugging mode on or off 12 | 13 | In this article, you can learn how to set and read environment variables on a Unix system (Linux, MacOS) 14 | 15 | ---- 16 | 17 | ## How to create an environment variable? 18 | 19 | Type into the terminal: 20 | 21 | export MY_TEXT=hello 22 | 23 | Note the following: 24 | 25 | * do not put spaces around the assignment operator `=` 26 | * all environment variables have the same data type. They are **strings** 27 | * `MY_TEXT` is the name of the variable. You choose it 28 | * `hello` is the content of the variable 29 | * add single quotes if your text contains spaces: `'hello world'` 30 | 31 | ---- 32 | 33 | ## How to read an environment variable? 34 | 35 | Type into a terminal: 36 | 37 | echo $MY_TEXT 38 | 39 | The Unix `echo` command is the equivalent of `print()` in Python. 40 | The `$` symbol dereferences the variable. 41 | 42 | If you want to see *all* environment variables that are defined, try the command: 43 | 44 | env 45 | 46 | The output is usually quite a mess. 47 | 48 | ---- 49 | 50 | ## Are the environment variables global? 51 | 52 | No. Each environment has a local *scope*. Each program has its own variables. That means that typing 53 | 54 | echo $MY_TEXT 55 | 56 | in two terminals may yield different results. 57 | 58 | More precisely, when one program starts another program the current environment variables are copied to the new program. 59 | E.g. when you start a Python program from a Unix command line, it receives the current state of `$MY_TEXT` . 60 | 61 | ---- 62 | 63 | ## How can I make environment variables permanent? 64 | 65 | If you want **all** programs to have a certain environment variable, add the `EXPORT` statement to a configuration file in your home directory. 66 | Open the file `.bashrc` (Linux) or `.bash_profile` (MacOS) and add the same line as above: 67 | 68 | export MY_TEXT=hello 69 | 70 | The changes are applied as soon as you start a new terminal. 71 | You can update your environment with: 72 | 73 | source ~/.bashrc 74 | 75 | **Note: Restart your Python editor, if you want it to see the new environment variables.** 76 | 77 | ---- 78 | 79 | ## How to read environment variables from Python? 80 | 81 | You can read an environment variable in two lines: 82 | 83 | import os 84 | 85 | text = os.getenv('MY_TEXT') 86 | 87 | The `os.getenv()` function returns an empty string if the variable is not defined. 88 | 89 | ---- 90 | 91 | ## Are there any environment variables I should know? 92 | 93 | Here are a few common ones: 94 | 95 | | name | description | 96 | |------|-------------| 97 | | PATH | directories in which your terminal is looking for executable programs | 98 | | PYTHONPATH | directories in which Python is looking for importable modules | 99 | | USER | unix username | 100 | | HOME | absolute path to your home directory | 101 | | LANG | language setting | 102 | 103 | If you want to append a directory to an existing `PATH` or `PYTHONPATH`, this expression is useful: 104 | 105 | export PATH=$PATH:/my/new/dir/ 106 | -------------------------------------------------------------------------------- /exercises.md: -------------------------------------------------------------------------------- 1 | 2 | # Exercises 3 | 4 | ### Exercise 1: Track changes 5 | 6 | To track changes to their code over time, a programmer copies the entire source folder whenever they finish a piece of work. 7 | They rename the copy folder so that it contains the current date and copy it to their Google Drive. 8 | 9 | **Questions:** 10 | 11 | - What are disadvantages of this approach? 12 | - When would it definitely fail? 13 | - What is a better alternative? 14 | 15 | ---- 16 | 17 | ### Exercise 2: Debugging 18 | 19 | A programmer debugs a program by reading the code over and over whenever an error occurs. 20 | This takes a lot of time and sometimes they don't find the bug at all. 21 | 22 | **Enumerate as many alternative debugging strategies as possible** 23 | 24 | ---- 25 | 26 | ### Exercise 3: Testing 27 | 28 | A programmer is using a small data file to test their code. 29 | After changing the code, they run the entire program with the test file and inspect the output carefully. 30 | They are generally happy with their approach, and it helps them to remove lots of issues. 31 | 32 | **Questions:** 33 | 34 | - What kind of bugs would you find by testing the program this way? 35 | - What limitations does the approach have? 36 | - What is a complementary strategy to make sure the program is working? 37 | 38 | ---- 39 | 40 | ### Exercise 4: Versions 41 | 42 | Two programmers work together on the same program. Both of them use slightly different Python versions. 43 | After some time, they decide to install exactly the same Python version. 44 | 45 | **Questions:** 46 | 47 | - What do Python versions differ in? Find one example. 48 | - How could the programmers install exactly the same versions of Python libraries? 49 | - One of the programmers is working on another project that requires different library versions. Do they need to get another computer? 50 | 51 | ---- 52 | 53 | ### Exercise 5: Programming Skills 54 | 55 | **Discuss the following questions in a small group:** 56 | 57 | * How would you assume that the velocity of runners distributed across a population? (If you want a statistical answer, check the *Central Limit Theorem*) 58 | * Enumerate a few activities that programmers need to do 59 | * Which of these activities do you enjoy in particular? 60 | * Which do you find difficult? 61 | -------------------------------------------------------------------------------- /github_issues.md: -------------------------------------------------------------------------------- 1 | 2 | # Create Issues on GitHub 3 | 4 | At the start of a project, you may want to plan a bit. 5 | Much has been written about planning a software project, e.g. [User Stories](user_stories.md) or entire processes like Scrum. 6 | However, for a small one-person project, a simple checklist is sufficient. 7 | Let's collect a few features as an **Issue on GitHub**. 8 | 9 | ## Creating a new Issue 10 | 11 | Go to the **Issues** tab on your repository on GitHub. 12 | Press the big **New Issue** button on the right side. 13 | Enter a title for the Issue, e.g. 14 | 15 | Features for the Snake Game 16 | 17 | In the large text field below, you can add what is to be done. 18 | There are plenty of controls to format text and attach files (e.g. screenshots). 19 | One of the buttons lets you create a **Checklist**: 20 | 21 | - [ ] there is a wall around the playing field 22 | - [ ] there is food on the playing field 23 | - [ ] the snake gets longer when it eats food 24 | - [ ] the game is over when the snake hits a wall 25 | 26 | ---- 27 | 28 | ## Annotate the Issue 29 | 30 | On the right side, there are a few extra controls. 31 | These are mostly useful in projects with more persons, but you may want to find out what they do: 32 | 33 | * in **Assignees** you can specify who is responsible for that issue. So you might add yourself here. 34 | * **Labels** describes the type of issue. I tend to label new features as **enhancement** or create my own labels. 35 | * **Projects** and **Milestone** really do not make any sense unless you have 20+ issues open. 36 | 37 | Finish the procss by pressing the big button **Submit new Issue** at the bottom. 38 | The final issue might look like this: 39 | 40 | ![GitHub issue](images/github_issue.png) 41 | 42 | ---- 43 | 44 | ## Referencing the Issue 45 | 46 | You can reference GitHub issues in commits with a message that starts with a `#` and the number of the issue. 47 | Suppose you add and commit the placeholder files `game.py` and `__main__.py` with: 48 | 49 | git add snake/game.py 50 | git add snake/__main__.py 51 | 52 | git commit -m "#1 add placeholder files" 53 | 54 | As soon as you push the change, you should see a note in your issue: 55 | 56 | ![comment in GitHub issue](images/github_issue_comment.png) 57 | 58 | ---- 59 | 60 | ## Exercise 61 | 62 | What features would you want to see in your game? 63 | Add a few more items to your checklist. 64 | 65 | Even if you will not implement everything in the end (which is BTW very common in software projects), 66 | having a checklist helps you to prioritize your work and see your progress. 67 | -------------------------------------------------------------------------------- /good_software.md: -------------------------------------------------------------------------------- 1 | 2 | # How to recognize good scientific software? 3 | 4 | With heaps of data to evaluate, scientific software has become increasingly relevant to create or evaluate results. Lots of software exists, but is it good enough for what you want to do? How can you tell whether you can trust a program to solve your problem? In the first place, you could treat an existing publication as a sign of quality. Unfortunately it is not a particularly reliable one. A publication does not tell you whether the authors are still developing their program further, whether they have stopped maintaining it, or whether the developers have switched fields altogether. 5 | 6 | In this article, I introduce five criteria by which you can recognize good software: 7 | 8 | ![Criteria for good scientific software](images/software_qa.png) 9 | 10 | ### 1. What has the software been used for? 11 | 12 | In the first place, scientific programs are written for a particular purpose or problem. When it is written, authors figure out that it might be useful to other scientists as well. So the authors decide to make ther program available. What is good about this kind of software is that it usually has been proven that it is good for something: you usually will find a reference reporting an experiment supported by the software. 13 | 14 | However, sometimes software is published while such results are still being generated. Then, the program is a prototype and you might be test-driving it, which is not bad in itself, but you need to be prepared for surprises. Therefore, look out for hard data what the program has been used for. If a real research question has been answered, this is much harder evidence than a proof-of-concept or a statistical evaluation of an algorithm. 15 | 16 | The most successful programs are the ones used over a long period of time. They are generally the most stable. If you find evidence like "Over the last two years, the program X has been used by an average of Y persons per month via our website.", you know you are on safe ground. 17 | 18 | ### 2. Are the authors responsive? 19 | 20 | Field-testing a program is good and necessary. Scientific programmers cannot expect the same number of users as your average mobile app. Often enough, they have to do with a few dozen users, and sometimes it is just you and them. The good news is that they have time for your questions. Give the documentation a chance first, but as soon as you get stuck, write to the authors! If they care about their program, you should get a response within a couple of days. Usually, this provides both of you with useful information. 21 | 22 | ### 3. Where is the program available? 23 | 24 | Of course, a program needs to be somewhere physically, so that you can download/install/execute it on your computer (unless you use it via a web interface). There is however more to it than putting a zip file on a web page. You can look out for instance, whether the authors have deposited their program in a public code repository like Sourceforge, Github, or Bitbucket. These havens for open-source software make it easier for someone else to join working on a project - actually, you can browse all the source code on the web pages. When you see them, it is a sign not only of collaborative spirit, but also the program is in a more neat, cleaned-up form than if it were just a collection of files. And you can be sure that it will still be there tomorrow. 25 | 26 | ### 4. How can the program be installed? 27 | 28 | One step further, you can check whether there is an auto-installation procedure: Good signs are if the program is installable via any of PyPi, CPAN, CRAN, Maven or as an Ubuntu package. Also, if there is a separate Windows installer, a mobile app or similar thing that installs the program with a few clicks, it is a sign that the programmers made an effort for you: these things take a lot of time to build. All these tools are indicators of solid engineering practices, so if you see them it tells you the authors have thought about the sustainability of their software. 29 | 30 | ### 5. Can you prove the program works? 31 | 32 | When you use a program, you need to be 100% sure that it does exactly what you think it does. You may very well be unforgiving in this point, especially when calculations are involved that you cannot simply double-check on a pocket calculator (which is probably why you want to use a computer in the first place). The authors are actually responsible of proving that their program does what is written in the manual. Because software changes within days or weeks, simply referring to the results section of a publication is not enough! 33 | 34 | **How can you verify then that a program works?** 35 | 36 | Each scientific program should include at least one set of sample data. There should be an instruction how to use the sample data and exactly what output it produces. Sometimes, this approach is broken down into small steps: a cookbook explaining small actions and their effect. Eventually, you will find an automatic test suite. This is a script that automatically checks whether different parts of the program work correctly. When you see a message like 37 | 38 | 110 of 110 tests OK. 39 | 40 | you know that at least everything the developers felt important to check works. 41 | 42 | All of these methods have in common that some input data with a known output is used. They allow you to verify whether the program works now and on your computer, as opposed to 'A long time ago, far far away...' 43 | 44 | ### Conclusions 45 | 46 | If a program fails several of the above quality indicators, it does not mean that the program is bad or that the authors can't program. Probably you are seeing only a tiny bit of all the work that went into the software. But it also means that your risk of usage is higher. If the software you are using is a prototype (and many projects never leave that stage), one of the best things you can do is to contact the authors directly. This is beneficial for both of you. 47 | 48 | The list in this post is incomplete. If you are an author and I missed your favorite engineering technique, or if you use scientific software and have a suggestion what would make your life easier, drop me a line. 49 | 50 | ### Acknowledgements 51 | Thist text emerged from a discussion round at the GFZ Potsdam, with special support from Bernadette Fritsch, Björn Brembs, Dominik Reusser and Jens Klump. 52 | -------------------------------------------------------------------------------- /images/Figure_test_driven_development_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/Figure_test_driven_development_3.png -------------------------------------------------------------------------------- /images/InputProcessingOutput.odp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/InputProcessingOutput.odp -------------------------------------------------------------------------------- /images/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/cloud.png -------------------------------------------------------------------------------- /images/cloud.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 64 | walls snake stuff to eat controls 109 | 110 | -------------------------------------------------------------------------------- /images/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/cover.jpg -------------------------------------------------------------------------------- /images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/cover.png -------------------------------------------------------------------------------- /images/cover.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 58 | 68 | Scientific SoftwareEngineering in Python Kristian & Magdalena Rother 118 | 123 | 128 | 133 | 138 | 143 | 148 | 153 | 158 | 159 | 165 | 170 | 180 | 190 | 200 | 210 | 217 | 223 | 232 | 241 | 250 | 255 | 264 | 273 | 282 | 291 | 300 | 305 | 310 | 315 | 320 | 329 | 338 | 347 | 356 | 364 | 373 | 382 | 383 | 384 | 385 | -------------------------------------------------------------------------------- /images/cover_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/cover_small.jpg -------------------------------------------------------------------------------- /images/crc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/crc.png -------------------------------------------------------------------------------- /images/decomposing_stories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/decomposing_stories.png -------------------------------------------------------------------------------- /images/github_issue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/github_issue.png -------------------------------------------------------------------------------- /images/github_issue_comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/github_issue_comment.png -------------------------------------------------------------------------------- /images/introspection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/introspection.png -------------------------------------------------------------------------------- /images/io_def1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/io_def1.png -------------------------------------------------------------------------------- /images/io_def2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/io_def2.png -------------------------------------------------------------------------------- /images/io_def3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/io_def3.png -------------------------------------------------------------------------------- /images/legacy_graph_simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/legacy_graph_simple.png -------------------------------------------------------------------------------- /images/legacy_graph_simple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 27 | 32 | 33 | 40 | 45 | 46 | 53 | 58 | 59 | 66 | 71 | 72 | 79 | 84 | 85 | 92 | 97 | 98 | 105 | 110 | 111 | 112 | 131 | 133 | 134 | 136 | image/svg+xml 137 | 139 | 140 | 141 | 142 | 143 | 147 | 157 | 163 | 170 | 177 | 184 | 191 | 192 | 200 | everythingfine dirtywork Arrgh!!! dirtywork complexity engineering quality 299 | 300 | -------------------------------------------------------------------------------- /images/mind_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/mind_map.png -------------------------------------------------------------------------------- /images/pair_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/pair_user.png -------------------------------------------------------------------------------- /images/pbis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/pbis.png -------------------------------------------------------------------------------- /images/program_publish_prove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/program_publish_prove.png -------------------------------------------------------------------------------- /images/roadmap/python_roadmap.md: -------------------------------------------------------------------------------- 1 | 2 | # Roadmap for Python Beginners 3 | 4 | * Linux command line 5 | * git 6 | 7 | ## Path of the Web Developer 8 | 9 | * Bootstrap 10 | * Djangogirls tutorial 11 | 12 | bottle, requests, regex, bs4 13 | 14 | ## Path of the Data Analyst 15 | 16 | * Jupyter notebook 17 | * matplotlib 18 | * pandas 19 | * scikit-learn 20 | 21 | ## Path of the Scientific software developer 22 | 23 | * virtualenv 24 | * algorithms 25 | * classes 26 | * automated testing 27 | 28 | -------------------------------------------------------------------------------- /images/roadmap/python_roadmap_DE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/roadmap/python_roadmap_DE.png -------------------------------------------------------------------------------- /images/software_qa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/software_qa.png -------------------------------------------------------------------------------- /images/software_quality.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 24 | 28 | 32 | 33 | 40 | 49 | 58 | 67 | 76 | 85 | 86 | 105 | 107 | 108 | 110 | image/svg+xml 111 | 113 | 114 | 115 | 116 | 117 | 121 | 132 | 138 | 144 | 150 | 155 | 156 | 162 | 168 | 174 | 179 | 180 | 186 | 189 | 195 | 201 | 206 | 207 | 208 | 214 | 220 | 226 | 231 | 232 | 238 | 244 | 250 | 255 | 256 | goodscientificsoftware? being used easy totest available responsiveauthors easy toinstall Quality criteria shed light on a black box 388 | 389 | -------------------------------------------------------------------------------- /images/starmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/starmap.png -------------------------------------------------------------------------------- /images/toolbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/toolbox.png -------------------------------------------------------------------------------- /images/userstory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/userstory.png -------------------------------------------------------------------------------- /images/warning_signs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/warning_signs.png -------------------------------------------------------------------------------- /images/waterfall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krother/software-engineering-python/0f40f94b88bd625ab257da2565a47f0cd4f49a05/images/waterfall.png -------------------------------------------------------------------------------- /impostor.md: -------------------------------------------------------------------------------- 1 | 2 | # Impostor-Syndrome 3 | 4 | ## What is Impostor Syndrome? 5 | 6 | Programming is sometimes frustrating. 7 | Sometimes you get so frustrated that you start to believe that you are incompetent and should not be programming at all. 8 | Looking at the code of other programmers, it is easy to find examples that someone did something better than you. 9 | 10 | This is the so-called **Impostor Syndrome**. It is a warning message from your brain that it is currently overloaded. 11 | In other words: *"time for a break!"*. 12 | 13 | ---- 14 | 15 | ## Does it go away? 16 | 17 | You should make the Impostor Syndrome a good friend, because it will be with you as long as you program. 18 | Even after 30 years, I still come across code I don't understand, problems I cannot solve, programs from seemingly unattainable genius programmers. 19 | 20 | But often it turns out that only one or two tricks are missing to achieve something similar. 21 | Once you can look back at code you wrote a few weeks earlier, you'll see how far you've come. 22 | 23 | ---- 24 | 25 | ## What can you do? 26 | 27 | The worst thing you can do is to bang your head against the same wall over and over. 28 | The main trick is to refocus your brain to look at the situation from another angle. 29 | All of the following help your brain switch gears: 30 | 31 | * take a break (no, an off-screen break) 32 | * go to sleep 33 | * talk to someone 34 | * read about the tools / libraries / algorithms you are working with 35 | * solve a smaller version of the problem first 36 | * draw a solution on paper 37 | -------------------------------------------------------------------------------- /index.rst: -------------------------------------------------------------------------------- 1 | Python Software Development 2 | =========================== 3 | 4 | What this guide is about? 5 | ------------------------- 6 | 7 | This guide is for you if you are writing programs with more than 500 8 | lines. 9 | 10 | You know how to write Python code, but have realized that creating a 11 | piece of software is more complex. You are facing questions like: 12 | 13 | - How to clean up my code? 14 | - How to make sure my program works? 15 | - How to install my program on multiple computers? 16 | - How to keep the program running over time? 17 | - How to deliver the program to other people? 18 | 19 | Below you find development tools and techniques that help you to write 20 | programs that get the job done and don’t fall apart. 21 | 22 | 23 | 24 | Planning and Design 25 | ------------------- 26 | 27 | .. toctree:: 28 | :maxdepth: 1 29 | 30 | interface.md 31 | user_stories.md 32 | github_issues.md 33 | crc_cards.md 34 | project_checklist.rst 35 | 36 | -------------- 37 | 38 | Coding Strategies 39 | ----------------- 40 | 41 | .. toctree:: 42 | :maxdepth: 1 43 | 44 | writing_code.md 45 | coding_style.md 46 | refactoring/README.md 47 | 48 | -------------- 49 | 50 | Advanced Stuff 51 | -------------- 52 | 53 | .. toctree:: 54 | :maxdepth: 1 55 | 56 | loc.md 57 | tech_debt.md 58 | project_templates.md 59 | project_management.md 60 | legacy_code.md 61 | documenting.md 62 | citable_code.md 63 | 64 | -------------- 65 | 66 | Other Things 67 | ------------ 68 | 69 | .. toctree:: 70 | :maxdepth: 1 71 | 72 | editors.md 73 | environment_variables.md 74 | good_software.md 75 | impostor.md 76 | programming_language_exercise.md 77 | exercises.md 78 | 79 | -------------- 80 | 81 | .. topic:: License 82 | 83 | © 2023 `Kristian Rother `__ and `Magdalena Rother `__ 84 | 85 | Usable under the conditions of the Creative Commons Attribution Share-alike License 4.0 (CC-BY-SA 4.0). 86 | See `creativecommons.org `__ for details 87 | -------------------------------------------------------------------------------- /interface.md: -------------------------------------------------------------------------------- 1 | 2 | # Define an Interface 3 | 4 | One of the most difficult questions about writing your program is deciding where to start. 5 | You probably would like to divide the program into smaller components. 6 | So for a snake game, you would separate the snake from the playing field separate the walls from the food and so on. 7 | At this stage, these concepts are still floating in space: 8 | 9 | ![cloud](images/cloud.png) 10 | 11 | When you think about converting the words in this cloud to code, you discover a problem: 12 | Should there be a Snake class? Could you implement the snake with functions instead? Should you implement the tail as a list, a dictionary or something else? 13 | 14 | In this article, we will approach these questions. 15 | 16 | ## Change is inevitable 17 | 18 | One key property of software is that it will change over time. 19 | While you develop, you learn more about the problem you are solving. 20 | This means that your initial implementation will turn out to be not so great, and you will have to modify it. 21 | This is also common when you are maintaining software over a longer period, and your requirements slowly change. 22 | This change is inevitable. 23 | 24 | You can think about the classes, functions and data structures of your program as the interior design of a building. 25 | It is very important for the functionality and comfort where you place the furniture. 26 | But you also know that you may want to replace or rearrange the furniture sometimes. 27 | The design of the interior is temporary, and the design of classes, functions and data structures is temporary, too. 28 | 29 | Practically this means: **In the beginning, it does not matter much whether we implement the snake as a class or a list or a bunch of functions, because it is going to change anyway.** 30 | What we need is to embed the snake component in a program structure that makes it easy for us to change its implementation in the future. 31 | 32 | Where do we expect change in the snake game? 33 | First, the game mechanics could change: There could be more walls or multiple food items being added in the future. 34 | Second, we might want to change the user interface as well, replacing the terminal view by actual graphics. 35 | A key point is that these are two types of changes. 36 | We should separate them. 37 | 38 | ## The Interface 39 | 40 | What we are looking for at this stage is to identify something that does not change: the walls, doors and windows of your building (or software). 41 | We call these **interfaces**. 42 | Interfaces are what connects one part of your program to another. 43 | While both parts may change, the interface should remain stable. 44 | 45 | Let's separate the **User Interface** of the snake game from the **Game Logic**. 46 | For that, we will define a `SnakeGame` class that will be used as the only point of communication by the user interface: 47 | 48 | class SnakeGame: 49 | 50 | running: bool 51 | 52 | def start_game(): 53 | ... 54 | 55 | def set_direction(): 56 | ... 57 | 58 | def update(): 59 | ... 60 | 61 | def get_tiles(): 62 | ... 63 | 64 | Instead of a class, you could do the same using functions, JSON objects or something else. 65 | 66 | ## An interface is a contract 67 | 68 | A key property of an interface is that we expect it to be stable over a long time. 69 | With a stable interface, we can do things that make little sense with temporary components: 70 | 71 | * implement different graphical clients that use the same game logic 72 | * altrnative implementations of the interface (e.g. implement pacman instead of snake) 73 | * write automated tests against the interface 74 | * write documentation for the interface 75 | 76 | Concluding, the user interface can use the `SnakeGame` interface without knowing anything about the game logic. 77 | Likewise, the `SnakeGame` class can handle the game logic (using any extra classes or functions it needs) without knowing how the playing field is displayed or how the player controls the snake. 78 | 79 | 80 | ## Further Reading 81 | 82 | See [Lehmanns Laws of Software Evolution](https://en.wikipedia.org/wiki/Lehman%27s_laws_of_software_evolution) 83 | -------------------------------------------------------------------------------- /legacy_code.md: -------------------------------------------------------------------------------- 1 | # How to work with legacy code? 2 | 3 | ## In this chapter you can learn: 4 | 5 | * why problems with legacy code emerge 6 | * how to quickly assess the complexity and engineering quality of a project 7 | * strategies to overcome initial difficulties 8 | 9 | ## The Modomics story 10 | 11 | In March 2007 I inherited the [Modomics database](http://www.genesilico.pl/modomics) from Staszek, a MSc student in the lab. Staszek handed me the code and the server passwords. Then he moved to Germany. Although he did whatever he could to support me by email, a sackful of knowledge moved away with him. 12 | 13 | ![Modomics](images/modomics.jpg) 14 | 15 | There was a hard deadline for publication in June. In May, the hard disk of the server crashed. I restored most of the code from the SVN repository and loaded the database dump. However, some features were lost on the way. I was determined to not only fully recover the project, but also to add enough value to submit the publication on time. 16 | 17 | Working on the code was tough: *"What does this mean? How does this work? Why is this character on the web page three positions further to the left than it should?"* I frequently found myself tracing Python & HTML code line by line. As a result, adding even small features and debugging became a daunting task. 18 | 19 | When the deadline drew near, I worked literally every minute, including late evenings and weekends, until the very last moment. I was constantly overslept and emotionally brittle to the point of resignation. It took me a year to realize the correct term for this: burnout. 20 | 21 | I missed the deadline, or to be precise, my supervisor hit the **STOP** button in time. He decided to postpone submission by one year. An extra year was the best thing that could happen to the project and its maintainer. First of all, I relaxed. Second, I spent more time talking to scientists using the website and understood better what they needed. Then, I cleaned up many big and small issues: I drew a data model for the database, refactored smaller components with descriptive names and wrote unit tests. 22 | 23 | In the end, I had rewritten much of the code. The site was working, the publication got accepted. 24 | 25 | Finally, after two more years, it was time to hand over the project to my successors Sebastian and Kaja. The first thing Sebastian did was that he dumped most of my code and rewrote the site in Django within two weeks. Kaja kept on maintaining the server diligently for years, and so the database lives on until the day I write these lines, with different code, but the same vision it was originally created with. 26 | 27 | What I learned is that taking over a program from someone else is difficult. 28 | 29 | 30 | #### Project summary 31 | 32 | | Name | Modomics | 33 | |------|----------| 34 | | Summary | Web database of modified RNA nucleotides. | 35 | | Duration | 2006 - 2014 | 36 | | Developers | 2 coders (2009) | 37 | | Stakeholders | 2 senior scientists, 4 data curators (2009) | 38 | | Size | ~10000 Python LOC | 39 | | Technologies used | TurboGears web server
PostGreSQL database
Biopython
PIL | 40 | | Development tools used | bug tracker (TRAC)
automatic tests (partial)
SVN repository
User Stories
Entity-relationship diagram | 41 | | Reference | Machnicka MA, Milanowska K, Osman Oglu O, Purta E, Kurkowska M, Olchowik A, Januszewski W, Kalinowski S, Dunin-Horkawicz S, Rother KM, Helm M, Bujnicki JM, Grosjean H. MODOMICS: a database of RNA modification pathways: 2012 update. Nucleic Acids Res 2013 Jan 1;41(D1): D262-D267 | 42 | 43 | ---- 44 | 45 | ## Assessing a Legacy Project 46 | 47 | When you take over a project, you need to find out first what you got yourself into. There are two aspects to consider before you can decide what to do: 48 | 49 | 1. How complex is the project? 50 | 2. How well-engineered is the code? 51 | 52 | Intuitively, you would expect the according graph to look like this: 53 | 54 | ![Simple assessment graph](images/legacy_graph_simple.png) 55 | 56 | In the section on **Code Metrics**, you will find questions to assess complexity and engineering quality in a project *before* you take it over. 57 | 58 | 59 | ## What you can do when you take over a project 60 | 61 | Once you figured out what situation you got yourself into, you probably want to get to work. What can you do to get a firm hold on the code you inherited? Here you find eight options. 62 | 63 | ### 1. Abandon 64 | There may be good reasons to jump ship while still in the harbour. A clean decision to stop a project altogether can save you months or even years of pain and struggle. If it turns out that the project is unmaintainable later, abandoning it immediately is much cheaper. Convincing others of this option will be difficult. Consider it a last resort. 65 | 66 | ### 2. Rewrite 67 | Imagine you have built a small Cessna airplane, but figure out that you really need a Boeing 727. Nobody would say *"Oh, great, you have a pair of wings already."* You would need to build an entirely new plane. It is the same with programming. There is nothing bad about throwing away code. It does not take up space and does not pollute the environment. If the code works, but you can't work with it reasonably, write it from scratch. 68 | 69 | Rewriting a program happens frequently, and often it is faster than fixing the old one. Fred Brooks (The Mythical Man-Month, 1982, p115ff.) recommends even to plan for throwing away the first design. Incorporating some parts from existing code into a new design is less risky than assuming all existing parts can be reused. 70 | 71 | ### 3. Reduce complexity 72 | The trouble of maintaining a project successfully can be diminished by kicking out some features. Possibly the project does things that have a very low priority or turned out to be not important anyway. 73 | 74 | One way to increase maintainability with a simple decision is to reduce the number of supported platforms to one. Maintaining a web-only or source-only version instead of three operating systems and a browser interface in parallel gets plenty of problems out of the way. Consider focusing on one platform until you feel familiar enough with the project to expand. 75 | 76 | ### 4. Cleanup time 77 | Cleaning up improves the quality of your project. Increasing engineering quality takes time. You may consider spending at least some to add things that were missing from the engineering score. Create a repository. Create an installer. Write a few automated tests (this will pay off very quickly). Run **pylint** and improve your score. The latter is a great way to familiarize with the code while improving things. 78 | 79 | ### 5. Divide and conquer 80 | Separate portions of your code and clean them up one by one. Create separate functions, classes, modules, libraries from larger blobs and test them. If done consequently, you can either isolate the messy parts or reduce them to small portions that you can handle with ease. 81 | 82 | Michael Feathers proposes a five-step process to divide code into smaller chunks (*Feathers, Michael. Working Effectively With Legacy Code, Prentice Hall PTR, 2005*): 83 | 84 | 1. Find change points in the code (portions that can be separated). 85 | 2. Find test points. 86 | 3. Break dependencies. 87 | 4. Write tests. 88 | 5. Change the code and refactor. 89 | 90 | 91 | ### 6. Identify people who can support you 92 | 93 | Code does not exist by itself; it is maintained by persons. When you start work on a legacy project, there are four main players who might be able to help you: 94 | 95 | #### The former developer 96 | Do you have a chance to meet the former developer once per week? Daily? Whenever you need? Is he able to support you directly during a transition period? Can you meet face-to-face? 97 | 98 | #### Other developers 99 | Are the more people who have worked with the code? Are they still actively involved? A co-developer is a valuable source of information, because often they view the code from a similar perspective as you. 100 | 101 | #### Users 102 | Does the program have active users? Can you talk to them on a regular basis? Do you need the program yourself? Real users help you to understand what matters and motivate you to keep going. 103 | 104 | #### Your supervisor 105 | Is your supervisor aware of the state of the project? Can you discuss technical issues with him or a trusted mentor? Do you receive a clear vision or next major step for the project? Maybe your supervisor has been in a similar spot before and give you some valuable hints. 106 | 107 | ---- 108 | 109 | ### 7. Mission Impossible Game 110 | The [Mission Impossible Game](http://www.gamestorming.com/games-for-design/mission-impossible/) is a brainstorming method. The art of brainstorming is to first ask the right question. Then take decisions. 111 | 112 | In the Mission Impossible Game you collect actionable ideas to work with legacy code. Ask: *"How to prepare the next release of our program in one day?"*. Collect ideas for half an hour. 113 | 114 | Then, choose the ideas most relevant for the project vision, discard the others and get working. 115 | 116 | ### 8. Change one thing at a time 117 | How many of the main parameters of the project will change the moment you take over? Things that could change include the team composition, project size, goals, features and platforms. Ideally only one parameter should be changed at a time. 118 | 119 | The moment you take over as main developer, the team composition changes in any case. That means, nothing else should change. Spend some time making yourself comfortable with the code, working on small issues. You may allocate a week or more to learn a technology crucial to the project which you don't know yet. Don't start revolutions on day one. When you feel the code has become *yours*, it is time to enter the next development stage. 120 | 121 | ## Things that help 122 | 123 | * give an incoming programmer authority to change everything. 124 | * give an outgoing programmer an incentive to contribute (publications, open-source) 125 | * encourage other people to take side roles in the project early. --> you have a backup, they have a side project, and the main dev is forced to explain his code to someone else 126 | * Change one parameter at a time (Vision, Features, Platform, Developers) 127 | -------------------------------------------------------------------------------- /loc.md: -------------------------------------------------------------------------------- 1 | 2 | # Counting Lines of Code 3 | 4 | ## How much code is there? 5 | 6 | In a small project, you can simply roll up your sleeves and start fixing things. In a big project, however, you need to keep an overview what parts of a project local changes might affect. 7 | 8 | More code means more work. The amount of code gives you a ballpark figure of how much you need to read and understand before getting to work. 9 | 10 | You can count the total number of files on Unix: 11 | 12 | find . -name "*.py" | wc -l 13 | 14 | A common metric is the number of **lines of code (LOC)**. The following command gives you the total number of LOC for all Python files in a Python directory tree: 15 | 16 | find . -name "*.py" | xargs wc -l 17 | 18 | Empty lines, docstrings and comments are counted, too, as they are part of the source code. 19 | 20 | ---- 21 | 22 | ## What does the LOC number tell me? 23 | 24 | Some implications of the LOC: 25 | 26 | #### Small (<100 LOC) 27 | 28 | Small Python programs such as standalone scripts do not require a lot of structure. 29 | The may or may not contain functions or other structural elements. 30 | In case the code proliferates beyond control, a small program is easy to throw away or rewrite. 31 | 32 | #### Medium (<1000 LOC) 33 | 34 | In a medium-sized program, more structure is necessary. 35 | You will need to use some of the structuring options Python offers. 36 | Most likely these will be functions. 37 | But if you want to mix in a few classes or split the code over multiple modules that is fine as well. 38 | If you have not started using version control yet, it will be hard to move beyond 1000 LOC without. 39 | 40 | #### Large (<10000 LOC) 41 | 42 | In a large program, you will need classes to manage complexity. 43 | Unless you are a fan of large source files, distributing the code over multiple files/folders is a good idea. 44 | To maintain a source code of that size, automated tools for testing and linting are indispensible, especially during refactoring. Consider using a build tool. 45 | 46 | #### Very Large (<100000 LOC) 47 | 48 | Very large programs are structured into multiple folders with modules. Sometimes you will find a very large program divided into several pip-installable packages. 49 | In a very large program, it is crucial to have a clean build/release process, and probably some continuous integration. 50 | You might also want to maintain documentation for a program in this size. You shouldn't be surprised to see many configuration files appear in addition to Python files. 51 | 52 | #### Huge (100000+ LOC) 53 | 54 | Python software of this size does exist, mostly in the form of well-known libraries. 55 | Usually, these evolve over years and involve dozens or hundreds of developers. 56 | In other cases, a huge software might consists of multiple sub-packages or even programming languages so that the LOC number is not easy to determine. Also, at this size the lack of strong typing and strict encapsulation in Python may get in the way a lot, so that other languages may be a better choice. 57 | -------------------------------------------------------------------------------- /programming_language_exercise.md: -------------------------------------------------------------------------------- 1 | 2 | # Exercise: Programming languages 3 | 4 | * pick a programming language 5 | * find an example piece of code 6 | * try to understand the code 7 | * look for common things and differences to Python 8 | * research strengths, weaknesses and applications of that language 9 | * present the code example 10 | * fill a table together with the most important properties of languages 11 | -------------------------------------------------------------------------------- /project_checklist.rst: -------------------------------------------------------------------------------- 1 | 2 | Checklist for a Backend Project 3 | =============================== 4 | 5 | Here is a list of things you may want to keep in mind when starting a project that involves a Python backend. 6 | I wrote much of it following the tracks of the [12 Factor App](https://12factor.net/) paradigm: 7 | 8 | Project Communication 9 | --------------------- 10 | 11 | - What is the business value the project generates? 12 | - Who is on the project team? 13 | - How does the team communicate (face2face, chat, email, calendar, file exchange, Wiki, JIRA)? 14 | - Does the team work in iterations? How long are they? 15 | - Is there a requirements document? Is it updated? 16 | - Is there a single git repository available? 17 | 18 | Architecture 19 | ------------ 20 | 21 | - What are your main use cases? 22 | - What is your data flow? 23 | - Do you have a pattern for the systems overall architecture (layered, star-shaped, hexagonal etc.) 24 | - How many separate physical machines does the project require? 25 | - Does a prototype exist? 26 | - What are your most important non-functional requirements? 27 | - What legal requlations do you need to comply to (GDPR etc.) 28 | - Are you using containers? 29 | - Do you need enough containers to justify Kubernetes? 30 | - welche Container gibt es? 31 | - How does the release/deployment process look like? 32 | - Will there be separate test/staging servers? 33 | - What special security / safety risks exist? 34 | 35 | Credentials 36 | ----------- 37 | 38 | - How is authentication managed? 39 | - What roles are defined in the project? 40 | - Do end users need to authenticate? 41 | - Which protocols for authentication do you need (SSL, OAUTH2, two-factor-auth, Kerberos etc.) 42 | - Is there a central authentication service? 43 | - Within services, are credentials stored mainly in the environment? 44 | - What is the procedure when a team member leaves? 45 | - What is the procedure when you learn that your credentials have been compromised? 46 | 47 | Databases 48 | --------- 49 | 50 | - How much data are you expecting (now and in the future)? 51 | - How much traffic are you expecting? 52 | - Is there a data model already? 53 | - Which database system(s) do you choose? Consider: ease of use, rigor of the data model, core features, tool support, scalability, speed. 54 | - How will you migrate the data when the data model changes? 55 | - What data import processes do you anticipate? 56 | - Will it be possible to re-create the entire database from scratch? 57 | - How are backups of the database handled? 58 | 59 | Web Servers 60 | ----------- 61 | 62 | - What availability do you need? 63 | - Does the project expose an API? 64 | - Is the API going to be public? 65 | - Will there be a HTML front-end? 66 | - Will there be a mobile app? 67 | - Will there be a proxy server (e.g. nginx)? 68 | - Will the backend use an ORM? 69 | - Will you use pydantic models for API endpoints? 70 | - How will you manage requirements? 71 | - Which language(s) will you use for the back-end/front-end parts? 72 | - How will you manage versions of the software (front-end and back-end)? 73 | 74 | Software Quality 75 | ---------------- 76 | 77 | - How will you write automated tests for the backend? 78 | - How will you write automated tests for the front-end? 79 | - How will you write end-to-end tests covering both parts? 80 | - Can you run slim tests against the production server? 81 | - Which CI tool are you going to use? 82 | - What software quality gates will you apply (pyflakes, mypy)? 83 | - Can you autmatically check for known security issues? 84 | - How is logging done? How can you access logs? 85 | - How is monitoring done (who is messaged when something goes wrong)? 86 | - do you have test users? 87 | -------------------------------------------------------------------------------- /project_management.md: -------------------------------------------------------------------------------- 1 | 2 | # Software Project Management 3 | 4 | **Managing software projects is difficult.** 5 | 6 | ## Uncertainty 7 | 8 | **Do you know where you are going to be next week?** Probably, you do. 9 | 10 | **Do you know where you are going to be in the summer three years from now?** Probably not. 11 | 12 | There is a lot of uncertainty in the second question. You can't look ahead too far. When developing software it is the same: There is a lot of uncertainty, only the horizon begins to become blurred much earlier: within weeks, days or even hours. 13 | 14 | Programming projects change and evolve for a multitude of reasons: 15 | 16 | * your users request changes. 17 | * bugs need to be fixed. 18 | * the libraries you use evolve. 19 | * external requirements (e.g. regulations) change. 20 | * you have ideas you want to implement. 21 | 22 | Change is inevitable. 23 | 24 | ---- 25 | 26 | ## Waterfall 27 | 28 | Naively, one could try to structure a programming project as consecutive steps, known as the **Waterfall model**. 29 | 30 | ![Waterfall](images/waterfall.png) 31 | 32 | Because of the nature of change, the waterfall model only works for projects where you know the problem *and* the technologies very well. Even then, the program will need to be maintained afterwards. 33 | 34 | In practice, there are no finished programs. 35 | 36 | It is more helpful to think of programming as an ongoing activity, like gardening. 37 | 38 | ---- 39 | 40 | ## Supervisors 41 | 42 | One thing that makes software projects difficult for managers is that they cannot see a half-finished program. Many times, they will ask questions like: 43 | 44 | "When will the program be finished?" 45 | 46 | It is very difficult for non-programmers to understand that this question is meaningless. You might as well 47 | 48 | Therefore it is challenging to make managers happy and get them out of the way at the same time. The key to make managers contribute something useful is of course *communication*. Some things that might help you: 49 | 50 | * learn what real-world problem you are solving. 51 | * develop clear, specific goals together. 52 | * write a specification. Split it into smaller steps (e.g. User Stories and Use Cases). 53 | * do not discuss whether or not to use tools like testing. You wouldn't discuss whether to use `for` or `while` with your manager either. 54 | * demonstrate a small working version early. 55 | * learn about the Agile methodology, but do not become attached to it 56 | 57 | ---- 58 | 59 | ## What does 'done' mean? 60 | 61 | Have you encountered the following situation in a programming project? The project is divided into tasks, the tasks are placed in an electronic tracking system or as cards on a task board. After some time, programmers declare they are finished: Some come up with a basic solution very quickly and prefer to take care of special cases and cleanup work later. Others invest a lot of time into building a solid, maintainable structure. The former carries the risk that problems get swept under the rug and technical debt is accumulating, the latter that tasks linger in a half-done state forever. Moreover, your team may disagree to what extent a task needs to be implemented to count as “done”. How can you as a project manager know what your team means by “done”? 62 | 63 | Historically excessively detailed specifications were used to describe what a program should do (and often still are). But in many programming projects this approach is too clumsy. User Stories, cards describing features in a few words, provide a short objective description, but they do not represent the engineering details. Methodologies like SCRUM admit that “done” in programming can be interpreted in many ways. They claim that an important prerequisite for productivity is that “done” means the same thing to all people involved. This has resulted in a pragmatic solution: the “definition of done”. 64 | 65 | The “definition of done” is a convention the programming team creates. It is a list of simple, criteria a task must pass in order to count as 'done'. The definition could include “the code is in the repository”, “the code has been released on the server”, “automatic tests have been written” etc. All criteria must be objective and easy to check. The “definition of done” represents a quality standard and reflects the engineering practices used by the team. 66 | 67 | Creating and agreeing on a “definition of done” may require intensive discussion among team members, but it is worth it: Once established, the team knows what everyone of them means when they say something is “done”, a quality standard is established, and the experience helps the team to grow together. 68 | -------------------------------------------------------------------------------- /project_templates.md: -------------------------------------------------------------------------------- 1 | 2 | # Starting a Python project with pyscaffold 3 | 4 | When starting a small program from scratch, you probably don't need to worry much about organizing files and directories. It is OK to keep program and data files in the same place. But as the project grows you need to organize files differently. 5 | 6 | A good directory structure helps you to: 7 | 8 | * separate data and code 9 | * separate program and tests 10 | * extract program releases easily 11 | * keep huge files away from small ones 12 | 13 | Generally, in a good directory structure there is one obvious place for every file. 14 | 15 | Fortunately, there is a de-facto standard for Python projects. The **pyscaffold** tool creates this structure for you. In this text, you can learn about **pyscaffold**, the directories in a Python project and a few important files. 16 | 17 | ---- 18 | 19 | ### Setting up a project with pyscaffold 20 | 21 | The command-line-tool **pyscaffold** creates the directory structure for a Python project. To install and use **pyscaffold**, start from your main folder or wherever you keep your projects, and type: 22 | 23 | sudo pip install pyscaffold 24 | putup myproject 25 | 26 | Where `myproject` is the name of your Python package. You should see that **pyscaffold** has created a `myproject/` directory with a couple of subdirectories and files: 27 | 28 | docs/ 29 | myproject/ 30 | tests/ 31 | AUTHORS.rst 32 | MANIFEST.in 33 | requirements.txt 34 | LICENSE.txt 35 | README.rst 36 | setup.py 37 | versioneer.py 38 | 39 | Let's have a look what each of these does. 40 | 41 | ---- 42 | 43 | ### Directories 44 | 45 | #### docs/ 46 | This is the place to keep documentation. Initial files for use with the document generator **Sphinx** are already there. So if you have **Sphinx** installed, you can create and view your documentation with: 47 | 48 | cd docs 49 | make html 50 | firefox _build/html/index.html 51 | 52 | #### Python directory 53 | Here your Python files have their home. You can add your own Python modules and packages here. The `__init__.py` file marks the directory as a Python package. The file `_version.py` helps with assigning versions, you don't have to edit it. 54 | 55 | #### tests/ 56 | This is where automated tests are stored. Apart from an `__init__.py` file, the directory should be empty. Nevertheless you can already run the test suite with 57 | 58 | python setup.py test 59 | 60 | #### Other directories 61 | Sometimes, you will also find a `bin/` directory for binary files in a Python project. As soon as you start creating releases of your program, the directories `build/` and/or `dist/` will appear as well. 62 | 63 | Of course, you can add your own directories. For instance, it is generally wise to have a separate directory for data, especially if you have a lot of it. 64 | 65 | ### Files 66 | 67 | #### setup.py 68 | The `setup.py` file is the heartpiece of your project. It contains instructions how to build your program, create releases, run tests. You can configure `setup.py` to release your program to the **Python Package Index** or to create an executable with **py2exe** on Windows. 69 | 70 | The most common use is to build your program. The following command collects everything that is needed to run the program'in the `build/` directory: 71 | 72 | python setup.py build 73 | 74 | You can also install the program alongside other Python libraries on your system: 75 | 76 | python setup.py install 77 | 78 | Finally, you can create a `.tar.gz` archive for distributing the containing all files specified in the `MANIFEST.in` file: 79 | 80 | python setup.py sdist 81 | 82 | 83 | #### README.rst 84 | The `README.rst` file in the main project directory is the first thing most developers read if they consider installing the program or are simply curious. This file should contain a brief summary of what your program does, how a simple usage looks like and where to read more. 85 | 86 | Having a README file in the ReStructuredText format (`.rst`) allows you to use markup language that is used by **github** or **bitbucket** to format your pitch nicely. 87 | 88 | #### AUTHORS.rst 89 | A simple list of developers and their contact info. 90 | 91 | #### LICENSE.rst 92 | A document covering the legal aspects. By default, you will find a copyright message and your username there. Feel free to paste any software license there. 93 | 94 | #### MANIFEST.in 95 | The `MANIFEST.in` file contains a list of file names or file name patterns. This list is being used to identify fiĺes that should be included in builds and source code releases (e.g. by default, you won't find the tests there). 96 | 97 | #### versioneer.py 98 | A script that facilitates updating version numbers with git. 99 | 100 | #### requirements.txt 101 | This file is used by **pip** to resolve dependencies. If your program requires specific version numbers of libraries, you can write them into *requirements.txt*. The following commands installs all the dependencies: 102 | 103 | pip -r requirements.txt 104 | 105 | ### Benefits of using pyscaffold 106 | Of course, you could set up most of the above with a few Linux commands as well. The benefit of using **pyscaffold** is that you ensure consistency over multiple projects from the very beginning. Also, starting with a cleanly written `setup.py` script allows you to create a software that can be built, installed and distributed over its entire life cycle. 107 | -------------------------------------------------------------------------------- /refactoring/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Kristian Rother 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 | -------------------------------------------------------------------------------- /refactoring/solution/01-extract-module/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | from text_en import TEXT 8 | 9 | 10 | def travel(): 11 | 12 | print(TEXT["OPENING_MESSAGE"]) 13 | 14 | planet = "earth" 15 | engines = False 16 | copilot = False 17 | credits = False 18 | game_end = False 19 | 20 | while not game_end: 21 | 22 | # display inventory 23 | print("-" * 79) 24 | inventory = "\nYou have: " 25 | inventory += "plenty of credits, " if credits else "" 26 | inventory += "a hyperdrive, " if engines else "" 27 | inventory += "a skilled copilot, " if copilot else "" 28 | if inventory.endswith(", "): 29 | print(inventory.strip(", ")) 30 | 31 | # 32 | # interaction with planets 33 | # 34 | if planet == "earth": 35 | destinations = ["centauri", "sirius"] 36 | print(TEXT["EARTH_DESCRIPTION"]) 37 | 38 | if planet == "centauri": 39 | print(TEXT["CENTAURI_DESCRIPTION"]) 40 | destinations = ["earth", "orion"] 41 | 42 | if not engines: 43 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 44 | if input() == "yes": 45 | if credits: 46 | engines = True 47 | else: 48 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 49 | 50 | if planet == "sirius": 51 | print(TEXT["SIRIUS_DESCRIPTION"]) 52 | destinations = ["orion", "earth", "black_hole"] 53 | 54 | if not credits: 55 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 56 | answer = input() 57 | if answer == "2": 58 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 59 | credits = True 60 | else: 61 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 62 | 63 | if planet == "orion": 64 | destinations = ["centauri", "sirius"] 65 | if not copilot: 66 | print(TEXT["ORION_DESCRIPTION"]) 67 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 68 | if input() == "42": 69 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 70 | copilot = True 71 | else: 72 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 73 | else: 74 | print(TEXT["ORION_DESCRIPTION"]) 75 | 76 | if planet == "black_hole": 77 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 78 | destinations = ["sirius"] 79 | if input() == "yes": 80 | if engines and copilot: 81 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 82 | game_end = True 83 | else: 84 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 85 | return 86 | 87 | if not game_end: 88 | # select next planet 89 | print("\nWhere do you want to travel?") 90 | position = 1 91 | for d in destinations: 92 | print(f"[{position}] {d}") 93 | position += 1 94 | 95 | choice = input() 96 | planet = destinations[int(choice) - 1] 97 | 98 | print(TEXT["END_CREDITS"]) 99 | 100 | 101 | if __name__ == "__main__": 102 | travel() 103 | -------------------------------------------------------------------------------- /refactoring/solution/01-extract-module/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /refactoring/solution/01-extract-module/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ------------------------------------------------------------------------------- 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. The stars are waiting for you. 7 | """, 8 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 9 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 10 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 11 | 12 | Would you like to buy one [yes/no]""", 13 | "HYPERDRIVE_TOO_EXPENSIVE": """ 14 | You cannot afford it. The GPU is too expensive.""", 15 | "SIRIUS_DESCRIPTION": """ 16 | You are on Sirius. The system is full of media companies and content delivery networks.""", 17 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 18 | Here is your question: 19 | 20 | Which star do you find on the shoulder of Orion? 21 | 22 | [1] Altair 23 | [2] Betelgeuse 24 | [3] Aldebaran 25 | [4] Andromeda 26 | """, 27 | "SIRIUS_QUIZ_CORRECT": """ 28 | *Correct!!!* You win a ton or credits. 29 | """, 30 | "SIRIUS_QUIZ_INCORRECT": """ 31 | Sorry, this was the wrong answer. Don't take it too sirius. 32 | Better luck next time. 33 | """, 34 | "ORION_DESCRIPTION": """ 35 | You are on Orion. An icy world inhabited by furry sentients.""", 36 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 37 | They promise to join as a copilot if you can answer a question: 38 | 39 | What is the answer to question of life, the universe and everything? 40 | 41 | What do you answer?""", 42 | "COPILOT_QUESTION_CORRECT": """ 43 | Your new copilot jumps on board and immediately starts 44 | configuring new docker containers. 45 | """, 46 | "COPILOT_QUESTION_INCORRECT": """ 47 | Sorry, that's not it. Try again later. 48 | """, 49 | "BLACK_HOLE_DESCRIPTION": """ 50 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 51 | Do you want to examine the black hole closer? [yes/no] 52 | """, 53 | "BLACK_HOLE_CRUNCHED": """ 54 | The black hole condenses your spaceship into a grain of dust. 55 | 56 | THE END 57 | """, 58 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 59 | On the rim of the black hole your copilot blurts out: 60 | 61 | Turn left! 62 | 63 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 64 | You travel through other dimensions and experience wonders beyond description. 65 | """, 66 | "END_CREDITS": """ 67 | THE END 68 | """, 69 | } 70 | -------------------------------------------------------------------------------- /refactoring/solution/02-extract-function/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | from text_en import TEXT 8 | 9 | 10 | def display_inventory(credits, engines, copilot): 11 | print("-" * 79) 12 | inventory = "\nYou have: " 13 | inventory += "plenty of credits, " if credits else "" 14 | inventory += "a hyperdrive, " if engines else "" 15 | inventory += "a skilled copilot, " if copilot else "" 16 | if inventory.endswith(", "): 17 | print(inventory.strip(", ")) 18 | 19 | 20 | def select_planet(destinations): 21 | print("\nWhere do you want to travel?") 22 | for i, d in enumerate(destinations, 1): 23 | print(f"[{i}] {d}") 24 | 25 | choice = input() 26 | return destinations[int(choice) - 1] 27 | 28 | 29 | def travel(): 30 | 31 | print(TEXT["OPENING_MESSAGE"]) 32 | 33 | planet = "earth" 34 | engines = False 35 | copilot = False 36 | credits = False 37 | game_end = False 38 | 39 | while not game_end: 40 | display_inventory(credits, engines, copilot) 41 | 42 | # 43 | # interaction with planets 44 | # 45 | if planet == "earth": 46 | destinations = ["centauri", "sirius"] 47 | print(TEXT["EARTH_DESCRIPTION"]) 48 | 49 | if planet == "centauri": 50 | print(TEXT["CENTAURI_DESCRIPTION"]) 51 | destinations = ["earth", "orion"] 52 | 53 | if not engines: 54 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 55 | if input() == "yes": 56 | if credits: 57 | engines = True 58 | else: 59 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 60 | 61 | if planet == "sirius": 62 | print(TEXT["SIRIUS_DESCRIPTION"]) 63 | destinations = ["orion", "earth", "black_hole"] 64 | 65 | if not credits: 66 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 67 | answer = input() 68 | if answer == "2": 69 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 70 | credits = True 71 | else: 72 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 73 | 74 | if planet == "orion": 75 | destinations = ["centauri", "sirius"] 76 | if not copilot: 77 | print(TEXT["ORION_DESCRIPTION"]) 78 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 79 | if input() == "42": 80 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 81 | copilot = True 82 | else: 83 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 84 | else: 85 | print(TEXT["ORION_DESCRIPTION"]) 86 | 87 | if planet == "black_hole": 88 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 89 | destinations = ["sirius"] 90 | if input() == "yes": 91 | if engines and copilot: 92 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 93 | game_end = True 94 | else: 95 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 96 | return 97 | 98 | if not game_end: 99 | planet = select_planet(destinations) 100 | 101 | print(TEXT["END_CREDITS"]) 102 | 103 | 104 | if __name__ == "__main__": 105 | travel() 106 | -------------------------------------------------------------------------------- /refactoring/solution/02-extract-function/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /refactoring/solution/02-extract-function/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ------------------------------------------------------------------------------- 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. The stars are waiting for you. 7 | """, 8 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 9 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 10 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 11 | 12 | Would you like to buy one [yes/no]""", 13 | "HYPERDRIVE_TOO_EXPENSIVE": """ 14 | You cannot afford it. The GPU is too expensive.""", 15 | "SIRIUS_DESCRIPTION": """ 16 | You are on Sirius. The system is full of media companies and content delivery networks.""", 17 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 18 | Here is your question: 19 | 20 | Which star do you find on the shoulder of Orion? 21 | 22 | [1] Altair 23 | [2] Betelgeuse 24 | [3] Aldebaran 25 | [4] Andromeda 26 | """, 27 | "SIRIUS_QUIZ_CORRECT": """ 28 | *Correct!!!* You win a ton or credits. 29 | """, 30 | "SIRIUS_QUIZ_INCORRECT": """ 31 | Sorry, this was the wrong answer. Don't take it too sirius. 32 | Better luck next time. 33 | """, 34 | "ORION_DESCRIPTION": """ 35 | You are on Orion. An icy world inhabited by furry sentients.""", 36 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 37 | They promise to join as a copilot if you can answer a question: 38 | 39 | What is the answer to question of life, the universe and everything? 40 | 41 | What do you answer?""", 42 | "COPILOT_QUESTION_CORRECT": """ 43 | Your new copilot jumps on board and immediately starts 44 | configuring new docker containers. 45 | """, 46 | "COPILOT_QUESTION_INCORRECT": """ 47 | Sorry, that's not it. Try again later. 48 | """, 49 | "BLACK_HOLE_DESCRIPTION": """ 50 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 51 | Do you want to examine the black hole closer? [yes/no] 52 | """, 53 | "BLACK_HOLE_CRUNCHED": """ 54 | The black hole condenses your spaceship into a grain of dust. 55 | 56 | THE END 57 | """, 58 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 59 | On the rim of the black hole your copilot blurts out: 60 | 61 | Turn left! 62 | 63 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 64 | You travel through other dimensions and experience wonders beyond description. 65 | """, 66 | "END_CREDITS": """ 67 | THE END 68 | """, 69 | } 70 | -------------------------------------------------------------------------------- /refactoring/solution/03-extract-and-modify/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | from text_en import TEXT 8 | 9 | 10 | def display_inventory(credits, engines, copilot): 11 | print("-" * 79) 12 | inventory = "\nYou have: " 13 | inventory += "plenty of credits, " if credits else "" 14 | inventory += "a hyperdrive, " if engines else "" 15 | inventory += "a skilled copilot, " if copilot else "" 16 | if inventory.endswith(", "): 17 | print(inventory.strip(", ")) 18 | 19 | 20 | def select_planet(destinations): 21 | print("\nWhere do you want to travel?") 22 | for i, d in enumerate(destinations, 1): 23 | print(f"[{i}] {d}") 24 | 25 | choice = input() 26 | return destinations[int(choice) - 1] 27 | 28 | 29 | def visit_planet(planet, engines, copilot, credits, game_end): 30 | if planet == "earth": 31 | destinations = ["centauri", "sirius"] 32 | print(TEXT["EARTH_DESCRIPTION"]) 33 | 34 | if planet == "centauri": 35 | print(TEXT["CENTAURI_DESCRIPTION"]) 36 | destinations = ["earth", "orion"] 37 | 38 | if not engines: 39 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 40 | if input() == "yes": 41 | if credits: 42 | engines = True 43 | else: 44 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 45 | 46 | if planet == "sirius": 47 | print(TEXT["SIRIUS_DESCRIPTION"]) 48 | destinations = ["orion", "earth", "black_hole"] 49 | 50 | if not credits: 51 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 52 | answer = input() 53 | if answer == "2": 54 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 55 | credits = True 56 | else: 57 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 58 | 59 | if planet == "orion": 60 | destinations = ["centauri", "sirius"] 61 | if not copilot: 62 | print(TEXT["ORION_DESCRIPTION"]) 63 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 64 | if input() == "42": 65 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 66 | copilot = True 67 | else: 68 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 69 | else: 70 | print(TEXT["ORION_DESCRIPTION"]) 71 | 72 | if planet == "black_hole": 73 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 74 | destinations = ["sirius"] 75 | if input() == "yes": 76 | if engines and copilot: 77 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 78 | print(TEXT["END_CREDITS"]) 79 | else: 80 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 81 | game_end = True 82 | 83 | return destinations, engines, copilot, credits, game_end 84 | 85 | 86 | def travel(): 87 | 88 | print(TEXT["OPENING_MESSAGE"]) 89 | 90 | planet = "earth" 91 | engines = False 92 | copilot = False 93 | credits = False 94 | game_end = False 95 | 96 | while not game_end: 97 | display_inventory(credits, engines, copilot) 98 | destinations, engines, copilot, credits, game_end = visit_planet(planet, engines, copilot, credits, game_end) 99 | if not game_end: 100 | planet = select_planet(destinations) 101 | 102 | 103 | if __name__ == "__main__": 104 | travel() 105 | -------------------------------------------------------------------------------- /refactoring/solution/03-extract-and-modify/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /refactoring/solution/03-extract-and-modify/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ------------------------------------------------------------------------------- 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. The stars are waiting for you. 7 | """, 8 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 9 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 10 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 11 | 12 | Would you like to buy one [yes/no]""", 13 | "HYPERDRIVE_TOO_EXPENSIVE": """ 14 | You cannot afford it. The GPU is too expensive.""", 15 | "SIRIUS_DESCRIPTION": """ 16 | You are on Sirius. The system is full of media companies and content delivery networks.""", 17 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 18 | Here is your question: 19 | 20 | Which star do you find on the shoulder of Orion? 21 | 22 | [1] Altair 23 | [2] Betelgeuse 24 | [3] Aldebaran 25 | [4] Andromeda 26 | """, 27 | "SIRIUS_QUIZ_CORRECT": """ 28 | *Correct!!!* You win a ton or credits. 29 | """, 30 | "SIRIUS_QUIZ_INCORRECT": """ 31 | Sorry, this was the wrong answer. Don't take it too sirius. 32 | Better luck next time. 33 | """, 34 | "ORION_DESCRIPTION": """ 35 | You are on Orion. An icy world inhabited by furry sentients.""", 36 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 37 | They promise to join as a copilot if you can answer a question: 38 | 39 | What is the answer to question of life, the universe and everything? 40 | 41 | What do you answer?""", 42 | "COPILOT_QUESTION_CORRECT": """ 43 | Your new copilot jumps on board and immediately starts 44 | configuring new docker containers. 45 | """, 46 | "COPILOT_QUESTION_INCORRECT": """ 47 | Sorry, that's not it. Try again later. 48 | """, 49 | "BLACK_HOLE_DESCRIPTION": """ 50 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 51 | Do you want to examine the black hole closer? [yes/no] 52 | """, 53 | "BLACK_HOLE_CRUNCHED": """ 54 | The black hole condenses your spaceship into a grain of dust. 55 | 56 | THE END 57 | """, 58 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 59 | On the rim of the black hole your copilot blurts out: 60 | 61 | Turn left! 62 | 63 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 64 | You travel through other dimensions and experience wonders beyond description. 65 | """, 66 | "END_CREDITS": """ 67 | THE END 68 | """, 69 | } 70 | -------------------------------------------------------------------------------- /refactoring/solution/04-extract-data-structure/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | from text_en import TEXT 8 | 9 | 10 | credits, engines, copilot, game_end = range(4) 11 | 12 | 13 | def display_inventory(flags): 14 | print("-" * 79) 15 | inventory = "\nYou have: " 16 | inventory += "plenty of credits, " if credits in flags else "" 17 | inventory += "a hyperdrive, " if engines in flags else "" 18 | inventory += "a skilled copilot, " if copilot in flags else "" 19 | if inventory.endswith(", "): 20 | print(inventory.strip(", ")) 21 | 22 | 23 | def select_planet(destinations): 24 | print("\nWhere do you want to travel?") 25 | for i, d in enumerate(destinations, 1): 26 | print(f"[{i}] {d}") 27 | 28 | choice = input() 29 | return destinations[int(choice) - 1] 30 | 31 | 32 | def buy_engine(flags): 33 | if engines not in flags: 34 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 35 | if input() == "yes": 36 | if credits in flags: 37 | flags.add(engines) 38 | else: 39 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 40 | 41 | 42 | def stellar_quiz(flags): 43 | if credits not in flags: 44 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 45 | answer = input() 46 | if answer == "2": 47 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 48 | flags.add(credits) 49 | else: 50 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 51 | 52 | 53 | def hire_copilot(flags): 54 | if copilot not in flags: 55 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 56 | if input() == "42": 57 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 58 | flags.add(copilot) 59 | else: 60 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 61 | 62 | 63 | def black_hole(flags): 64 | if input() == "yes": 65 | if engines in flags and copilot in flags: 66 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 67 | print(TEXT["END_CREDITS"]) 68 | else: 69 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 70 | flags.add(game_end) 71 | 72 | 73 | STARMAP = { 74 | 'earth': ["centauri", "sirius"], 75 | 'centauri': ["earth", "orion"], 76 | 'sirius': ["orion", "earth", "black_hole"], 77 | 'orion': ["centauri", "sirius"], 78 | 'black_hole': ["sirius"] 79 | } 80 | 81 | def visit_planet(planet, flags): 82 | key = planet.upper() + '_DESCRIPTION' 83 | print(TEXT[key]) 84 | 85 | if planet == "centauri": 86 | buy_engine(flags) 87 | 88 | if planet == "sirius": 89 | stellar_quiz(flags) 90 | 91 | if planet == "orion": 92 | hire_copilot(flags) 93 | 94 | if planet == "black_hole": 95 | black_hole(flags) 96 | 97 | return STARMAP[planet] 98 | 99 | 100 | def travel(): 101 | 102 | planet = "earth" 103 | flags = set() 104 | 105 | print(TEXT["OPENING_MESSAGE"]) 106 | destinations = visit_planet(planet, flags) 107 | 108 | while game_end not in flags: 109 | planet = select_planet(destinations) 110 | display_inventory(flags) 111 | destinations = visit_planet(planet, flags) 112 | 113 | 114 | if __name__ == "__main__": 115 | travel() 116 | -------------------------------------------------------------------------------- /refactoring/solution/04-extract-data-structure/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /refactoring/solution/04-extract-data-structure/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ------------------------------------------------------------------------------- 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. The stars are waiting for you. 7 | """, 8 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 9 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 10 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 11 | 12 | Would you like to buy one [yes/no]""", 13 | "HYPERDRIVE_TOO_EXPENSIVE": """ 14 | You cannot afford it. The GPU is too expensive.""", 15 | "SIRIUS_DESCRIPTION": """ 16 | You are on Sirius. The system is full of media companies and content delivery networks.""", 17 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 18 | Here is your question: 19 | 20 | Which star do you find on the shoulder of Orion? 21 | 22 | [1] Altair 23 | [2] Betelgeuse 24 | [3] Aldebaran 25 | [4] Andromeda 26 | """, 27 | "SIRIUS_QUIZ_CORRECT": """ 28 | *Correct!!!* You win a ton or credits. 29 | """, 30 | "SIRIUS_QUIZ_INCORRECT": """ 31 | Sorry, this was the wrong answer. Don't take it too sirius. 32 | Better luck next time. 33 | """, 34 | "ORION_DESCRIPTION": """ 35 | You are on Orion. An icy world inhabited by furry sentients.""", 36 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 37 | They promise to join as a copilot if you can answer a question: 38 | 39 | What is the answer to question of life, the universe and everything? 40 | 41 | What do you answer?""", 42 | "COPILOT_QUESTION_CORRECT": """ 43 | Your new copilot jumps on board and immediately starts 44 | configuring new docker containers. 45 | """, 46 | "COPILOT_QUESTION_INCORRECT": """ 47 | Sorry, that's not it. Try again later. 48 | """, 49 | "BLACK_HOLE_DESCRIPTION": """ 50 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 51 | Do you want to examine the black hole closer? [yes/no] 52 | """, 53 | "BLACK_HOLE_CRUNCHED": """ 54 | The black hole condenses your spaceship into a grain of dust. 55 | 56 | THE END 57 | """, 58 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 59 | On the rim of the black hole your copilot blurts out: 60 | 61 | Turn left! 62 | 63 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 64 | You travel through other dimensions and experience wonders beyond description. 65 | """, 66 | "END_CREDITS": """ 67 | THE END 68 | """, 69 | } 70 | -------------------------------------------------------------------------------- /refactoring/solution/05-extract-class/puzzles.py: -------------------------------------------------------------------------------- 1 | 2 | from text_en import TEXT 3 | 4 | 5 | credits, engines, copilot, game_end = range(4) 6 | 7 | 8 | def buy_engine(flags): 9 | if engines not in flags: 10 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 11 | if input() == "yes": 12 | if credits in flags: 13 | flags.add(engines) 14 | else: 15 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 16 | 17 | 18 | def stellar_quiz(flags): 19 | if credits not in flags: 20 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 21 | answer = input() 22 | if answer == "2": 23 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 24 | flags.add(credits) 25 | else: 26 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 27 | 28 | 29 | def hire_copilot(flags): 30 | if copilot not in flags: 31 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 32 | if input() == "42": 33 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 34 | flags.add(copilot) 35 | else: 36 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 37 | 38 | 39 | def black_hole(flags): 40 | if input() == "yes": 41 | if engines in flags and copilot in flags: 42 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 43 | print(TEXT["END_CREDITS"]) 44 | else: 45 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 46 | flags.add(game_end) 47 | -------------------------------------------------------------------------------- /refactoring/solution/05-extract-class/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | from text_en import TEXT 8 | from puzzles import buy_engine, hire_copilot, stellar_quiz, black_hole 9 | from puzzles import credits, engines, copilot, game_end 10 | 11 | 12 | def display_inventory(flags): 13 | print("-" * 79) 14 | inventory = "\nYou have: " 15 | inventory += "plenty of credits, " if credits in flags else "" 16 | inventory += "a hyperdrive, " if engines in flags else "" 17 | inventory += "a skilled copilot, " if copilot in flags else "" 18 | if inventory.endswith(", "): 19 | print(inventory.strip(", ")) 20 | 21 | 22 | def select_planet(destinations): 23 | print("\nWhere do you want to travel?") 24 | for i, d in enumerate(destinations, 1): 25 | print(f"[{i}] {d}") 26 | 27 | choice = input() 28 | return PLANETS[destinations[int(choice) - 1]] 29 | 30 | 31 | class Planet: 32 | 33 | def __init__(self, name, connections, puzzle=None): 34 | self.name = name 35 | self.description = TEXT[name.upper() + "_DESCRIPTION"] 36 | self.connections = connections 37 | self.puzzle = puzzle 38 | 39 | def visit(self, flags): 40 | print(self.description) 41 | if self.puzzle: 42 | self.puzzle(flags) 43 | 44 | 45 | PLANETS = {p.name: p for p in [ 46 | Planet('earth', ["centauri", "sirius"]), 47 | Planet('centauri', ["earth", "orion"], buy_engine), 48 | Planet('sirius', ["orion", "earth", "black_hole"], stellar_quiz), 49 | Planet('orion', ["centauri", "sirius"], hire_copilot), 50 | Planet('black_hole', ["sirius"], black_hole) 51 | ]} 52 | 53 | 54 | def travel(): 55 | """main game function""" 56 | planet = PLANETS["earth"] 57 | flags = set() 58 | 59 | print(TEXT["OPENING_MESSAGE"]) 60 | planet.visit(flags) 61 | 62 | while game_end not in flags: 63 | planet = select_planet(planet.connections) 64 | display_inventory(flags) 65 | planet.visit(flags) 66 | 67 | 68 | if __name__ == "__main__": 69 | travel() 70 | -------------------------------------------------------------------------------- /refactoring/solution/05-extract-class/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /refactoring/solution/05-extract-class/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ------------------------------------------------------------------------------- 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. The stars are waiting for you. 7 | """, 8 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 9 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 10 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 11 | 12 | Would you like to buy one [yes/no]""", 13 | "HYPERDRIVE_TOO_EXPENSIVE": """ 14 | You cannot afford it. The GPU is too expensive.""", 15 | "SIRIUS_DESCRIPTION": """ 16 | You are on Sirius. The system is full of media companies and content delivery networks.""", 17 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 18 | Here is your question: 19 | 20 | Which star do you find on the shoulder of Orion? 21 | 22 | [1] Altair 23 | [2] Betelgeuse 24 | [3] Aldebaran 25 | [4] Andromeda 26 | """, 27 | "SIRIUS_QUIZ_CORRECT": """ 28 | *Correct!!!* You win a ton or credits. 29 | """, 30 | "SIRIUS_QUIZ_INCORRECT": """ 31 | Sorry, this was the wrong answer. Don't take it too sirius. 32 | Better luck next time. 33 | """, 34 | "ORION_DESCRIPTION": """ 35 | You are on Orion. An icy world inhabited by furry sentients.""", 36 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 37 | They promise to join as a copilot if you can answer a question: 38 | 39 | What is the answer to question of life, the universe and everything? 40 | 41 | What do you answer?""", 42 | "COPILOT_QUESTION_CORRECT": """ 43 | Your new copilot jumps on board and immediately starts 44 | configuring new docker containers. 45 | """, 46 | "COPILOT_QUESTION_INCORRECT": """ 47 | Sorry, that's not it. Try again later. 48 | """, 49 | "BLACK_HOLE_DESCRIPTION": """ 50 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 51 | Do you want to examine the black hole closer? [yes/no] 52 | """, 53 | "BLACK_HOLE_CRUNCHED": """ 54 | The black hole condenses your spaceship into a grain of dust. 55 | 56 | THE END 57 | """, 58 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 59 | On the rim of the black hole your copilot blurts out: 60 | 61 | Turn left! 62 | 63 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 64 | You travel through other dimensions and experience wonders beyond description. 65 | """, 66 | "END_CREDITS": """ 67 | THE END 68 | """, 69 | } 70 | -------------------------------------------------------------------------------- /refactoring/solution/06-another-class/puzzles.py: -------------------------------------------------------------------------------- 1 | 2 | from text_en import TEXT 3 | 4 | 5 | credits, engines, copilot, game_end = range(4) 6 | 7 | 8 | def buy_engine(flags): 9 | if engines not in flags: 10 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 11 | if input() == "yes": 12 | if credits in flags: 13 | flags.add(engines) 14 | else: 15 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 16 | 17 | 18 | def stellar_quiz(flags): 19 | if credits not in flags: 20 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 21 | answer = input() 22 | if answer == "2": 23 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 24 | flags.add(credits) 25 | else: 26 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 27 | 28 | 29 | def hire_copilot(flags): 30 | if copilot not in flags: 31 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 32 | if input() == "42": 33 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 34 | flags.add(copilot) 35 | else: 36 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 37 | 38 | 39 | def black_hole(flags): 40 | if input() == "yes": 41 | if engines in flags and copilot in flags: 42 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 43 | print(TEXT["END_CREDITS"]) 44 | else: 45 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 46 | flags.add(game_end) 47 | -------------------------------------------------------------------------------- /refactoring/solution/06-another-class/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | from text_en import TEXT 8 | from puzzles import buy_engine, hire_copilot, stellar_quiz, black_hole 9 | from puzzles import credits, engines, copilot, game_end 10 | 11 | 12 | 13 | class Planet: 14 | 15 | def __init__(self, name, connections, puzzle=None): 16 | self.name = name 17 | self.description = TEXT[name.upper() + "_DESCRIPTION"] 18 | self.connections = connections 19 | self.puzzle = puzzle 20 | 21 | def visit(self, flags): 22 | print(self.description) 23 | if self.puzzle: 24 | self.puzzle(flags) 25 | 26 | 27 | PLANETS = {p.name: p for p in [ 28 | Planet('earth', ["centauri", "sirius"]), 29 | Planet('centauri', ["earth", "orion"], buy_engine), 30 | Planet('sirius', ["orion", "earth", "black_hole"], stellar_quiz), 31 | Planet('orion', ["centauri", "sirius"], hire_copilot), 32 | Planet('black_hole', ["sirius"], black_hole) 33 | ]} 34 | 35 | 36 | class SpaceGame: 37 | 38 | def __init__(self): 39 | self.flags = set() 40 | self.planet = PLANETS["earth"] 41 | 42 | @property 43 | def running(self): 44 | return game_end not in self.flags 45 | 46 | def display_inventory(self): 47 | """Returns a string description of the inventory""" 48 | inventory = "\nYou have: " 49 | inventory += "plenty of credits, " if credits in self.flags else "" 50 | inventory += "a hyperdrive, " if engines in self.flags else "" 51 | inventory += "a skilled copilot, " if copilot in self.flags else "" 52 | if inventory.endswith(", "): 53 | inventory = inventory.strip(", ") 54 | return inventory 55 | 56 | def visit_planet(self): 57 | self.planet.visit(self.flags) 58 | 59 | def display_destinations(self): 60 | """Returns the planet selection menu""" 61 | result = "\nWhere do you want to travel?" 62 | for i, d in enumerate(self.planet.connections, 1): 63 | result += f"[{i}] {d}" 64 | return result 65 | 66 | def select_planet(self): 67 | choice = input() 68 | self.planet = PLANETS[self.planet.connections[int(choice) - 1]] 69 | 70 | 71 | def travel(): 72 | """main game function""" 73 | game = SpaceGame() 74 | 75 | print(TEXT["OPENING_MESSAGE"]) 76 | game.visit_planet() 77 | 78 | while game.running: 79 | print(game.display_destinations()) 80 | game.select_planet() 81 | print('-' * 79) 82 | print(game.display_inventory()) 83 | game.visit_planet() 84 | 85 | 86 | if __name__ == "__main__": 87 | travel() 88 | -------------------------------------------------------------------------------- /refactoring/solution/06-another-class/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /refactoring/solution/06-another-class/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | ------------------------------------------------------------------------------- 4 | 5 | You and your trusted spaceship set out to look for 6 | fame, wisdom, and adventure. The stars are waiting for you. 7 | """, 8 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 9 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 10 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 11 | 12 | Would you like to buy one [yes/no]""", 13 | "HYPERDRIVE_TOO_EXPENSIVE": """ 14 | You cannot afford it. The GPU is too expensive.""", 15 | "SIRIUS_DESCRIPTION": """ 16 | You are on Sirius. The system is full of media companies and content delivery networks.""", 17 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 18 | Here is your question: 19 | 20 | Which star do you find on the shoulder of Orion? 21 | 22 | [1] Altair 23 | [2] Betelgeuse 24 | [3] Aldebaran 25 | [4] Andromeda 26 | """, 27 | "SIRIUS_QUIZ_CORRECT": """ 28 | *Correct!!!* You win a ton or credits. 29 | """, 30 | "SIRIUS_QUIZ_INCORRECT": """ 31 | Sorry, this was the wrong answer. Don't take it too sirius. 32 | Better luck next time. 33 | """, 34 | "ORION_DESCRIPTION": """ 35 | You are on Orion. An icy world inhabited by furry sentients.""", 36 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 37 | They promise to join as a copilot if you can answer a question: 38 | 39 | What is the answer to question of life, the universe and everything? 40 | 41 | What do you answer?""", 42 | "COPILOT_QUESTION_CORRECT": """ 43 | Your new copilot jumps on board and immediately starts 44 | configuring new docker containers. 45 | """, 46 | "COPILOT_QUESTION_INCORRECT": """ 47 | Sorry, that's not it. Try again later. 48 | """, 49 | "BLACK_HOLE_DESCRIPTION": """ 50 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 51 | Do you want to examine the black hole closer? [yes/no] 52 | """, 53 | "BLACK_HOLE_CRUNCHED": """ 54 | The black hole condenses your spaceship into a grain of dust. 55 | 56 | THE END 57 | """, 58 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 59 | On the rim of the black hole your copilot blurts out: 60 | 61 | Turn left! 62 | 63 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 64 | You travel through other dimensions and experience wonders beyond description. 65 | """, 66 | "END_CREDITS": """ 67 | THE END 68 | """, 69 | } 70 | -------------------------------------------------------------------------------- /refactoring/solution/07-oop-decouple-game-logic/puzzles.py: -------------------------------------------------------------------------------- 1 | """ 2 | Puzzle classes that work with decoupled UI / game logic 3 | 4 | implements the Strategy Pattern (puzzle objects combine with any planet) 5 | """ 6 | from text_en import TEXT 7 | from abc import ABC, abstractmethod 8 | 9 | credits, engines, copilot, game_end = range(4) 10 | 11 | 12 | class Puzzle(ABC): 13 | """Abstract Base Class (ABC) for puzzles""" 14 | 15 | @abstractmethod 16 | def is_active(self, flags): 17 | pass 18 | 19 | @abstractmethod 20 | def get_question(self, flags): 21 | pass 22 | 23 | @abstractmethod 24 | def answer(self, flags, answer): 25 | pass 26 | 27 | 28 | class BuyEngine(Puzzle): 29 | 30 | def is_active(self, flags): 31 | return engines not in flags 32 | 33 | def get_question(self, flags): 34 | return TEXT["HYPERDRIVE_SHOPPING_QUESTION"] 35 | 36 | def answer(self, flags, answer): 37 | if answer == "yes": 38 | if credits in flags: 39 | flags.add(engines) 40 | return '' 41 | else: 42 | return TEXT["HYPERDRIVE_TOO_EXPENSIVE"] 43 | return '' 44 | 45 | 46 | class StellarQuiz(Puzzle): 47 | 48 | def is_active(self, flags): 49 | return credits not in flags 50 | 51 | def get_question(self, flags): 52 | return TEXT["SIRIUS_QUIZ_QUESTION"] 53 | 54 | def answer(self, flags, answer): 55 | if answer == "2": 56 | flags.add(credits) 57 | return TEXT["SIRIUS_QUIZ_CORRECT"] 58 | else: 59 | return TEXT["SIRIUS_QUIZ_INCORRECT"] 60 | 61 | 62 | class HireCopilot(Puzzle): 63 | 64 | def is_active(self, flags): 65 | return copilot not in flags 66 | 67 | def get_question(self, flags): 68 | return TEXT["ORION_HIRE_COPILOT_QUESTION"] 69 | 70 | def answer(self, flags, answer): 71 | if answer == "42": 72 | flags.add(copilot) 73 | return TEXT["COPILOT_QUESTION_CORRECT"] 74 | else: 75 | return TEXT["COPILOT_QUESTION_INCORRECT"] 76 | 77 | 78 | class BlackHole(Puzzle): 79 | 80 | def is_active(self, flags): 81 | return True 82 | 83 | def get_question(self, flags): 84 | return '' 85 | 86 | def answer(self, flags, answer): 87 | if answer == "yes": 88 | flags.add(game_end) 89 | if engines in flags and copilot in flags: 90 | return TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"] + TEXT["END_CREDITS"] 91 | else: 92 | return TEXT["BLACK_HOLE_CRUNCHED"] 93 | return '' 94 | -------------------------------------------------------------------------------- /refactoring/solution/07-oop-decouple-game-logic/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | from text_en import TEXT 8 | from puzzles import StellarQuiz, BuyEngine, HireCopilot, BlackHole 9 | from puzzles import credits, engines, copilot, game_end 10 | 11 | 12 | class Planet: 13 | 14 | def __init__(self, name, connections, puzzle=None): 15 | self.name = name 16 | self.description = TEXT[name.upper() + "_DESCRIPTION"] 17 | self.connections = connections 18 | self.puzzle = puzzle 19 | 20 | def has_active_puzzle(self, flags): 21 | return self.puzzle and self.puzzle.is_active(flags) 22 | 23 | def get_puzzle_text(self, flags): 24 | return self.puzzle.get_question(flags) 25 | 26 | def answer_puzzle(self, flags, action): 27 | return self.puzzle.answer(flags, action) 28 | 29 | 30 | PLANETS = {p.name: p for p in [ 31 | Planet('earth', ["centauri", "sirius"]), 32 | Planet('centauri', ["earth", "orion"], BuyEngine()), 33 | Planet('sirius', ["orion", "earth", "black_hole"], StellarQuiz()), 34 | Planet('orion', ["centauri", "sirius"], HireCopilot()), 35 | Planet('black_hole', ["sirius"], BlackHole()) 36 | ]} 37 | 38 | 39 | class SpaceGame: 40 | 41 | def __init__(self): 42 | self.flags = set() 43 | self.planet = PLANETS["earth"] 44 | self.state = 'start' 45 | 46 | @property 47 | def running(self): 48 | return game_end not in self.flags 49 | 50 | def display_inventory(self): 51 | """Returns a string description of the inventory""" 52 | inventory = "\nYou have: " 53 | inventory += "plenty of credits, " if credits in self.flags else "" 54 | inventory += "a hyperdrive, " if engines in self.flags else "" 55 | inventory += "a skilled copilot, " if copilot in self.flags else "" 56 | if inventory.endswith(", "): 57 | return inventory.strip(", ") 58 | return '' 59 | 60 | def visit_planet(self): 61 | self.planet.visit(self.flags) 62 | 63 | @property 64 | def choices(self): 65 | if self.state == 'move': 66 | return self.planet.connections 67 | elif self.state == 'puzzle': 68 | return None 69 | 70 | def get_situation_text(self): 71 | result = '' 72 | if self.state == 'start': 73 | result = TEXT["OPENING_MESSAGE"] 74 | self.state = 'move' 75 | if self.state == 'move': 76 | result += self.display_inventory() 77 | result += "\n\nWhere do you want to travel?" 78 | if self.state == 'puzzle': 79 | result = self.planet.get_puzzle_text(self.flags) 80 | return result 81 | 82 | def take_action(self, action): 83 | """manages state transitions""" 84 | if self.state == 'move': 85 | self.planet = PLANETS[action] 86 | if self.planet.has_active_puzzle(self.flags): 87 | self.state = 'puzzle' 88 | return self.planet.description 89 | 90 | if self.state == 'puzzle': 91 | self.state = 'move' 92 | return(self.planet.answer_puzzle(self.flags, action)) 93 | 94 | 95 | # 96 | # User Interface 97 | # 98 | # the only part of the program that knows about print() and input() 99 | # 100 | def display_options(choices): 101 | """Returns a generic selection menu""" 102 | if choices: 103 | for i, d in enumerate(choices, 1): 104 | print(f"[{i}] {d}") 105 | 106 | def select_option(choices): 107 | """Returns keyboard input""" 108 | action = input() 109 | if choices: 110 | return choices[int(action) - 1] 111 | return action 112 | 113 | 114 | def travel(): 115 | """main game function""" 116 | game = SpaceGame() 117 | while game.running: 118 | print('-' * 79) 119 | print(game.get_situation_text()) 120 | display_options(game.choices) 121 | action = select_option(game.choices) 122 | print(game.take_action(action)) 123 | 124 | 125 | if __name__ == "__main__": 126 | travel() 127 | -------------------------------------------------------------------------------- /refactoring/solution/07-oop-decouple-game-logic/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /refactoring/solution/07-oop-decouple-game-logic/text_en.py: -------------------------------------------------------------------------------- 1 | TEXT = { 2 | "OPENING_MESSAGE": """ 3 | You and your trusted spaceship set out to look for 4 | fame, wisdom, and adventure. The stars are waiting for you. 5 | """, 6 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 7 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 8 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 9 | 10 | Would you like to buy one [yes/no]""", 11 | "HYPERDRIVE_TOO_EXPENSIVE": """ 12 | You cannot afford it. The GPU is too expensive.""", 13 | "SIRIUS_DESCRIPTION": """ 14 | You are on Sirius. The system is full of media companies and content delivery networks.""", 15 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 16 | Here is your question: 17 | 18 | Which star do you find on the shoulder of Orion? 19 | 20 | [1] Altair 21 | [2] Betelgeuse 22 | [3] Aldebaran 23 | [4] Andromeda 24 | """, 25 | "SIRIUS_QUIZ_CORRECT": """ 26 | *Correct!!!* You win a ton or credits. 27 | """, 28 | "SIRIUS_QUIZ_INCORRECT": """ 29 | Sorry, this was the wrong answer. Don't take it too sirius. 30 | Better luck next time. 31 | """, 32 | "ORION_DESCRIPTION": """ 33 | You are on Orion. An icy world inhabited by furry sentients.""", 34 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 35 | They promise to join as a copilot if you can answer a question: 36 | 37 | What is the answer to question of life, the universe and everything? 38 | 39 | What do you answer?""", 40 | "COPILOT_QUESTION_CORRECT": """ 41 | Your new copilot jumps on board and immediately starts 42 | configuring new docker containers. 43 | """, 44 | "COPILOT_QUESTION_INCORRECT": """ 45 | Sorry, that's not it. Try again later. 46 | """, 47 | "BLACK_HOLE_DESCRIPTION": """ 48 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 49 | Do you want to examine the black hole closer? [yes/no] 50 | """, 51 | "BLACK_HOLE_CRUNCHED": """ 52 | The black hole condenses your spaceship into a grain of dust. 53 | 54 | THE END 55 | """, 56 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 57 | On the rim of the black hole your copilot blurts out: 58 | 59 | Turn left! 60 | 61 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 62 | You travel through other dimensions and experience wonders beyond description. 63 | """, 64 | "END_CREDITS": """ 65 | THE END 66 | """, 67 | } 68 | -------------------------------------------------------------------------------- /refactoring/space_game.py: -------------------------------------------------------------------------------- 1 | """ 2 | Space Travel Game 3 | 4 | A simple text adventure written for a refactoring tutorial. 5 | """ 6 | 7 | TEXT = { 8 | "OPENING_MESSAGE": """ 9 | ------------------------------------------------------------------------------- 10 | 11 | You and your trusted spaceship set out to look for 12 | fame, wisdom, and adventure. The stars are waiting for you. 13 | """, 14 | "EARTH_DESCRIPTION": "\nYou are on Earth. Beautiful is better than ugly.", 15 | "CENTAURI_DESCRIPTION": "\nYou are on Alpha Centauri. All creatures are welcome here.", 16 | "HYPERDRIVE_SHOPPING_QUESTION": """There is a brand new hyperdrive with a built-in GPU for sale. 17 | 18 | Would you like to buy one [yes/no]""", 19 | "HYPERDRIVE_TOO_EXPENSIVE": """ 20 | You cannot afford it. The GPU is too expensive.""", 21 | "SIRIUS_DESCRIPTION": """ 22 | You are on Sirius. The system is full of media companies and content delivery networks.""", 23 | "SIRIUS_QUIZ_QUESTION": """You manage to get a place in *Stellar* - the greatest quiz show in the universe. 24 | Here is your question: 25 | 26 | Which star do you find on the shoulder of Orion? 27 | 28 | [1] Altair 29 | [2] Betelgeuse 30 | [3] Aldebaran 31 | [4] Andromeda 32 | """, 33 | "SIRIUS_QUIZ_CORRECT": """ 34 | *Correct!!!* You win a ton or credits. 35 | """, 36 | "SIRIUS_QUIZ_INCORRECT": """ 37 | Sorry, this was the wrong answer. Don't take it too sirius. 38 | Better luck next time. 39 | """, 40 | "ORION_DESCRIPTION": """ 41 | You are on Orion. An icy world inhabited by furry sentients.""", 42 | "ORION_HIRE_COPILOT_QUESTION": """A tech-savvy native admires your spaceship. 43 | They promise to join as a copilot if you can answer a question: 44 | 45 | What is the answer to question of life, the universe and everything? 46 | 47 | What do you answer?""", 48 | "COPILOT_QUESTION_CORRECT": """ 49 | Your new copilot jumps on board and immediately starts 50 | configuring new docker containers. 51 | """, 52 | "COPILOT_QUESTION_INCORRECT": """ 53 | Sorry, that's not it. Try again later. 54 | """, 55 | "BLACK_HOLE_DESCRIPTION": """ 56 | You are close to Black Hole #0997. Maybe coming here was a really stupid idea. 57 | Do you want to examine the black hole closer? [yes/no] 58 | """, 59 | "BLACK_HOLE_CRUNCHED": """ 60 | The black hole condenses your spaceship into a grain of dust. 61 | 62 | THE END 63 | """, 64 | "BLACK_HOLE_COPILOT_SAVES_YOU": """ 65 | On the rim of the black hole your copilot blurts out: 66 | 67 | Turn left! 68 | 69 | You ignite the next-gen hyperdrive, creating a time-space anomaly. 70 | You travel through other dimensions and experience wonders beyond description. 71 | """, 72 | "END_CREDITS": """ 73 | THE END 74 | """, 75 | } 76 | 77 | 78 | def travel(): 79 | 80 | print(TEXT["OPENING_MESSAGE"]) 81 | 82 | planet = "earth" 83 | engines = False 84 | copilot = False 85 | credits = False 86 | game_end = False 87 | 88 | while not game_end: 89 | 90 | # display inventory 91 | print("-" * 79) 92 | inventory = "\nYou have: " 93 | inventory += "plenty of credits, " if credits else "" 94 | inventory += "a hyperdrive, " if engines else "" 95 | inventory += "a skilled copilot, " if copilot else "" 96 | if inventory.endswith(", "): 97 | print(inventory.strip(", ")) 98 | 99 | # 100 | # interaction with planets 101 | # 102 | if planet == "earth": 103 | destinations = ["centauri", "sirius"] 104 | print(TEXT["EARTH_DESCRIPTION"]) 105 | 106 | if planet == "centauri": 107 | print(TEXT["CENTAURI_DESCRIPTION"]) 108 | destinations = ["earth", "orion"] 109 | 110 | if not engines: 111 | print(TEXT["HYPERDRIVE_SHOPPING_QUESTION"]) 112 | if input() == "yes": 113 | if credits: 114 | engines = True 115 | else: 116 | print(TEXT["HYPERDRIVE_TOO_EXPENSIVE"]) 117 | 118 | if planet == "sirius": 119 | print(TEXT["SIRIUS_DESCRIPTION"]) 120 | destinations = ["orion", "earth", "black_hole"] 121 | 122 | if not credits: 123 | print(TEXT["SIRIUS_QUIZ_QUESTION"]) 124 | answer = input() 125 | if answer == "2": 126 | print(TEXT["SIRIUS_QUIZ_CORRECT"]) 127 | credits = True 128 | else: 129 | print(TEXT["SIRIUS_QUIZ_INCORRECT"]) 130 | 131 | if planet == "orion": 132 | destinations = ["centauri", "sirius"] 133 | if not copilot: 134 | print(TEXT["ORION_DESCRIPTION"]) 135 | print(TEXT["ORION_HIRE_COPILOT_QUESTION"]) 136 | if input() == "42": 137 | print(TEXT["COPILOT_QUESTION_CORRECT"]) 138 | copilot = True 139 | else: 140 | print(TEXT["COPILOT_QUESTION_INCORRECT"]) 141 | else: 142 | print(TEXT["ORION_DESCRIPTION"]) 143 | 144 | if planet == "black_hole": 145 | print(TEXT["BLACK_HOLE_DESCRIPTION"]) 146 | destinations = ["sirius"] 147 | if input() == "yes": 148 | if engines and copilot: 149 | print(TEXT["BLACK_HOLE_COPILOT_SAVES_YOU"]) 150 | game_end = True 151 | else: 152 | print(TEXT["BLACK_HOLE_CRUNCHED"]) 153 | return 154 | 155 | if not game_end: 156 | # select next planet 157 | print("\nWhere do you want to travel?") 158 | position = 1 159 | for d in destinations: 160 | print(f"[{position}] {d}") 161 | position += 1 162 | 163 | choice = input() 164 | planet = destinations[int(choice) - 1] 165 | 166 | print(TEXT["END_CREDITS"]) 167 | 168 | 169 | if __name__ == "__main__": 170 | travel() 171 | -------------------------------------------------------------------------------- /refactoring/test_space_game.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pytest 3 | 4 | from space_game import travel 5 | 6 | 7 | # the actual solution to the game 8 | SOLUTION = [ 9 | "2", 10 | "2", # go to sirius and win quiz 11 | "1", 12 | "42", # hire copilot on orion 13 | "1", 14 | "yes", # go to centauri and buy GPU drive 15 | "2", 16 | "2", 17 | "3", 18 | "yes", # jump into black hole 19 | ] 20 | 21 | DEATH_BY_BLACK_HOLE = [ 22 | "2", 23 | "2", # go to sirius and win quiz 24 | "1", 25 | "41", # hire copilot on orion 26 | "1", 27 | "yes", # go to centauri and buy GPU drive 28 | "1", 29 | "2", 30 | "3", 31 | "yes", # jump into black hole 32 | ] 33 | 34 | # text sniplets that should appear literally in the output 35 | PHRASES = [ 36 | "The stars are waiting for you", 37 | "Betelgeuse", 38 | "credits", 39 | "tech-savvy native", 40 | "copilot", 41 | "buy", 42 | "life, the universe and everything", 43 | "Black Hole", 44 | "stupid idea", 45 | "wonders beyond description", 46 | "THE END", 47 | ] 48 | 49 | 50 | @pytest.fixture 51 | def solution_input(): 52 | """helper function to hijack the keyboard for testing""" 53 | return io.StringIO("\n".join(SOLUTION)) 54 | 55 | 56 | def test_travel(monkeypatch, solution_input): 57 | """game finishes""" 58 | monkeypatch.setattr("sys.stdin", solution_input) 59 | travel() 60 | 61 | 62 | def test_output(monkeypatch, capsys, solution_input): 63 | """text output is not empty""" 64 | monkeypatch.setattr("sys.stdin", solution_input) 65 | 66 | travel() 67 | 68 | captured = capsys.readouterr() 69 | assert len(captured.out) > 0 70 | 71 | 72 | def test_die(monkeypatch, capsys): 73 | """player dies""" 74 | monkeypatch.setattr("sys.stdin", io.StringIO("\n".join(DEATH_BY_BLACK_HOLE))) 75 | 76 | travel() 77 | 78 | captured = capsys.readouterr() 79 | assert "grain of dust" in captured.out 80 | assert " wonders beyond description" not in captured.out 81 | 82 | 83 | @pytest.mark.parametrize("phrase", PHRASES) 84 | def test_output_phrases(monkeypatch, capsys, solution_input, phrase): 85 | """check for some key phrases in the output""" 86 | monkeypatch.setattr("sys.stdin", solution_input) 87 | 88 | travel() 89 | 90 | captured = capsys.readouterr() 91 | assert phrase in captured.out 92 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-design 3 | sphinx-copybutton 4 | myst-parser 5 | -------------------------------------------------------------------------------- /tech_debt.md: -------------------------------------------------------------------------------- 1 | 2 | # Technical Debt 3 | 4 | ## What is technical debt? 5 | 6 | If refactoring is ignored a project may accumulate **Technnical debt**. 7 | **Technical debt** is a frequent problem in projects evolving over time. 8 | It includes: 9 | 10 | * lack of documentation 11 | * lack of structure 12 | * badly written code 13 | * code that breaks in special cases 14 | * bugs 15 | * .. and many more 16 | 17 | This phenomenon has also been described as [**software entropy**](https://en.wikipedia.org/wiki/Software_entropy) and [**Lehmanns Laws**](https://en.wikipedia.org/wiki/Lehman%27s_laws_of_software_evolution). 18 | 19 | ---- 20 | 21 | ## How does technical debt emerge? 22 | 23 | There are at least six reasons why technical debt accumulates: 24 | 25 | ### 1. Haste 26 | 27 | **Pressure to finish quickly** teases programmers to cut corners. Programmers under pressure try to get the code running, no matter what (*"I can clean this up later."*). Producing clean, transparent, well-tested code becomes a secondary issue. Small nodules of messy code will emerge, grow, accumulate, and if you rush from deadline to deadline, the program becomes a jungle. 28 | 29 | Slowing down your pace of programming under pressure takes courage. 30 | 31 | ### 2. Misunderstanding the problem 32 | 33 | When you first write a program, you are making assumptions about the real-world problem it solves. Almost inevitably, some of these assumptions turn out to be wrong. Every time you add new code to correct your wrong assumptions, they will lay a burden on the original design – unless you clean up properly. 34 | 35 | Because of that, the milestone book *"the mythical man-month"* (Brooks, 1963) states: *"Be prepared to throw one away."* 36 | 37 | ### 3. Lack of experience 38 | 39 | A programmer might write code that is difficult to maintain because he doesn't know better. An unexperienced programmer thinks that programming means writing code. An experienced programmer - like anyone interested in a book on software engineering - knows that sometimes programming means writing code, and sometimes it doesn't. 40 | 41 | Lack of experience often results in code that is unnecessary long or complicated. This can happen even to experienced programmers switching from another language. Once, we stumbled upon the following Python code fragment written by a C programmer: 42 | 43 | i = 0; s = [] 44 | f = open(filename,'r') 45 | while 1: 46 | z = f.seek(i) 47 | if z==None: 48 | break 49 | ch = f.read(1) 50 | s.append(ch) 51 | i = i+1 52 | 53 | This code fragment can be written as: 54 | 55 | s = list(open(filename).read()) 56 | 57 | Even though Python is considered easy to learn, writing good Python code is not trivial. 58 | 59 | ### 4. Overabundant experience 60 | 61 | Experienced programmers can create problematic code, too. In the first place, an experienced programmer is very good to have: They write sophisticated programs incredibly quickly, master new technologies and make them work. Such programmers are rare and valuable. 62 | 63 | The problem is that sometimes it takes another experienced programmer to understand their code. One example of such code is called **code golf**. In code golf, the programmer tries to implement a program with as few key strokes as possible: 64 | 65 | The moment an experienced programmer departs and leaves a lot of functional code that is hard to read, the project can suddenly go into debt. 66 | 67 | 68 | ### 5. Python 69 | 70 | Python checks for SyntaxErrors and the most obvious exceptions at runtime. Unfortunately, Python does not notice much more. 71 | 72 | Even a simple typo like the following could pass unnoticed: 73 | 74 | idx = 3 75 | 76 | ... 77 | 78 | def get_modification_name(ids): 79 | return DATABASE.get(idx) # should be ids 80 | 81 | When you move this function to a separate module during a refactoring session, the code will break, thus revealing the bug. 82 | 83 | ### 6. Changes in the environment 84 | 85 | Even if your program is written perfectly, it will slowly deteriorate. The libraries it uses may deprecate methods, new string encodings, display sizes, new customer wishes and other changes mean that your program is becoming less useful. To stay up to date technically, the code needs to adapt. 86 | -------------------------------------------------------------------------------- /user_stories.md: -------------------------------------------------------------------------------- 1 | # User Stories 2 | 3 | Writing down goals increases the probability that you will reach them. 4 | On one hand, written goals help to focus work both on your own and in a team. 5 | On the other hand, a writing down everything in detail is often not practical. 6 | **User Stories** are a short written form for project tasks. 7 | 8 | 9 | ## How to write User Stories? 10 | 11 | A User Story has to fit on an index card. 12 | It should contain: 13 | 14 | * a title 15 | * a clear benefit for users 16 | * no technical detail 17 | * optionally 2-3 criteria for success 18 | 19 | Many developers use the pattern **"As a X, I want to Y, so that Z."** 20 | Here is an example User Story for the Snake game: 21 | 22 | As a player, 23 | I want to eat food with my snake, 24 | so that it grows. 25 | 26 | 27 | ## What are User Stories good for? 28 | 29 | User Stories help with a couple of things: 30 | 31 | 1. formalize what a customer wants 32 | 2. mark who is working on a story 33 | 3. estimate the work required 34 | 4. track completion status (as GitHub issues, a Kanban board or JIRA) 35 | 5. discuss the details later (they are also called *"Promise of Communication"*) 36 | 37 | 38 | ## Decomposing Stories 39 | 40 | ![Decomposing Stories](images/decomposing_stories.png) 41 | 42 | Often, a project starts with a few big User Stories (also called Epics). 43 | These are later decomposed into smaller working units. 44 | A good size in a development project is 1-2 work days. 45 | Finding the right size may take several rounds of decomposing. 46 | 47 | ---- 48 | 49 | ## Exercise 50 | 51 | Write down 3 User Stories for the Snake game. 52 | Use the format 53 | 54 | As a , I want to , so that . 55 | 56 | ---- 57 | 58 | ## Further Reading 59 | 60 | [User Stories 101](https://adamfard.com/blog/user-stories) by Adam Fard 61 | -------------------------------------------------------------------------------- /writing_code.md: -------------------------------------------------------------------------------- 1 | 2 | # Coding Strategies 3 | 4 | Python code can be developed using many strategies. 5 | Here you find a few beginner-friendly ones that are also used by experienced professionals. 6 | 7 | ---- 8 | 9 | ## Line by Line 10 | 11 | 1. Write a line of code 12 | 2. Execute it 13 | 3. Check whether it is doing what you want 14 | 4. Back to 1. 15 | 16 | This strategy is useful mainly for experimenting with new commands and while working in an 17 | interactive **Python Shell** or a **Jupyter Notebook**. 18 | It also works with an editor as long as you either generate output with `print()` after every command or use your editors controls to step throught the program line by line. 19 | 20 | ---- 21 | 22 | ## Copy-Paste 23 | 24 | 1. Copy a small working piece of code 25 | 2. Execute it **without modification** 26 | 3. Make sure the code is working 27 | 4. Understand what the program is doing 28 | 5. Modify the code 29 | 30 | This is a good strategy for trying out new tools or programming libraries. 31 | Most Python packages come with a set of examples that you can try out directly. 32 | 33 | Copy-pasting code from documentation, tutorials or pages like StackOverflow is a totally legitimate coding strategy! 34 | 35 | ---- 36 | 37 | ## Modify a Program 38 | 39 | 1. Begin with a working piece of code 40 | 2. Modify a few lines 41 | 3. Execute the program 42 | 4. Observe what happens 43 | 44 | Starting with an existing program is often more challenging than writing everything from scratch. The main difference to the copy-paste strategy is that in step 4. you observe. 45 | Often you can learn something new here. 46 | 47 | ---- 48 | 49 | ## Skeleton Code 50 | 51 | 1. Write class and function definitions, but leave the bodies of the functions empty 52 | 2. Make each function return dummy values 53 | 3. Write a main section that uses the classes / functions 54 | 4. Execute everything and make sure the program runs without Exceptions 55 | 5. Start filling the function bodies one by one 56 | 57 | This is a somewhat different strategy that lets you think about the structure of a program without the details of the implementation getting in the way. 58 | 59 | ---- 60 | 61 | ## Write everything in one go 62 | 63 | **CAUTION:** This is **not** an easy strategy: 64 | 65 | 1. Write the entire program first 66 | 2. Then execute it and make sure it works 67 | 68 | The difficulty in Step 2 is that you not only have to deal with normal bugs. 69 | You are also confronted with *semmantic mistakes* and *miscondeptions* that you made while coding. It is very easy to get stuck here and give up. 70 | 71 | Writing programs with more than 20 lines is not easy for experienced programmers. 72 | I hope the strategies listed here give you a few ideas for taking the next step. 73 | --------------------------------------------------------------------------------