Python Table Manners- Cut the Cookie Gracefully @ Euro Python 2020

Python Table Manners- Cut the Cookie Gracefully @ Euro Python 2020

### Goals
I expect the audiences to gain knowledge of the tools I mention and the primary usage of them. The tools will cover various aspects of software engineering (e.g., dependencies, testing, security, etc.). Also, I'll purpose how I combine all these tools in my development workflow as a sample for how the audiences can integrate these tools into their workflow.

### Outline for 30 minutes
* Dependency Management (4 min)
* Testing - Don't let your customer debug for you (4 min)
* Style Check and auto-fix (4 min)
* Task Management - No more repetitive typing (3 min)
* pre-commit - Prevent committing bad code into codebase (3 min)
* commitizen-tool - How good commit message can help (4 min)
* Security (3 min)
* Cookiecutter - Wrap up all the tools (3 min)
* Q & A (2 min)

### Outline for 45 minutes
* Dependency Management (5 min)
* Testing - Don't let your customer debug for you (5 min)
* Style Check and auto-fix (5 min)
* Task Management - No more repetitive typing (5 min)
* pre-commit - Prevent committing bad code into codebase (5 min)
* commitizen-tool - How good commit message can help (5 min)
* Security (5 min)
* Continuous Integration - Assemble all the trivial steps (5 min)
* Cookiecutter - Wrap up all the tools (3 min)
* Q & A (2 min)

3e049ed33195d00a8b745b16c63dce6e?s=128

Lee Wei

July 21, 2020
Tweet

