diff --git a/docs/tutorial/code_quality.md b/docs/tutorial/code_quality.md deleted file mode 100644 index bf6750d..0000000 --- a/docs/tutorial/code_quality.md +++ /dev/null @@ -1 +0,0 @@ -# Code Quality diff --git a/docs/tutorial/code_quality/code_quality.md b/docs/tutorial/code_quality/code_quality.md new file mode 100644 index 0000000..5b67f01 --- /dev/null +++ b/docs/tutorial/code_quality/code_quality.md @@ -0,0 +1,11 @@ +# Code quality + +intro + +???question "Why standardizing my code?" + Allows running tools + +- [pre-commit hooks](pre_commit_hooks.md) +- [formatter: black](formatter.md) +- [type_checking: mypy](type_checking.md) +- [linter: ruff](linter.md) diff --git a/docs/tutorial/code_quality/formatter.md b/docs/tutorial/code_quality/formatter.md new file mode 100644 index 0000000..1f9c5d4 --- /dev/null +++ b/docs/tutorial/code_quality/formatter.md @@ -0,0 +1,14 @@ +# Formatting code with black + +Why manually bother to format your code when you can automate it? + +```yaml title=".pre-commit-config.yaml" +repos: + ... + + - repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black + +``` diff --git a/docs/tutorial/code_quality/linter.md b/docs/tutorial/code_quality/linter.md new file mode 100644 index 0000000..eec394c --- /dev/null +++ b/docs/tutorial/code_quality/linter.md @@ -0,0 +1,53 @@ +# Linting with ruff + +```yaml title=".pre-commit-config.yaml" +repos: + ... + + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.257 + hooks: + - id: ruff + args: [--fix] + +``` + +```toml title="pyproject.toml" +# https://github.com/charliermarsh/ruff +[tool.ruff] +line-length = 88 +target-version = "py38" +# https://beta.ruff.rs/docs/rules/ +extend-select = [ + "E", # style errors + "W", # style warnings + "F", # flakes + "D", # pydocstyle + "I", # isort + "U", # pyupgrade + # "S", # bandit + "C", # flake8-comprehensions + "B", # flake8-bugbear + "A001", # flake8-builtins + "RUF", # ruff-specific rules +] +# I do this to get numpy-style docstrings AND retain +# D417 (Missing argument descriptions in the docstring) +# otherwise, see: +# https://beta.ruff.rs/docs/faq/#does-ruff-support-numpy-or-google-style-docstrings +# https://github.com/charliermarsh/ruff/issues/2606 +extend-ignore = [ + "D100", # Missing docstring in public module + "D107", # Missing docstring in __init__ + "D203", # 1 blank line required before class docstring + "D212", # Multi-line docstring summary should start at the first line + "D213", # Multi-line docstring summary should start at the second line + "D401", # First line should be in imperative mood + "D413", # Missing blank line after last section + "D416", # Section name should end with a colon +] + +[tool.ruff.per-file-ignores] +"tests/*.py" = ["D", "S"] +"setup.py" = ["D"] +``` diff --git a/docs/tutorial/code_quality/pre_commit_hooks.md b/docs/tutorial/code_quality/pre_commit_hooks.md new file mode 100644 index 0000000..1645dc2 --- /dev/null +++ b/docs/tutorial/code_quality/pre_commit_hooks.md @@ -0,0 +1,18 @@ +# pre-commit + +Clean your room before going out! + +```text title="File Structure" +src/ +└── pydev_tutorial/ + ├── __init__.py +.pre-commit-config.yaml +``` + +```yaml title=".pre-commit-config.yaml" +repos: + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.12.1 + hooks: + - id: validate-pyproject +``` diff --git a/docs/tutorial/code_quality/type_checking.md b/docs/tutorial/code_quality/type_checking.md new file mode 100644 index 0000000..70d0ccb --- /dev/null +++ b/docs/tutorial/code_quality/type_checking.md @@ -0,0 +1,26 @@ +# Enforce types with mypy + +```yaml title=".pre-commit-config.yaml" +repos: + ... + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.1.1 + hooks: + - id: mypy + files: "^src/" + # # you have to add the things you want to type check against here + # additional_dependencies: + # - numpy +``` + +```toml title="pyproject.toml" +# https://mypy.readthedocs.io/en/stable/config_file.html +[tool.mypy] +files = "src/**/" +strict = true +disallow_any_generics = false +disallow_subclassing_any = false +show_error_codes = true +pretty = true +``` diff --git a/docs/tutorial/github/continuous_integration.md b/docs/tutorial/github/continuous_integration.md new file mode 100644 index 0000000..aee2ee1 --- /dev/null +++ b/docs/tutorial/github/continuous_integration.md @@ -0,0 +1,90 @@ +# Continuous integration with Actions + +```text title="File Structure" +.github/ +└── workflows/ + └── ci.yaml +src/ +tests/ +``` + +```yaml title="ci.yaml" +name: CI + +on: + push: + branches: + - main + tags: + - "v*" + pull_request: + workflow_dispatch: + schedule: + # run every week (for --pre release tests) + - cron: "0 0 * * 0" + +jobs: + check-manifest: + # check-manifest is a tool that checks that all files in version control are + # included in the sdist (unless explicitly excluded) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: pipx run check-manifest + + test: + name: ${{ matrix.platform }} (${{ matrix.python-version }}) + runs-on: ${{ matrix.platform }} + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11"] + platform: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - name: 🛑 Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.11.0 + with: + access_token: ${{ github.token }} + + - uses: actions/checkout@v3 + + - name: 🐍 Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache-dependency-path: "pyproject.toml" + cache: "pip" + + - name: Install pip + run: | + python -m pip install -U pip + + # if running a cron job, we add the --pre flag to + # test against pre-releases + - name: Install Dependencies + run: > + python -m pip install .[test] + ${{ github.event_name == 'schedule' && '--pre' || '' }} + + - name: 🧪 Run Tests + run: pytest --color=yes --cov --cov-report=xml --cov-report=term-missing + + # If something goes wrong with --pre tests, + # we can open an issue in the repo + - name: 📝 Report --pre Failures + if: failure() && github.event_name == 'schedule' + uses: JasonEtco/create-an-issue@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PLATFORM: ${{ matrix.platform }} + PYTHON: ${{ matrix.python-version }} + RUN_ID: ${{ github.run_id }} + TITLE: "[test-bot] pip install --pre is failing" + with: + filename: .github/TEST_FAIL_TEMPLATE.md + update_existing: true + + - name: Coverage + uses: codecov/codecov-action@v3 +``` diff --git a/docs/tutorial/github/deployment.md b/docs/tutorial/github/deployment.md new file mode 100644 index 0000000..28fc81c --- /dev/null +++ b/docs/tutorial/github/deployment.md @@ -0,0 +1,55 @@ +# Deployment to PyPi + +## Secrets + +```yaml +name: CI + +on: + push: + branches: + - main + tags: + - "v*" + pull_request: + workflow_dispatch: + schedule: + # run every week (for --pre release tests) + - cron: "0 0 * * 0" + +jobs: + ... + + deploy: + name: Deploy + needs: test + if: > + success() + && startsWith(github.ref, 'refs/tags/') + && github.event_name != 'schedule' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: 🐍 Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + + - name: 👷 Build + run: | + python -m pip install build + python -m build + + - name: 🚢 Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.TWINE_API_KEY }} + + - uses: softprops/action-gh-release@v1 + with: + generate_release_notes: true + +``` diff --git a/docs/tutorial/github.md b/docs/tutorial/github/github.md similarity index 100% rename from docs/tutorial/github.md rename to docs/tutorial/github/github.md diff --git a/docs/tutorial/github/github_apps.md b/docs/tutorial/github/github_apps.md new file mode 100644 index 0000000..3bba51a --- /dev/null +++ b/docs/tutorial/github/github_apps.md @@ -0,0 +1,5 @@ +# Using Github apps + +## Codecov + +## Pre-commit diff --git a/docs/tutorial/github/github_pages.md b/docs/tutorial/github/github_pages.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/tutorial/publishing.md b/docs/tutorial/github/publishing.md similarity index 100% rename from docs/tutorial/publishing.md rename to docs/tutorial/github/publishing.md diff --git a/docs/tutorial/pre_requisite/ide.md b/docs/tutorial/pre_requisite/ide.md new file mode 100644 index 0000000..1110174 --- /dev/null +++ b/docs/tutorial/pre_requisite/ide.md @@ -0,0 +1,13 @@ +# IDE: VSCode + +## Download and install VSCode + +## Basics + +## Debugging + +## Some settings + +### Starting from the command line + +### Setting a ruler diff --git a/docs/tutorial/pre_requisite/version_control.md b/docs/tutorial/pre_requisite/version_control.md new file mode 100644 index 0000000..9477274 --- /dev/null +++ b/docs/tutorial/pre_requisite/version_control.md @@ -0,0 +1,25 @@ +# Git + +Link to tutorial + +=== "macOS" + + 1. + +=== "Linux" + + 1. + +=== "Windows" + + 1. Git for Windows + +## Typical workflow + +```shell +git status +git checkout -b mybranch +git add +git commit -m "" +git push +``` diff --git a/docs/tutorial/pre_requisite/virtual_environment.md b/docs/tutorial/pre_requisite/virtual_environment.md new file mode 100644 index 0000000..ed32d57 --- /dev/null +++ b/docs/tutorial/pre_requisite/virtual_environment.md @@ -0,0 +1,5 @@ +# Virtual environments with Conda + +miniconda + +mamba? diff --git a/docs/tutorial/summary.md b/docs/tutorial/summary.md new file mode 100644 index 0000000..52a1319 --- /dev/null +++ b/docs/tutorial/summary.md @@ -0,0 +1,43 @@ + +# Summary + +Tooling, testing, packaging... These are words floating around but they sound +like work and we all have enough of that already, so why bother? In the end, you +just want code that works and that can be used by people! + +And that's exactly where tooling comes in. In this tutorial, we are going to +walk you through a set of tools. Each section is straightforward and shouldn't +take long to absorb. This is the pyramid scheme of programming: **for a small +investment you will get high returns**. + +!!!tip "What are these tools for?" + These tools are all about automating the tedious tasks and forcing you to + perform the dull chores bit by bit rather than just before release! + +Here is a list of the main tools we will go over: + +- `Conda`: virtual environments to code in a safe space. +- `git`: keep track of changes and benefit from all the advantages of Github. +- `VSCode`: a lightweight IDE to be more effective at writing and debugging +code. +- `pyproject.toml`: a single file with all your Python packaging information. +- `pytest`: get in the habit of writing tests to make your code bullet-proof. +- `black`: make your code more readable using automatic formatting. +- `mypy`: enforce types, and never be surprised by inputs and outputs. +- `ruff`: improve code quality using a linter. +- `codecov`: find the blind spots of your tests. +- `pre-commit`: automate formatter, linter, or type checker before every commit. +- `Github actions`: automate everything everywhere all at once! + +Apply these tools and you will see the result rapidly![^1] + +!!! note "Example repositories" + This tutorial guides you through the same tools that are automatically + installed if you create a repository using + [pyrepo-copier](https://github.com/pydev-guide/pyrepo-copier). + + Finally, in this tutorial we also create some code. The + [pydev-tutorial](https://github.com/pydev-guide/pydev-tutorial) repository + corresponds to what you will implement by following this step by step. + +[^1]: Did you know that 9 out of 10 programmers recommend these tools? diff --git a/docs/tutorial/testing/code_to_be_tested.md b/docs/tutorial/testing/code_to_be_tested.md new file mode 100644 index 0000000..119a545 --- /dev/null +++ b/docs/tutorial/testing/code_to_be_tested.md @@ -0,0 +1,75 @@ +# Adding code + +Let's create some code to be tested. Imagine we want to model the speed of a +swallow carrying a cargo. We know from movies that there are multiple swallow +species and that they are not all migratories. So first, we could create an +`enum` to account for the species: + +```python +from enum import Enum + + +class SwallowSpecies(str, Enum): + AFRICAN = "african" # non-migratory + EUROPEAN = "european" # migratory +``` + +Next, we want to define our swallow and its cargo, so let's define a class: + +```python +class Swallow: + def __init__(self, species: str, cargo_weight: float = 0) -> None: + if cargo_weight < 0: + raise ValueError("Cargo weight cannot be negative") + + if species.upper() != "AFRICAN" and species.upper() != "EUROPEAN": + raise ValueError('Species must be either "african" or "european"') + + self.species = SwallowSpecies[species.upper()] + self._cargo_weight = cargo_weight +``` + +We made `_cargo` private because we don't want negative weights to be set after +initialization. How to make sure of that? Let's prevent it in the `setter`: + +```python +class Swallow: + ... + + @property + def cargo_weight(self) -> float: + return self._cargo_weight + + @cargo_weight.setter + def cargo_weight(self, value: float) -> None: + if value < 0: + raise ValueError("Cargo weight cannot be negative") + self._cargo_weight = value +``` + +Right, now we cannot change the cargo weight. Let's finally add methods to +compute the speed or tell users whether the species is migratory: + +```python +class Swallow: + ... + + def get_speed(self) -> float: + if self.cargo_weight >= 0.45: + # the swallow is going backward, one pound is too heavy + return -60.0 / (1 + self._cargo_weight) + return 60.0 / (1 + self._cargo_weight) + + def is_migratory(self) -> bool: + return self.species == SwallowSpecies.AFRICAN +``` + +Now that we have code, we can write tests that can verify it behaves as +expected! + +!!!tip "The code could not be simpler, why test it?" + What if you decided to add an Asian swallow that is migratory. You might + modify `SwallowSpecies` and then the `Swallow.__init__` but forget to update + `Swallow.is_migratory`. How would you know about the problem before running + into it? By running tests automatically! Never underestimate our propension + to forget things! diff --git a/docs/tutorial/testing/real_tests.md b/docs/tutorial/testing/real_tests.md new file mode 100644 index 0000000..dab5b36 --- /dev/null +++ b/docs/tutorial/testing/real_tests.md @@ -0,0 +1,40 @@ +# Test for real + +It passes, but this does not give us any information about our code. Let's +replace it with actual tests: + +```python +def test_migratory(): + """Test that the European swallow is migratory, while the African swallow is + not.""" + european_swallow = Swallow(species="european") + assert european_swallow.is_migratory() + + african_swallow = Swallow(species="african") + assert not african_swallow.is_migratory() + + +def test_unladen_velocity(): + """Test that unladen swallows fly at 60 km/h.""" + european_swallow = Swallow(species="european", cargo_weight=0) + assert european_swallow.get_speed() == 60.0 + + african_swallow = Swallow(species="african") + assert african_swallow.get_speed() == 60.0 +``` + +These tests are naive (see next sections) but they work. Or... do they? That's +right, `test_migratory` should actually not pass. + +???question "Why is that?" + It seems we were a bit too fast writig our code and we made a mistake. Is + it thecode or the test that is wrong? The European swallows should be + migratory, which is what the tests checks. Indeed, there is a mistake in + the code `return self.species == SwallowSpecies.AFRICAN` should actually be: + `return self.species == SwallowSpecies.EUROPEAN` + + Good that we tested our code! + + Writing the tests before implementing the code is called + **test-driven development** and you might want take this approach when + coding. diff --git a/docs/tutorial/testing/test_conclusion.md b/docs/tutorial/testing/test_conclusion.md new file mode 100644 index 0000000..7c7c922 --- /dev/null +++ b/docs/tutorial/testing/test_conclusion.md @@ -0,0 +1,5 @@ +# Conclusion + +Tests are very powerful and can prevent bugs from insidiously live in your code +without your knowledge. In the next section, we will go one step further and +automate not only tests but also principles to improve code quality! diff --git a/docs/tutorial/testing/test_coverage.md b/docs/tutorial/testing/test_coverage.md new file mode 100644 index 0000000..28a1688 --- /dev/null +++ b/docs/tutorial/testing/test_coverage.md @@ -0,0 +1,91 @@ +# Testing coverage + +Code coverage tells you how much of your code is ran by your tests. While this +is a crude metrics and does not ensure that your tests are good, at least +it points your towards blind spots in your tests! + +So `coverage`, one more tool for testing? Fortunately there is a test coverage +plugin for `pytest`: `pytest-cov`. + +## Adding pytest-cov + +The first thing is to add the dependency: + +```toml title="pyproject.toml" +[project.optional-dependencies] +# add dependencies used for testing here +test = ["pytest", "pytest-cov"] +``` + +Then, let's add some parameters to `coverage`: + +```toml title="pyproject.toml" +# https://coverage.readthedocs.io/en/6.4/config.html +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "@overload", + "except ImportError", + "\\.\\.\\.", + "raise NotImplementedError()", +] +[tool.coverage.run] +source = ["src"] +``` + +Finally, let's grab the dependencies using the `test` dependency group: + +```console +pip install -e ".[test]" +``` + +## Run your tests with coverage + +Now you can check coverage report using the following `pytest` command: + +
+ +```console +$ pytest --color=yes --cov --cov-report=html --cov-report=term-missing +====================== test session starts ========================= +platform darwin -- Python 3.10.10, pytest-7.3.0, pluggy-1.0.0 +rootdir: /Users/joran.deschamps/git/pydev/pydev-tutorial +configfile: pyproject.toml +testpaths: tests +plugins: cov-4.0.0 +collected 17 items + +tests/test_swallow.py ................. [100%] + +--------- coverage: platform darwin, python 3.10.10-final-0 ---------- +Name Stmts Miss Cover Missing +-------------------------------------------------------------- + +src/pydev_tutorial/__init__.py 7 2 71% 7-8 +src/pydev_tutorial/swallow.py 26 2 92% 40, 43 +-------------------------------------------------------------- + +TOTAL 33 4 88% +Coverage HTML written to dir htmlcov + +======================= 17 passed in 0.14s ======================== +``` + +
+ +This should have created a `htmlcov` directory. Open `index.html` and click on +the files name to see which lines are missing in the test coverage! + +In `__init__.py`, there are: + +```python +except PackageNotFoundError: + __version__ = "uninstalled" +``` + +This can be excluded using the `exclude_lines` in `pyproject.toml`. But in +general you don't want to exclude lines just because we didn't test for them! + +???question "What lines are missing in `swallow.py`?" + Check the missing lines and implement tests to reach 100% coverage! diff --git a/docs/tutorial/testing/test_fixtures.md b/docs/tutorial/testing/test_fixtures.md new file mode 100644 index 0000000..cec2694 --- /dev/null +++ b/docs/tutorial/testing/test_fixtures.md @@ -0,0 +1,56 @@ + +# `pytest` fixtures + +A particular useful thing in `pytest` are fixtures. These are default or custom +values that can be passed as parameters to your tests. + +For instance, `pytest` has by default a `tmp_path` fixture, which can directly +be passed to your test: + +```python +from pathlib import Path + +def test_something(tmp_path) + # save our file in a temporary folder created by pytest + save_something(tmp_path, name='myfile.txt') + + # check that save_something worked + assert (tmp_path / 'myfile.txt').exists() + + # do more checks about the correctness of the file + ... + + # we don't need to delete the file, pytest will take care of the folder! +``` + +This is really powerful! + +You can also create your own fixtures: + +```python +@pytest.fixture +def unhappy_european_swallow(): + return Swallow(species="european", cargo_weight=0.45) + + +def test_swallow_going_home(unhappy_european_swallow): + """Test that unhappy European swallows are going home.""" + assert unhappy_european_swallow.get_speed() < 0 + +``` + +## The `conftest.py` file + +Your fixtures can be shared across multiple test files by simply defining them +in a separate file called `conftest.py`: + +```text title="File Structure" +src/ +└── pydev_tutorial/ + ├── __init__.py + └── swallow.py +tests/ +├── __init__.py +├── conftest.py +└── test_swallow.py +``` diff --git a/docs/tutorial/testing/test_for_errors.md b/docs/tutorial/testing/test_for_errors.md new file mode 100644 index 0000000..79b1650 --- /dev/null +++ b/docs/tutorial/testing/test_for_errors.md @@ -0,0 +1,16 @@ +# Test for errors + +Errors are often encountered in python and they are useful to tell users that +they are doing something wrong. `pytest` allows you to easily test if the +correct error is raised. + +For instance, let's test initializing a swallow with negative weight: + +```python +@pytest.mark.parametrize("species", ["european", "african"]) +def test_swallow_negative_cargo_weight(species): + """Test that cargo weight cannot be negative.""" + swallow = Swallow(species=species) + with pytest.raises(ValueError): + swallow.cargo_weight = -1 +``` diff --git a/docs/tutorial/testing/test_with_parameters.md b/docs/tutorial/testing/test_with_parameters.md new file mode 100644 index 0000000..03530f2 --- /dev/null +++ b/docs/tutorial/testing/test_with_parameters.md @@ -0,0 +1,53 @@ + +# Test with parameters + +We called the tests "naive" before, simply because there are tons of features in +`pytest` that can improve your tests. + +We tested both European and African species in the first test. Following this +logic, adding an Asian species will require adding an additional two lines +there. There should be a more elegant way. + +In the second test, we only check a single value of `cargo_weight`, how do we +know if the other values also lead to correct results? + +Let's investigate the decorator `@pytest.mark.parametrize`. This decorator, +placed before a test, helps giving a range of values to use as parameters to +the test. For instance: + +```python +@pytest.mark.parametrize("species", ["european", "african"]) +@pytest.mark.parametrize("cargo_weight", [0, 0.1, 0.2, 0.3, 0.4]) +def test_swallow_velocity(species, cargo_weight): + """Test that the velocity of the swallow is correct.""" + swallow = Swallow(species=species) + swallow.cargo_weight = cargo_weight + + # Can you tell why this test is not good? + assert swallow.get_speed() == pytest.approx(60.0 / (1 + cargo_weight)) + + +@pytest.mark.parametrize( + "species, is_migratory", [("european", True), ("african", False)] +) +def test_swallow_migration(species, is_migratory): + """Test that the European swallow is migratory, while the African swallow is + not.""" + swallow = Swallow(species=species) + assert swallow.is_migratory() == is_migratory +``` + +As you might have noticed, a parameter defined in `@pytest.mark.parametrize` +needs to appear in the test function signature. Secondly, you can defined +multiple parameters in one decorator, their values are then passed as tuples +(`("african", False)`). If there are multiple `@pytest.mark.parametrize` +decorators, then all values of one are run against the values of the other! + +Run the tests! + +???question "Do you notice something wrong with the tests?" + It seems we are testing the `cargo_weight` only until `0.4`, but in our + code, the behaviour changes at `0.45`! That's a pretty big oversight. + + Tests are only as good as we write them! Try writing a test accounting for + the change in values. diff --git a/docs/tutorial/testing/testing.md b/docs/tutorial/testing/testing.md new file mode 100644 index 0000000..015a95f --- /dev/null +++ b/docs/tutorial/testing/testing.md @@ -0,0 +1,28 @@ + +# Why test? + +Testing is to programming what sport is to most people: everybody agrees it is +important but nobody has time for it. In this chapter, we will introduce the +minimum you need to perform tests with `pytest` and even enjoy it! + +???question "Why not using `unittest`, the Python built-in testing library?" + Although it is an additional dependency, `pytest` is often preferred + because it leads to cleaner and easier to read tests, while unittest + can lead to a lot of boilerplate code. + +## Testing + +Testing consist in writing small functions that test: + +- whether small units of your code function as expected +- whether these small units integrate well together +- whether your code takes care of edge cases +- whether your code's inputs and outputs are correctly treated + +Running your tests allow checking that new changes to the code base did +not break anything in your code (at least what your are testing for!). + +!!!tip "There are side-effects to writing tests!" + As you write tests, you get to experience what it takes to use your + library and this might lead you to ***refactor*** parts of your code; + refactoring is an important part of a software life-cycle! diff --git a/docs/tutorial/testing/using_pytest.md b/docs/tutorial/testing/using_pytest.md new file mode 100644 index 0000000..ffa8ac7 --- /dev/null +++ b/docs/tutorial/testing/using_pytest.md @@ -0,0 +1,103 @@ +# Using pytest + +`pytest` is a package that facilitates testing. + +### Creating the tests folder + +The first thing to do is to create a test folder in which all our tests will +live. + +```text title="File Structure" +src/ +└── pydev_tutorial/ +└── pydev_tutorial/ + └── swallow.py +tests/ +├── __init__.py +└── test_swallow.py +``` + +### Adding the test dependency group + +We need to tell the world that our package uses pytest, this will help IDE and +continuous integration pipeline to run. + +In your `pyproject.toml`, add the following lines: + +```toml title="pyproject.toml" +[project.optional-dependencies] +# add dependencies used for testing here +test = ["pytest"] +``` + +This adds an **optional dependency group**, called `test`, which depends on +`pytest` and `pytest-cov`. We will discuss the later in the coverage chapter. + +Let's also add some `pytest` options: + +```toml title="pyproject.toml" +[tool.pytest.ini_options] +minversion = "6.0" +testpaths = ["tests"] +filterwarnings = ["error"] +``` + +The most important here is to tell `pytest` where to find the tests themselves. + +### Install dependencies using the `test` group + +Rather than installing `pytest` directly, we can use the fact that it is +now declared as a dependency group in our `pyproject.toml`: + +```shell +pip install -e ".[test]" +``` + +This should install your package in editable mode, which is necessary to run +the tests later, but also install all `test` dependencies. + +### Our first test + +In the `test_swallow.py` file, add the following test: + +```python +def test_my_module(): + pass +``` + +Yes, a minimal test is that simple... It is a function that starts with +**test_** and does something. Obviously `pass` is not very interesting here. +But let's run it for the fun of it. + +=== "Running tests with VSCode" + + 1. Click on the flask ("Testing") on the left menu. + 2. Then click on "Configure Python Tests". + 3. A menu will open, select "pytest" and then the "tests" folder. + 4. Your tests should appear on the left area. Click on the green arrow to + run a single test or all of them. + + !!!tip "VSCode can be silly and ignore your tests" + In this case, run it via the console first! + +=== "Running tests via the console" + + 1. In your project folder run: +
+ ```console + $ pytest + ======================================================== test session starts + ======================================================== + platform darwin -- Python 3.10.10, pytest-7.3.0, pluggy-1.0.0 + rootdir: /Users/python.developer/git/pydev/pydev-tutorial + configfile: pyproject.toml + testpaths: tests + plugins: cov-4.0.0 + collected 1 item + + tests/test_swallow.py ................ [100%] + + ========================================================== 1 passed in 001s + ========================================================== + ``` +
diff --git a/docs/tutorial/writing_code/code_documentation.md b/docs/tutorial/writing_code/code_documentation.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/tutorial/writing_code/pep8.md b/docs/tutorial/writing_code/pep8.md new file mode 100644 index 0000000..a50d34e --- /dev/null +++ b/docs/tutorial/writing_code/pep8.md @@ -0,0 +1 @@ +# PEP8 diff --git a/docs/tutorial/project_metadata.md b/docs/tutorial/writing_code/project_metadata.md similarity index 100% rename from docs/tutorial/project_metadata.md rename to docs/tutorial/writing_code/project_metadata.md diff --git a/docs/tutorial/writing_code/typing.md b/docs/tutorial/writing_code/typing.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/tutorial/writing_code/writing_code.md b/docs/tutorial/writing_code/writing_code.md new file mode 100644 index 0000000..c96055b --- /dev/null +++ b/docs/tutorial/writing_code/writing_code.md @@ -0,0 +1,3 @@ +# Writing code + +## PEP8 summary diff --git a/mkdocs.yml b/mkdocs.yml index b4ccb4d..7f7508c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,11 +8,41 @@ nav: - Quickstart: quickstart.md - Tutorial: - tutorial/index.md + - tutorial/summary.md - tutorial/bare_minimum.md - - tutorial/project_metadata.md - - tutorial/code_quality.md - - tutorial/github.md - - tutorial/publishing.md + - Pre-requisite: + - tutorial/pre_requisite/virtual_environment.md + - tutorial/pre_requisite/version_control.md + - tutorial/pre_requisite/ide.md + - Writing code: + - tutorial/writing_code/writing_code.md + - tutorial/writing_code/pep8.md + - tutorial/writing_code/code_documentation.md + - tutorial/writing_code/typing.md + - tutorial/writing_code/project_metadata.md + - Testing: + - tutorial/testing/testing.md + - tutorial/testing/code_to_be_tested.md + - tutorial/testing/using_pytest.md + - tutorial/testing/real_tests.md + - tutorial/testing/test_with_parameters.md + - tutorial/testing/test_for_errors.md + - tutorial/testing/test_fixtures.md + - tutorial/testing/test_coverage.md + - tutorial/testing/test_conclusion.md + - Code quality: + - tutorial/code_quality/code_quality.md + - tutorial/code_quality/pre_commit_hooks.md + - tutorial/code_quality/linter.md + - tutorial/code_quality/formatter.md + - tutorial/code_quality/type_checking.md + - Github: + - tutorial/github/github.md + - tutorial/github/continuous_integration.md + - tutorial/github/github_apps.md + - tutorial/github/deployment.md + - tutorial/github/publishing.md + - tutorial/github/github_pages.md - Guides: - guides/index.md - pyproject.toml: guides/pyproject.md @@ -63,7 +93,7 @@ theme: - content.code.annotate - content.action.edit # - navigation.footer # next/previous links in footer - - navigation.content_next # next/previous links bottom of content + - navigation.content_next # next/previous links bottom of content # - navigation.instant # hard to use with javascript on page load # - navigation.tracking - navigation.indexes @@ -76,6 +106,7 @@ markdown_extensions: - admonition - attr_list - def_list + - footnotes - pymdownx.details - pymdownx.inlinehilite - pymdownx.keys