├── .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 | 
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 | 
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 | 
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 |
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 |
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 |
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 |
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------