Transcript

  1. @clleew Python Table Manners: Cut the Cookie Gracefully A Guideline

    Toward Cleaner Code
  2. @clleew $ cat speaker.py __name__ = "Wei Lee / 李唯"

    __position__ = [ "Software Engineer @ Rakuten Slice", "Volunteer @ PyCon Taiwan", "Maintainer of commitizen-tools", ] __twitter__ = " @clleew" __github__ = " Lee-W" __blog__ = " http://lee-w.github.io"
  3. @clleew Outline • Clean up the table - Dependency Management

    • Put the correct tablewares - Testing • Use the tablewares elegantly - Coding Style • Mnemonic phrase - Task management • Say please! - Check before git operation • Speak formally - Cultivate a git commit convention • Safety matters - Security Issues • Where's the Cookie? - Project template
  4. @clleew Dependency Management Clean up the table

  5. @clleew How we used to start a Python project

  6. @clleew Sometimes we just forgot to… • ... activate /

    deactivate virtual environment • ... add package into requirements.txt
  7. @clleew Pipenv Python Dev Workflow for Humans • manage virtual

    environment + package → update at once → No more manual sync up! • generate hashes from packages → ensure getting the same packages next time
  8. @clleew Initial Virtual Environment

  9. @clleew Empty Pipfile [[source]] name = "pypi" url = "https://pypi.org/simple"

    verify_ssl = true [dev-packages] [packages] [requires] python_version = "3.7"
  10. @clleew Install Package

  11. @clleew Pipfile with “requests” [[source]] name = "pypi" url =

    "https://pypi.org/simple" verify_ssl = true [dev-packages] [packages] requests = "*" [requires] python_version = "3.7" [packages] requests = "*"
  12. @clleew "requests": { "hashes": [ "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" ], "index": "pypi",

    "version": "==2.22.0" }, { ...... "default": { ...... "requests": { "hashes": [ "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" ], "index": "pypi", "version": "==2.22.0" }, "urllib3": { "hashes": [ "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" ], "version": "==1.25.6" } }, ...... } Pipfile.lock with “requests”
  13. @clleew "requests": { "hashes": [ "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" ], "index": "pypi",

    "version": "==2.22.0" }, { ...... "default": { ...... "requests": { "hashes": [ "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" ], "index": "pypi", "version": "==2.22.0" }, "urllib3": { "hashes": [ "sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", "sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86" ], "version": "==1.25.6" } }, ...... } Pipfile.lock with “requests” You’re guaranteed to install the exact same package next time.
  14. @clleew

  15. @clleew Install package only in dev

  16. @clleew Pipfile with “pytest" [[source]] name = "pypi" url =

    "https://pypi.org/simple" verify_ssl = true [dev-packages] pytest = "*" [packages] requests = "*" [requires] python_version = "3.7" [dev-packages] pytest = "*"
  17. @clleew Run in virtual environment

  18. @clleew Some people might say that pipenv ... • ...

    does not update frequently. • ... locks slowly. • ... does not syncs up with install_requires in setup.py
  19. @clleew Poetry

  20. @clleew Poetry commands

  21. @clleew Testing Put the correct tablewares

  22. @clleew unittest Unit testing framework in Python Standard Library

  23. @clleew pytest

  24. @clleew Why pytest • More Pythonic • Compatible with old

    unittest tests • No more assert.+ (e.g., assertEqual, assertTrue, etc.)
 → just assert • Better test discovery • Advance features: mark, parameterize, etc. • Plenty of plugins
  25. @clleew Run pytest

  26. @clleew Run pytest

  27. @clleew always run python inside a virtual environment

  28. @clleew unittest style tests import unittest from atta.partner import sponsor

    class TestSponsor(unittest.TestCase): def setUp(self): sponsors = sponsor.get_all_sponsors('./data/packages.yaml', './data/sponsors.yaml') self.sponsors = sponsors ...... def test_sponsor_number(self): self.assertEqual(len(self.sponsors), 1) .......
  29. @clleew unittest style tests import unittest from atta.partner import sponsor

    class TestSponsor(unittest.TestCase): def setUp(self): sponsors = sponsor.get_all_sponsors('./data/packages.yaml', './data/sponsors.yaml') self.sponsors = sponsors ...... def test_sponsor_number(self): self.assertEqual(len(self.sponsors), 1) ....... Prepare all the data needed in test cases
  30. @clleew unittest style tests import unittest from atta.partner import sponsor

    class TestSponsor(unittest.TestCase): def setUp(self): sponsors = sponsor.get_all_sponsors('./data/packages.yaml', './data/sponsors.yaml') self.sponsors = sponsors ...... def test_sponsor_number(self): self.assertEqual(len(self.sponsors), 1) .......
  31. @clleew unittest style tests import unittest from atta.partner import sponsor

    class TestSponsor(unittest.TestCase): def setUp(self): sponsors = sponsor.get_all_sponsors('./data/packages.yaml', './data/sponsors.yaml') self.sponsors = sponsors ...... def test_sponsor_number(self): self.assertEqual(len(self.sponsors), 1) .......
  32. @clleew pytest style tests import pytest from report_generator.partner import sponsor

    class TestSponsor: @pytest.fixture(scope="class") def sponsors(self): return sponsor.get_all_sponsors("test/data/packages.yaml", “test/data/sponsors.yaml") ...... def test_sponsor_number(self, sponsors): assert len(sponsors) == 1 ......
  33. @clleew pytest style tests import pytest from report_generator.partner import sponsor

    class TestSponsor: @pytest.fixture(scope="class") def sponsors(self): return sponsor.get_all_sponsors("test/data/packages.yaml", “test/data/sponsors.yaml") ...... def test_sponsor_number(self, sponsors): assert len(sponsors) == 1 ...... Prepare the data needed in separate fixtures
  34. @clleew pytest style tests import pytest from report_generator.partner import sponsor

    class TestSponsor: @pytest.fixture(scope="class") def sponsors(self): return sponsor.get_all_sponsors("test/data/packages.yaml", “test/data/sponsors.yaml") ...... def test_sponsor_number(self, sponsors): assert len(sponsors) == 1 ......
  35. @clleew pytest style tests import pytest from report_generator.partner import sponsor

    @pytest.fixture(scope="function") def sponsors(): return sponsor.get_all_sponsors("test/data/packages.yaml", "test/data/sponsors.yaml") def test_sponsor_number(sponsors): assert len(sponsors) == 1
  36. @clleew pytest style tests import pytest from report_generator.partner import sponsor

    @pytest.fixture(scope="function") def sponsors(): return sponsor.get_all_sponsors("test/data/packages.yaml", "test/data/sponsors.yaml") def test_sponsor_number(sponsors): assert len(sponsors) == 1
  37. @clleew Configuration - pytest.ini [pytest] addopts = --strict-markers norecursedirs =

    .* build dist CVS _darcs {arch} *.egg venv env virtualenv Configure through pyproject.toml will be when 6.0 released #7247
  38. @clleew Pytest - powerful plugins • pytest-mock: Replace objects that

    are hard to test with fake objects • pytest-cov: Generate test coverage report • pytest-xdist: Distributed testing • and etc.
  39. @clleew Coding Style Use the tablewares elegantly

  40. @clleew flake8 • Enforcing style consistency across Python project •

    Check possible errors before running your program • Eliminate bad style
  41. @clleew import os os = "My Operating system" Example -

    bad_code.py
  42. @clleew import os os = "My Operating system" redefinition of

    imported library
 (possible error) Example - bad_code.py
  43. @clleew import os os = "My Operating system" additional space


    (bad coding style) redefinition of imported library
 (possible error) Example - bad_code.py
  44. @clleew Run flake8

  45. @clleew Configuration - setup.cfg [flake8] ignore = # F632: use

    ==/!= to compare str, bytes, and int literals F632, # W503: Line break occurred before a binary operator W503, # E501: Line too long E501, # E203: Whitespace before ':' (for black) E203 exclude = .git, __pycache__, build, dist max-line-length = 88
  46. @clleew Pylint

  47. @clleew import os os = "My Operating system" additional space


    (bad coding style) redefinition of imported library
 (possible error) Example - bad_code.py
  48. @clleew Run pylint

  49. @clleew Run pylint with "-r" argument A bunch of reports

    that 
 you can compare 
 with your previous run
  50. Configuration - pyproject.toml [tool.pylint.messages_control] disable = [ "bad-continuation", "missing-function-docstring", "missing-module-docstring",

    "invalid-name" ] [tool.pylint.format] max-line-length = 88
  51. @clleew mypy • Static type checker • Compile-time type checking


    → avoid possible runtime errors • Type annotation enhances readability
 → machine-checked documentation
  52. from typing import List def func(vals: List[str]): for val in

    vals: assert isinstance(val, str) func([1, 2, 3]) Example - error_type.py List[str] [1, 2, 3]
  53. from typing import List def func(vals: List[str]): for val in

    vals: assert isinstance(val, str) func([1, 2, 3]) Example - error_type.py List[str] [1, 2, 3]
  54. Run mypy

  55. Run mypy all the files with .py extension

  56. Run mypy ignore errors that libs that is not type

    annotated
  57. @clleew Configuration - setup.cfg [mypy] files = **/*.py ignore_missing_imports =

    true follow_imports = silent warn_redundant_casts = True warn_unused_ignores = True warn_unused_configs = True [mypy] files = **/*.py ignore_missing_imports = true
  58. @clleew Configuration - setup.cfg [mypy] files = **/*.py ignore_missing_imports =

    true follow_imports = silent warn_redundant_casts = True warn_unused_ignores = True warn_unused_configs = True [mypy] files = **/*.py ignore_missing_imports = true
  59. Run mypy with configuration

  60. @clleew One step forward Fix the style automatically

  61. Black

  62. Run Black

  63. How Black reformat

  64. How Black reformat

  65. Why Black? The black code style is not configurable.

  66. Why Black? The black code style is not configurable. No

    more arguing about which style is better
  67. –The Zen of Python, by Tim Peters “There should be

    one-- and preferably only one --obvious way to do it.”
  68. @clleew Configuration - pyproject.toml [tool.black] line-length = 88 include =

    '\.pyi?$' exclude = ''' /( \.eggs | \.git | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist )/ '''
  69. Isort

  70. Example wrong_import_order.py import django import os import some_custom_package import flask

    import datetime ...
  71. According to PEP8 • Imports should be grouped in the

    following order: 1. Standard library imports. 2. Related third party imports. 3. Local application/library specific imports. • You should put a blank line between each group of imports.
  72. Run Isort

  73. Run Isort import datetime import os import django import flask

    import some_custom_package ...
  74. @clleew Configuration - pyproject.toml [tool.isort] line_length = 88 multi_line_output =

    3 include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true
  75. Style formatting and linting

  76. None
  77. @clleew Task Management Mnemonic phrase

  78. Invoke • Task execution tool that can manage your commands

    • It’s like a Makefile but in Python.
  79. Invoke in practice (rg-cli)

  80. Before invoke in rg-cli

  81. Before invoke in rg-cli

  82. After invoke in rg-cli

  83. List all commands

  84. How ? - tasks.py from invoke import task VENV_PREFIX =

    "pipenv run" ...... @task def install(cmd): """Install script in pipenv environment""" cmd.run(f"{VENV_PREFIX} python setup.py install")
  85. How ? - tasks.py from invoke import task VENV_PREFIX =

    "pipenv run" ...... @task def install(cmd): """Install script in pipenv environment""" cmd.run(f"{VENV_PREFIX} python setup.py install")
  86. How ? - tasks.py from invoke import task VENV_PREFIX =

    "pipenv run" ...... @task def install(cmd): """Install script in pipenv environment""" cmd.run(f"{VENV_PREFIX} python setup.py install")
  87. Modulize - Namespace

  88. Autocomplete

  89. Autocomplete

  90. Autocomplete

  91. Why not use Makefile? 1.We’re Python developers! 2.Some tasks might

    not be easy to handle through shell script. 3.Combining Python and shell script
  92. @clleew Check before git operation Say please!

  93. @clleew pre-commit • Why? • Sometimes we still forget to

    run the check commands. • Prevent committing bad code into codebase • How? • pre-commit runs these commands before we do git operations (e.g., git push, git commit)
  94. @clleew Config (customized hooks) .pre-commit-config.yaml repos: - repo: local hooks:

    - id: style-reformat name: style-reformat stages: [commit] language: system pass_filenames: false entry: inv style.reformat types: [python] - id: style-check name: style-check stages: [push] language: system pass_filenames: false entry: inv style types: [python] ......
  95. @clleew Config (customized hooks) .pre-commit-config.yaml repos: - repo: local hooks:

    - id: style-reformat name: style-reformat stages: [commit] language: system pass_filenames: false entry: inv style.reformat types: [python] - id: style-check name: style-check stages: [push] language: system pass_filenames: false entry: inv style types: [python] ...... entry: inv style.reformat - id: style-reformat stages: [commit]
  96. @clleew Config (customized hooks) .pre-commit-config.yaml repos: - repo: local hooks:

    - id: style-reformat name: style-reformat stages: [commit] language: system pass_filenames: false entry: inv style.reformat types: [python] - id: style-check name: style-check stages: [push] language: system pass_filenames: false entry: inv style types: [python] ...... stages: [push] - id: style-check
  97. @clleew Config (existing hooks) .pre-commit-config.yaml ...... - repo: https://github.com/pre-commit/pre-commit-hooks rev:

    v3.1.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace args: [--markdown-linebreak-ext=md]
  98. @clleew Setup pre-commit

  99. @clleew Run pre-commit

  100. @clleew Run pre-commit

  101. @clleew Cultivate a git commit convention Speak formally

  102. None
  103. Hard to find to the right version

  104. commitizen-tools

  105. Commit - cz commit

  106. Commit - cz commit

  107. Commit - cz commit

  108. Commit - cz commit

  109. Commit - cz commit

  110. Commit - cz commit

  111. Commit - cz commit

  112. Standardized git commits

  113. Standardized git commits

  114. Other Commitizen features • pre-commit hook prevents you from not

    using commitizen • customizable commit rules • auto bump project version (SemVar) • auto generate changelog (Keep a Changelog)
  115. Let’s Sprint!

  116. Let’s Sprint!

  117. @clleew Security Issues Safety matters

  118. None
  119. None
  120. Safety Dependency Checking

  121. Check vulnerability in current environment

  122. Check vulnerability in current environment

  123. But if you use pipenv …

  124. But if you use pipenv …

  125. How does Safety work? • Search the vulnerability in CVS

    (Common Vulnerabilities and Exposures) • Free DB: update monthly • Paid DB: update realtime
  126. Bandit Static Analysis

  127. Check Common Security Issue in Python Code

  128. Check Common Security Issue in Python Code

  129. Check Common Security Issue in Python Code

  130. Check Common Security Issue in Python Code

  131. Check Common Security Issue in Python Code

  132. Not all the warnings should be fixed • Add exclude

    into your bandit configuration • Add # nosec after the end of the code to skip the warning
  133. @clleew

  134. @clleew Project template Where's the Cookie?

  135. @clleew We want these manners in all our Python projects

  136. @clleew We want these manners in all our Python projects

    ❗ But... we don't want to configure it everytime ❗
  137. @clleew Create a project template once and initialize projects through

    it
  138. @clleew Lee-W / cookiecutter-python-template

  139. @clleew

  140. @clleew

  141. @clleew

  142. @clleew

  143. @clleew

  144. @clleew How to make a template? cookiecutter.json

  145. @clleew How to make a template? cookiecutter.json

  146. @clleew Template structure Cookiecutter configuration Template (Use Jinja)

  147. @clleew An example in template {{cookiecutter.project_slug}}/tasks/env.py from invoke import task

    from tasks.common import VENV_PREFIX @task def init(ctx): """Install production dependencies""" {% if cookiecutter.dependency_management_tool == 'pipenv' -%} ctx.run("pipenv install --deploy") {%- elif cookiecutter.dependency_management_tool == 'poetry' -%} ctx.run("poetry install --no-dev") {%- endif %} {% if cookiecutter.dependency_management_tool == 'pipenv' -%} ctx.run("pipenv install --deploy")
  148. @clleew An example in template {{cookiecutter.project_slug}}/tasks/env.py from invoke import task

    from tasks.common import VENV_PREFIX @task def init(ctx): """Install production dependencies""" {% if cookiecutter.dependency_management_tool == 'pipenv' -%} ctx.run("pipenv install --deploy") {%- elif cookiecutter.dependency_management_tool == 'poetry' -%} ctx.run("poetry install --no-dev") {%- endif %} {% if cookiecutter.dependency_management_tool == 'pipenv' -%} ctx.run("pipenv install --deploy")
  149. @clleew An example in template {{cookiecutter.project_slug}}/tasks/env.py from invoke import task

    from tasks.common import VENV_PREFIX @task def init(ctx): """Install production dependencies""" {% if cookiecutter.dependency_management_tool == 'pipenv' -%} ctx.run("pipenv install --deploy") {%- elif cookiecutter.dependency_management_tool == 'poetry' -%} ctx.run("poetry install --no-dev") {%- endif %} {%- elif cookiecutter.dependency_management_tool == 'poetry' -%} ctx.run("poetry install --no-dev") {%- endif %}
  150. @clleew import os def remove_pipfile(): os.remove("Pipfile") def main(): if "{{

    cookiecutter.dependency_management_tool }}" != "pipenv": remove_pipfile() if __name__ == "__main__": main() Run before/after project is generated hooks/post_gen_project.py
  151. @clleew Lee-W / cookiecutter-python-template

  152. @clleew

  153. Other Tools • Testing • Hypothesis • nox • Documentation

    • Mkdocs • Sphinx
  154. @clleew Related Talks • Dependency Management • Tzu-ping Chung -

    這樣的開發環境沒問題嗎? (PyCon TW 2018) • Kenneth Reitz - Pipenv: The Future of Python Dependency Management (PyCon US 2018) • Patrick Muehlbauer - Python Dependency Management (PyCon DE 2018) • Test • Chun-Yu Tseng - 快快樂樂成為 Coding Ninja (by pytest) (PyCon APAC 2015) • Florian Bruhin – Pytest: Rapid Simple Testing (Swiss Python Summit 16)
  155. @clleew Related Talks • Style Check • Kyle Knapp -

    Automating Code Quality (PyCon US 2018) • Łukasz Langa - Life Is Better Painted Black, or: How to Stop Worrying and Embrace Auto-Formatting (PyCon US 2019) • Raymond Hettinger - Beyond PEP 8 -- Best practices for beautiful intelligible code (PyCon 2015) • Static Typing • Dustin Ingram - Static Typing in Python (PyCon US 2020) • Task Managements • Thea Flowers - Break the Cycle: Three excellent Python tools to automate repetitive tasks (PyCon US 2019) • Security • Terri Oda - Python Security Tools (PyCon US 2019) • Tennessee Leeuwenburg - Watch out for Safety Bandits! (PyCon AU 2018)
  156. @clleew References • pipenv • poetry • flake8 • pylint

    • mypy • black • isort • invoke • pre-commit • commitizen-tools • safety • bandit • cookiecutter
  157. @clleew

  158. @clleew Let's chat on discord