diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3a626c3a..fa963381 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,9 @@ updates: directory: / schedule: interval: monthly + cooldown: + default-days: 10 + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c1911de..95a72cf2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,9 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-python@v6 - - uses: actions/cache@v4 + - uses: actions/cache@v5 with: path: ~/.cache/pre-commit key: pre-commit|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} @@ -21,7 +21,7 @@ jobs: name: Build docs and check links runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: pandoc/actions/setup@v1 - uses: actions/setup-python@v6 with: diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index f123301a..dd542e36 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install packages run: sudo apt install plantuml - name: Setup pandoc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b38b10d7..7c57fdaa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,27 +15,27 @@ repos: - id: check-added-large-files args: ['--maxkb=1024'] - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.7.0 + rev: v2.16.2 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.24.1 + rev: v0.25 hooks: - id: validate-pyproject - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v1.0.0 + rev: v1.0.2 hooks: - id: sphinx-lint types: [rst] - repo: https://github.com/pycqa/isort - rev: 7.0.0 + rev: 8.0.1 hooks: - id: isort additional_dependencies: ["toml"] entry: isort --profile=black name: isort (python) - - repo: https://github.com/psf/black - rev: 25.9.0 + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 26.1.0 hooks: - id: black - repo: https://github.com/adamchainz/blacken-docs diff --git a/.python-version b/.python-version index e4fba218..24ee5b1b 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.12 +3.13 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index d1a42e4a..4f589eab 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,12 +7,12 @@ version: 2 # Set the version of Python and other tools you might need build: - os: ubuntu-22.04 + os: ubuntu-24.04 apt_packages: # plantuml is required for sphinxcontrib.plantuml - plantuml tools: - python: "3.12" + python: "3.13" jobs: install: - python -m pip install --upgrade pip diff --git a/docs/appendix/checks.rst b/docs/appendix/checks.rst index 4e76c007..adcaec3f 100644 --- a/docs/appendix/checks.rst +++ b/docs/appendix/checks.rst @@ -272,12 +272,12 @@ Checks .. code-block:: pycon - >>> sheet = {} - >>> sheet[("A", 0)] = 1 - >>> sheet[("A", 1)] = 2 - >>> sheet[("B", 0)] = 3 - >>> sheet[("B", 1)] = 4 - >>> print(sheet[("A", 1)]) + >>> tabular = {} + >>> tabular[("A", 0)] = 1 + >>> tabular[("A", 1)] = 2 + >>> tabular[("B", 0)] = 3 + >>> tabular[("B", 1)] = 4 + >>> print(tabular[("A", 1)]) 2 * How can you remove all duplicates from a list without changing the order of the @@ -501,7 +501,7 @@ Checks >>> pos [0, 1, 2, 3] -* How would you count the total number of negative numbers in the list ``[-[1, +* How would you count the total number of negative numbers in the list ``[[-1, 0, 1], [-1, 1, 3], [-2, 0, 2]]``? .. code-block:: pycon @@ -621,15 +621,11 @@ Checks .. code-block:: pycon - >> def my_func(*params): - ... for i in reversed(params): - ... print(i) - ... - >>> my_func(1, 2, 3, 4) - 4 - 3 - 2 - 1 + >>> values = input("Values separated by commas: ") + Values separated by commas: 1,3,2,4 + >>> value_list = values.split(",") + >>> reverse(value_list) + ['4', '3', '2', '1'] :doc:`/functions/variables` --------------------------- diff --git a/docs/conf.py b/docs/conf.py index 10889dfe..76691d70 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,7 +58,7 @@ plantuml_output_format = "svg" intersphinx_mapping = { - "python3": ("https://docs.python.org/3/", None), + "python3": ("https://docs.python.org/3.15/", None), "python3.14": ("https://docs.python.org/3.14/", None), "jupyter-tutorial": ("https://jupyter-tutorial.readthedocs.io/en/latest/", None), "Python4DataScience": ("https://www.python4data.science/en/latest/", None), diff --git a/docs/control-flow/boolean.rst b/docs/control-flow/boolean.rst index ecdd261c..e22b9233 100644 --- a/docs/control-flow/boolean.rst +++ b/docs/control-flow/boolean.rst @@ -18,19 +18,49 @@ considered ``True``. >>> x == y True - However, you should never compare calculated floating point numbers with - each other: + .. warning:: + However, you should never directly compare calculated floating point + numbers: - .. code-block:: pycon + .. code-block:: pycon - >>> u = 0.6 * 7 - >>> v = 0.7 * 6 - >>> u == v - False - >>> u - 4.2 - >>> v - 4.199999999999999 + >>> u = 0.6 * 7 + >>> v = 0.7 * 6 + >>> u == v + False + >>> u + 4.2 + >>> v + 4.199999999999999 + + Instead, you can use :func:`math.isclose`: + + .. code-block:: pycon + + >>> import math + >>> math.isclose(u, v) + True + + Alternatively, you can also use :func:`round`: + + .. code-block:: pycon + + >>> round(u, 2) == round(v, 2) + True + + .. warning:: + Integers smaller than -5 or larger than 256 are recreated each time, but + integers in between are pre-instantiated by the interpreter and reused. + Therefore, :py:func:`id` should not be used for comparisons of integers: + + .. code-block:: pycon + + >>> x = 256 + >>> y = 257 + >>> id(x) == id(256) + True + >>> id(y) == id(257) + False ``is``, ``is not``, ``in``, ``not in`` checks the identity: diff --git a/docs/control-flow/loops.rst b/docs/control-flow/loops.rst index eeb79538..ed5af094 100644 --- a/docs/control-flow/loops.rst +++ b/docs/control-flow/loops.rst @@ -85,8 +85,8 @@ flow control is continued with ``x`` being set to the next entry in the list. After the first matching integer is found, the loop is terminated with the ``break`` statement. -Loops with an index -------------------- +``for`` Loops with an index +~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can also output the index in a ``for`` loop, for example with :py:func:`enumerate()`: @@ -94,15 +94,15 @@ You can also output the index in a ``for`` loop, for example with .. code-block:: pycon >>> data_types = ["Data types", "Numbers", "Lists"] - >>> for index, title in enumerate(data_types): - ... print(index, title) + >>> for key, title in enumerate(data_types): + ... print(key, title) ... 0 Data types 1 Numbers 2 Lists List Comprehensions -------------------- +~~~~~~~~~~~~~~~~~~~ A list is usually generated as follows: @@ -153,6 +153,57 @@ appended to the end of the expression: >>> squares [16, 25, 36, 49] +Dict Comprehensions +~~~~~~~~~~~~~~~~~~~ + +:doc:`../types/sequences-sets/lists` can be converted into :doc:`../types/dicts` +as follows: + +.. code-block:: pycon + + >>> toc = {} + >>> for key, title in enumerate(data_types): + ... toc[key] = title + ... + >>> toc + {0: 'Data types', 1: 'Numbers', 2: 'Lists'} + +Mit Dict Comprehensions vereinfacht sich dies: + +.. code-block:: pycon + + >>> toc = {key: value for key, value in enumerate(data_types)} + >>> toc + {0: 'Data types', 1: 'Numbers', 2: 'Lists'} + +Das allgemeine Format für Dict Comprehensions ist: + +:samp:`{NEW_DICT} = \{{KEY}: {VALUE} for {MEMBER} in {ITERABLE}\}` + +Change a ``Collection`` +~~~~~~~~~~~~~~~~~~~~~~~ + +Modifying a ``collection`` while iterating over it can be difficult. Therefore, +a copy of the ``collection`` is often modified instead: + +.. code-block:: pycon + + >>> for key, title in data_types.items(): + ... if key == 0: + ... del data_types[key] + ... + Traceback (most recent call last): + File "", line 1, in + for key, title in data_types.items(): + ~~~~~~~~~~~~~~~~^^ + RuntimeError: dictionary changed size during iteration + >>> for key, title in data_types.copy().items(): + ... if key == 0: + ... del data_types[key] + ... + >>> data_types + {1: 'Numbers', 2: 'Lists'} + Checks ------ @@ -160,7 +211,7 @@ Checks * Which list comprehension would you use to achieve the same result? -* How would you count the total number of negative numbers in the list ``[-[1, +* How would you count the total number of negative numbers in the list ``[[-1, 0, 1], [-1, 1, 3], [-2, 0, 2]]``? * Creates a generator that only returns odd numbers from 1 to 10. diff --git a/docs/document/index.rst b/docs/document/index.rst index a6755de0..6bc52ed2 100644 --- a/docs/document/index.rst +++ b/docs/document/index.rst @@ -7,20 +7,22 @@ describes how your software can be installed, operated, used and improved: * Those who want to use your package need information, * what problems your software solves and what the main features and - limitations of the software are (``README``) + limitations of the software are (→ :ref:`readme`) * how the software can be used as an example - * what changes have come in more recent software versions (``CHANGELOG``) + * what changes have come in more recent software versions (→ :ref:`changelog`) * Those who want to run the software need an installation guide for your software and the required dependencies. * Those who want to improve the software need information about - * how to help improve the product with bug fixes (``CONTRIBUTING``) - * how to communicate with others (``CODE_OF_CONDUCT``) + * how to help improve the product with bug fixes (→ :ref:`contributing`) + * how to report security vulnerabilities (→ :ref:`security`) + * how to communicate with others (→ :ref:`coc`) -All together need information on how the product is licensed (``LICENSE`` file -or ``LICENSES`` folder) and how to get help if needed. +All together need information on how the product is licensed (:file:`LICENSE` +file or :file:`LICENSES` folder, → :ref:`license`) and how to get help if +needed. .. tip:: cusy seminars: diff --git a/docs/explore.rst b/docs/explore.rst index 24b6d8c0..e4bce384 100644 --- a/docs/explore.rst +++ b/docs/explore.rst @@ -4,28 +4,41 @@ Exploring Python Whether you use :ref:`idle` or the :ref:`interactive_shell`, there are some useful functions to explore Python. +.. code-block:: pycon + + >>> x = 4.2 + +``type()`` +---------- + +With :py:func:`type`, you can display the object type, for example: + +.. code-block:: pycon + + >>> type(x) + + .. _help: ``help()`` ---------- -``help()`` has two different modes. When you type ``help()``, you call the help -system, which you can use to get information about modules, keywords, and other -topics. When you are in the help system, you will see a prompt with ``help>``. -You can now enter a module name, for example ``float``, to search the `Python -documentation `_ for that type. +:py:func:`help` has two different modes. When you type :func:`help`, you call +the help system, which you can use to get information about modules, keywords, +and other topics. When you are in the help system, you will see a prompt with +``help>``. You can now enter a module name, for example ``float``, to search the +`Python documentation `_ for that type. -``help()`` is part of the :doc:`pydoc ` library, which +:func:`help` is part of the :doc:`pydoc ` library, which provides access to the documentation built into Python libraries. Since every Python installation comes with full documentation, you have all the documentation at your fingertips even offline. -Alternatively, you can use ``help()`` more specifically by passing a type or +Alternatively, you can use :func:`help` more specifically by passing a type or variable name as a parameter, for example: .. code-block:: pycon - >>> x = 4.2 >>> help(x) Help on float object: @@ -39,14 +52,27 @@ variable name as a parameter, for example: | __abs__(self, /) | abs(self) ... + | is_integer(self, /) + | Return True if the float is an integer. + ... For example, you will learn that ``x`` is of type ``float`` and has a function -:func:`__add__` that you can use with dot notation: + :func:`is_integer` that you can use with dot notation: + +.. code-block:: pycon + + >>> x.is_integer() + False + +``id()`` +-------- + +:py:func:`id` specifies the identification number of an object, for example: .. code-block:: pycon - >>> x.__add__(1) - 5.2 + >>> id(x) + 4304262800 ``dir()`` --------- diff --git a/docs/functions/decorators.rst b/docs/functions/decorators.rst index 5814bcd8..1a3c180c 100644 --- a/docs/functions/decorators.rst +++ b/docs/functions/decorators.rst @@ -78,21 +78,25 @@ as decorators, such as: .. code-block:: pycon :linenos: - >>> from functools import cache - >>> @cache + >>> import timeit + ... from functools import cache + ... @cache ... def factorial(n): ... return n * factorial(n - 1) if n else 1 - ... - >>> factorial(8) - 40320 - >>> factorial(10) - 3628800 - - Line 6 - Since there is no previously stored result, nine recursive calls are - made. - Line 8 - makes only two new calls, as the other results come from the cache. + ... timeit.timeit("factorial(8)", globals=globals()) + 0.02631620899774134 + + Line 1 + imports the :mod:`timeit` module for measuring execution time. + Line 2 + imports :func:`functools.cache`. + Line 5 + The ``@cache`` decorator is used to store intermediate results, which + can then be reused. In our case, the execution speed is increased + approximately tenfold. + Line 10 + :func:`timeit.timeit` measures the time of a call. Unless otherwise + specified, the call is made one million times. :func:`functools.wraps` This decorator makes the wrapper function look like the original function diff --git a/docs/functions/params.rst b/docs/functions/params.rst index 2b36f420..b0d4546a 100644 --- a/docs/functions/params.rst +++ b/docs/functions/params.rst @@ -24,8 +24,8 @@ Python offers flexible mechanisms for passing :term:`arguments ` to ... >>> func2(5, w=6) 45 - >>> def func3(u, v=1, w=1, *tup): - ... print((u, v, w) + tup) + >>> def func3(u, v=1, w=1, *args): + ... print((u, v, w) + args) ... >>> func3(7) (7, 1, 1) diff --git a/docs/packs/dataprep/pyproject.toml b/docs/packs/dataprep/pyproject.toml index 82a5f521..1a78ec2b 100644 --- a/docs/packs/dataprep/pyproject.toml +++ b/docs/packs/dataprep/pyproject.toml @@ -1,45 +1,48 @@ [build-system] -requires = ["Cython", "setuptools>=77.0"] build-backend = "setuptools.build_meta" +requires = [ "cython", "setuptools>=77" ] [project] name = "dataprep" version = "0.1.0" -authors = [ - { name="Veit Schiele", email="veit@cusy.io" }, -] description = "A small dataprep package" readme = "README.rst" -License-Expression = "BSD-3-Clause" -License-File = [ "LICENSE" ] +authors = [ + { name = "Veit Schiele", email = "veit@cusy.io" }, +] requires-python = ">=3.9" classifiers = [ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: BSD License", - "Operating System :: OS Independent", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dependencies = [ - "Cython", - "pandas", + "cython", + "pandas", ] +urls."Bug Tracker" = "https://github.com/veit/dataprep/issues" +urls."Homepage" = "https://github.com/veit/dataprep" +License-Expression = "BSD-3-Clause" +License-File = [ "LICENSE" ] [dependency-groups] -tests = [ - "coverage[toml]", - "pytest>=6.0", +dev = [ + "pre-commit", + { include-group = "docs" }, + { include-group = "tests" }, ] docs = [ - "furo", - "sphinxext-opengraph", - "sphinx-copybutton", - "sphinx_inline_tabs" + "furo", + "sphinx-copybutton", + "sphinx-inline-tabs", + "sphinxext-opengraph", ] -dev = [ - {include-group = "tests"}, - {include-group = "docs"}, - "pre-commit", +tests = [ + "coverage[toml]", + "pytest>=6", ] - -[project.urls] -"Homepage" = "https://github.com/veit/dataprep" -"Bug Tracker" = "https://github.com/veit/dataprep/issues" diff --git a/docs/packs/distribution.rst b/docs/packs/distribution.rst index 08163201..fbdc8acd 100644 --- a/docs/packs/distribution.rst +++ b/docs/packs/distribution.rst @@ -234,11 +234,12 @@ as: ``readme`` is a path to a file containing a detailed description of the package. This is displayed on the package details page on :term:`Python Package Index` - (:term:`PyPI`). In this case, the description is loaded from ``README.rst``. + (:term:`PyPI`). In this case, the description is loaded from + :file:`README.rst`. .. _license-expression: -``License-Expression`` +``license-expression`` contains valid `SPDX license expressions `_. @@ -309,8 +310,8 @@ Dependency groups .. literalinclude:: dataprep/pyproject.toml :language: toml - :lines: 26-36 - :lineno-start: 26 + :lines: 34, 40-45 + :lineno-start: 34 Recursive dependency groups are also possible. For example, for ``dev`` you can take over all dependencies from ``docs`` and ``test`` in addition to @@ -318,8 +319,8 @@ take over all dependencies from ``docs`` and ``test`` in addition to .. literalinclude:: dataprep/pyproject.toml :language: toml - :lines: 37-41 - :lineno-start: 37 + :lines: 35-39 + :lineno-start: 35 You can install these dependency groups, for example with: @@ -397,11 +398,220 @@ directory. Other files ----------- -:file:`CONTRIBUTORS.rst` -~~~~~~~~~~~~~~~~~~~~~~~~ +.. _changelog: + +:file:`CHANGELOG` +~~~~~~~~~~~~~~~~~ + +All significant changes to a project should be documented in the +:file:`CHANGELOG`. `Keep a Changelog +`_ recommends the following format for +this: + +.. code-block:: rest + + [Unreleased] + ============ + + Added + ----- + + … + + Changed + ------- + + … + + Removed + ------- + + … + + [x.y.z] - YYYY-MM-DD + ==================== + + Added + ----- + + … .. seealso:: - * `All contributors `_ + * `Keep a Changelog `__ + +There are also several Python libraries that can help you create the +:file:`CHANGELOG` file: + +`Release Drafter `_ + creates drafts for your next release notes as soon as pull requests are + merged into the main branch. Release Drafter was developed with `Probot + `_, a framework for creating GitHub apps + to automate and improve your workflows. + + .. image:: https://raster.shields.io/github/stars/release-drafter/release-drafter + :alt: Stars + :target: https://github.com/release-drafter/release-drafter + + .. image:: https://raster.shields.io/github/contributors/release-drafter/release-drafter + :alt: Contributors + :target: https://github.com/release-drafter/release-drafter/graphs/contributors + + .. image:: https://raster.shields.io/github/commit-activity/y/release-drafter/release-drafter + :alt: Commit activity + :target: https://github.com/release-drafter/release-drafter/graphs/commit-activity + + .. image:: https://raster.shields.io/github/license/release-drafter/release-drafter + :alt: Licence + +`towncrier `_ + is a utility for creating useful, summarised news files for your project. + + .. image:: https://raster.shields.io/github/stars/twisted/towncrier + :alt: Stars + :target: https://github.com/twisted/towncrier + + .. image:: https://raster.shields.io/github/contributors/twisted/towncrier + :alt: Contributors + :target: https://github.com/twisted/towncrier/graphs/contributors + + .. image:: https://raster.shields.io/github/commit-activity/y/twisted/towncrier + :alt: Commit activity + :target: https://github.com/twisted/towncrier/graphs/commit-activity + + .. image:: https://raster.shields.io/github/license/twisted/towncrier + :alt: Licence + +`Scriv `_ + is a command-line tool that helps developers keep useful change logs. It + manages a directory of change log fragments, which are summarised into + entries in a :file:`CHANGELOG.rst` file. + + .. image:: https://raster.shields.io/github/stars/nedbat/scriv + :alt: Stars + :target: https://github.com/nedbat/scriv + + .. image:: https://raster.shields.io/github/contributors/nedbat/scriv + :alt: Contributors + :target: https://github.com/nedbat/scriv/graphs/contributors + + .. image:: https://raster.shields.io/github/commit-activity/y/nedbat/scriv + :alt: Commit activity + :target: https://github.com/nedbat/scriv/graphs/commit-activity + + .. image:: https://raster.shields.io/github/license/nedbat/scriv + :alt: Licence + +`Dinghy `_ + uses the GitHub GraphQL API to find current activity on releases, issues, + and pull requests, and creates a compact HTML overview from this + information. + + .. image:: https://raster.shields.io/github/stars/nedbat/dinghy + :alt: Stars + :target: https://github.com/nedbat/dinghy + + .. image:: https://raster.shields.io/github/contributors/nedbat/dinghy + :alt: Contributors + :target: https://github.com/nedbat/dinghy/graphs/contributors + + .. image:: https://raster.shields.io/github/commit-activity/y/nedbat/dinghy + :alt: Commit activity + :target: https://github.com/nedbat/dinghy/graphs/commit-activity + + .. image:: https://raster.shields.io/github/license/nedbat/dinghy + :alt: Licence + +`github-activity `_ + generates Markdown change logs for GitHub repositories, offering more + control over the types of contributions and metadata used to create the + change logs. + + .. image:: https://raster.shields.io/github/stars/executablebooks/github-activity + :alt: Stars + :target: https://github.com/executablebooks/github-activity + + .. image:: https://raster.shields.io/github/contributors/executablebooks/github-activity + :alt: Contributors + :target: https://github.com/executablebooks/github-activity/graphs/contributors + + .. image:: https://raster.shields.io/github/commit-activity/y/executablebooks/github-activity + :alt: Commit activity + :target: https://github.com/executablebooks/github-activity/graphs/commit-activity + + .. image:: https://raster.shields.io/github/license/executablebooks/github-activity + :alt: Licence + +`changelog_manager `_ + helps you create a :file:`CHANGELOG.md` file for your Git repo that complies + with the `Keep A Changelog `_ + standard. + + .. image:: https://raster.shields.io/github/stars/masukomi/changelog_manager + :alt: Stars + :target: https://github.com/masukomi/changelog_manager + + .. image:: https://raster.shields.io/github/contributors/masukomi/changelog_manager + :alt: Contributors + :target: https://github.com/masukomi/changelog_manager/graphs/contributors + + .. image:: https://raster.shields.io/github/commit-activity/y/masukomi/changelog_manager + :alt: Commit activity + :target: https://github.com/masukomi/changelog_manager/graphs/commit-activity + + .. image:: https://raster.shields.io/github/license/masukomi/changelog_manager + :alt: Licence + +`blurb `_ + is a tool for freeing CPython development from the tedious conflicts in + `cpython/Misc/NEWS.d/ + `_. + + .. image:: https://raster.shields.io/github/stars/python/blurb + :alt: Stars + :target: https://github.com/python/blurb + + .. image:: https://raster.shields.io/github/contributors/python/blurb + :alt: Contributors + :target: https://github.com/python/blurb/graphs/contributors + + .. image:: https://raster.shields.io/github/commit-activity/y/python/blurb + :alt: Commit activity + :target: https://github.com/python/blurb/graphs/commit-activity + + .. image:: https://raster.shields.io/github/license/python/blurb + :alt: Licence + +.. _coc: + +:file:`CODE_OF_CONDUCT` +~~~~~~~~~~~~~~~~~~~~~~~ + +If you want to provide guidance on how questions can be asked or how others can +contribute to the project, a :file:`CODE_OF_CONDUCT` file can be helpful. This +allows you to specify what kind of interaction you expect. It also sets rules to +protect you and others from unwanted behaviour. + +.. seealso:: + * `SciPy Code of Conduct + `_ + +.. _contributing: + +:file:`CONTRIBUTING` +~~~~~~~~~~~~~~~~~~~~ + +.. seealso:: + * `Python Developer’s Guide `_ + * `Contributing to pandas + `_ + +:file:`CONTRIBUTORS` +~~~~~~~~~~~~~~~~~~~~ + +.. seealso:: + * `All contributors `_ + +.. _license: :file:`LICENSE` ~~~~~~~~~~~~~~~ @@ -409,39 +619,46 @@ Other files You can find detailed information on this in the :doc:`Python4DataScience:productive/licensing` section. -:file:`README.rst` -~~~~~~~~~~~~~~~~~~ +.. _readme: + +:file:`README` +~~~~~~~~~~~~~~ This file briefly tells those who are interested in the package how to use it. .. seealso:: - * `Make a README `_ - * `readme.so `_ + * `Make a README `_ + * `readme.so `_ If you write the document in :doc:`/document/sphinx/rest`, you can also include the contents as a detailed description in your package: .. literalinclude:: dataprep/pyproject.toml :language: toml + :emphasize-lines: 5 :lineno-start: 5 - :lines: 5, 12 + :lines: 5-12 You can also include them in your :doc:`Sphinx documentation ` with ``.. include:: ../../README.rst``. -:file:`CHANGELOG.rst` -~~~~~~~~~~~~~~~~~~~~~ +.. _security: + +:file:`SECURITY` +~~~~~~~~~~~~~~~~ + +This file should contain information on + +* how to report a security vulnerability without it becoming publicly visible, +* the process and schedule for disclosing the vulnerability, +* links, such as URLs and emails, where support can be requested. .. seealso:: - * `Keep a Changelog `_ - * `Scriv `_ - * `changelog_manager `_ - * `github-activity `_ - * `Dinghy `_ - * `Python core-workflow blurb - `_ - * `Release Drafter `_ - * `towncrier `_ + * GitHub DocsDokumentation: `Adding a security policy to your repository + `_ + * `github.com/veit/items/security + `_ + Historical files or files needed for binary extensions ------------------------------------------------------ @@ -556,7 +773,7 @@ structure for packages. .. literalinclude:: mypack/pyproject.toml :caption: mypack/pyproject.toml - :emphasize-lines: 12-13 + :emphasize-lines: 21 :file:`mypack/src/mypack/__init__.py` The module defines a CLI function :func:`main`: @@ -685,7 +902,7 @@ You can then call ``mypack`` with ``uv run``: .. seealso:: * `Troubleshooting build failures - `_ + `_ .. note:: There are still many instructions that include a step to call diff --git a/docs/packs/myapp/pyproject.toml b/docs/packs/myapp/pyproject.toml index b17df705..681b307b 100644 --- a/docs/packs/myapp/pyproject.toml +++ b/docs/packs/myapp/pyproject.toml @@ -1,17 +1,20 @@ +[build-system] +build-backend = "hatchling.build" +requires = [ "hatchling" ] + [project] name = "myapp" version = "0.1.0" description = "Add your description here" readme = "README.md" authors = [ - { name = "Veit Schiele", email = "veit@cusy.io" } + { name = "Veit Schiele", email = "veit@cusy.io" }, ] requires-python = ">=3.13" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] dependencies = [] - -[project.scripts] -myapp = "myapp:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +scripts.myapp = "myapp:main" diff --git a/docs/packs/mypack/pyproject.toml b/docs/packs/mypack/pyproject.toml index bcac75a1..eeb00be2 100644 --- a/docs/packs/mypack/pyproject.toml +++ b/docs/packs/mypack/pyproject.toml @@ -1,17 +1,20 @@ +[build-system] +build-backend = "hatchling.build" +requires = [ "hatchling" ] + [project] name = "mypack" version = "0.1.0" description = "Add your description here" readme = "README.md" authors = [ - { name = "Veit Schiele", email = "veit@cusy.io" } + { name = "Veit Schiele", email = "veit@cusy.io" }, ] requires-python = ">=3.13" +classifiers = [ + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] dependencies = [] - -[project.scripts] -mypack = "mypack:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +scripts.mypack = "mypack:main" diff --git a/docs/packs/pyproject.toml b/docs/packs/pyproject.toml index 0f10bf80..0fc77403 100644 --- a/docs/packs/pyproject.toml +++ b/docs/packs/pyproject.toml @@ -2,8 +2,6 @@ test-requires = "pytest" test-command = "pytest {project}/tests" build-verbosity = 1 - # support Universal2 for Apple Silicon: -[tool.cibuildwheel.macos] -archs = ["auto", "universal2"] -test-skip = ["*universal2:arm64"] +macos.archs = [ "auto", "universal2" ] +macos.test-skip = [ "*universal2:arm64" ] diff --git a/docs/packs/templating/advanced.rst b/docs/packs/templating/advanced.rst index 7fd8d51c..17c42de1 100644 --- a/docs/packs/templating/advanced.rst +++ b/docs/packs/templating/advanced.rst @@ -19,7 +19,6 @@ Variables, for example, can be validated in a pre-generate hook: import re import sys - MODULE_REGEX = r"^[_a-zA-Z][_a-zA-Z0-9]+$" module_name = "{{ cookiecutter.module_name }}" diff --git a/docs/save-data/files-directories.rst b/docs/save-data/files-directories.rst index 6d42d7e7..503e785f 100644 --- a/docs/save-data/files-directories.rst +++ b/docs/save-data/files-directories.rst @@ -46,7 +46,7 @@ Line 2: .. code-block:: pycon :lineno-start: 2 - >>> p = Path("c:", "Users", "Veit", "My Documents", "myfile.txt") + >>> p = Path("C:/", "Users", "Veit", "My Documents", "myfile.txt") >>> with p.open() as f: ... f.readline() ... diff --git a/docs/save-data/sqlite/create-data.rst b/docs/save-data/sqlite/create-data.rst index 601504fd..9bdbe045 100644 --- a/docs/save-data/sqlite/create-data.rst +++ b/docs/save-data/sqlite/create-data.rst @@ -5,20 +5,20 @@ Create data .. literalinclude:: create_data.py :language: python - :lines: 7-10 + :lines: 7-9 :lineno-start: 7 #. Save data to database: .. literalinclude:: create_data.py :language: python - :lines: 14 - :lineno-start: 14 + :lines: 12 + :lineno-start: 12 #. Insert multiple records using the more secure ``?`` method where the number of ``?`` should correspond to the number of columns: .. literalinclude:: create_data.py :language: python - :lines: 17- - :lineno-start: 17 + :lines: 15- + :lineno-start: 15 diff --git a/docs/save-data/sqlite/create_data.py b/docs/save-data/sqlite/create_data.py index 9167e204..8469d078 100644 --- a/docs/save-data/sqlite/create_data.py +++ b/docs/save-data/sqlite/create_data.py @@ -4,11 +4,9 @@ cursor = conn.cursor() # insert a record into the database -cursor.execute( - """INSERT INTO books +cursor.execute("""INSERT INTO books VALUES ('Python basics', 'en', 'Veit Schiele', 'BSD', - '2021-10-28')""" -) + '2021-10-28')""") # save data to database conn.commit() diff --git a/docs/save-data/sqlite/create_db.py b/docs/save-data/sqlite/create_db.py index 63536362..229470ff 100644 --- a/docs/save-data/sqlite/create_db.py +++ b/docs/save-data/sqlite/create_db.py @@ -6,9 +6,7 @@ cursor = conn.cursor() # create books table -cursor.execute( - """CREATE TABLE books +cursor.execute("""CREATE TABLE books (title text, language text, author text, license text, release_date text) - """ -) + """) diff --git a/docs/save-data/sqlite/normalise.py b/docs/save-data/sqlite/normalise.py index 72ea100e..dd08d156 100644 --- a/docs/save-data/sqlite/normalise.py +++ b/docs/save-data/sqlite/normalise.py @@ -3,24 +3,17 @@ conn = sqlite3.connect("library.db") cursor = conn.cursor() -cursor.execute( - """CREATE TABLE languages +cursor.execute("""CREATE TABLE languages (id INTEGER PRIMARY KEY AUTOINCREMENT, - language_code VARCHAR(2))""" -) + language_code VARCHAR(2))""") -cursor.execute( - """INSERT INTO languages (language_code) - VALUES ('de')""" -) +cursor.execute("""INSERT INTO languages (language_code) + VALUES ('de')""") -cursor.execute( - """INSERT INTO languages (language_code) - VALUES ('en')""" -) +cursor.execute("""INSERT INTO languages (language_code) + VALUES ('en')""") -cursor.execute( - """CREATE TABLE "temp" ( +cursor.execute("""CREATE TABLE "temp" ( "id" INTEGER, "title" TEXT, "language_code" INTEGER REFERENCES languages(id), @@ -29,25 +22,18 @@ "license" TEXT, "release_date" DATE, PRIMARY KEY("id" AUTOINCREMENT) - )""" -) + )""") -cursor.execute( - """INSERT INTO temp (title,language,author,license,release_date) - SELECT title,language,author,license,release_date FROM books""" -) +cursor.execute("""INSERT INTO temp (title,language,author,license,release_date) + SELECT title,language,author,license,release_date FROM books""") -cursor.execute( - """UPDATE temp +cursor.execute("""UPDATE temp SET language_code = 1 - WHERE language = 'de'""" -) + WHERE language = 'de'""") -cursor.execute( - """UPDATE temp +cursor.execute("""UPDATE temp SET language_code = 2 - WHERE language = 'en'""" -) + WHERE language = 'en'""") # Only SQLite ≥ 3.35.0 allows DROP COLUMN; # Only Python versions ≥ 3.8 released after 27 April 2021 will receive these or diff --git a/docs/save-data/sqlite/normalise.rst b/docs/save-data/sqlite/normalise.rst index 6cc32026..624a0f4d 100644 --- a/docs/save-data/sqlite/normalise.rst +++ b/docs/save-data/sqlite/normalise.rst @@ -16,15 +16,15 @@ published. .. literalinclude:: normalise.py :language: python - :lines: 6-9 + :lines: 6-8 :lineno-start: 6 #. Then we create the values ``de`` and ``en`` in this table: .. literalinclude:: normalise.py :language: python - :lines: 12-18 - :lineno-start: 12 + :lines: 10-14 + :lineno-start: 10 #. Since SQLite does not support ``MODIFY COLUMN``, we now create a temporary table ``temp`` with all columns from ``books`` and a column ``language_code`` @@ -32,30 +32,30 @@ published. .. literalinclude:: normalise.py :language: python - :lines: 22-32 - :lineno-start: 22 + :lines: 16-25 + :lineno-start: 16 #. Now we transfer the values from the ``books`` table to the ``temp`` table: .. literalinclude:: normalise.py :language: python - :lines: 35-37 - :lineno-start: 35 + :lines: 27-28 + :lineno-start: 27 #. Transfer the specification of the language in ``books`` as the ``id`` of the data records from the ``languages`` table to ``temp``. .. literalinclude:: normalise.py :language: python - :lines: 40-44 - :lineno-start: 40 + :lines: 30-36 + :lineno-start: 30 #. Now we can delete the ``languages`` column in the ``temp`` table: .. literalinclude:: normalise.py :language: python - :lines: 55 - :lineno-start: 55 + :lines: 41 + :lineno-start: 41 .. note:: ``DROP COLUMN`` can only be used from Python versions from 3.8 that were @@ -69,12 +69,12 @@ published. .. literalinclude:: normalise.py :language: python - :lines: 57 - :lineno-start: 57 + :lines: 43 + :lineno-start: 43 #. And finally, the ``temp`` table can be renamed ``books``: .. literalinclude:: normalise.py :language: python - :lines: 59 - :lineno-start: 59 + :lines: 45 + :lineno-start: 45 diff --git a/docs/save-data/sqlite/query-normalised.rst b/docs/save-data/sqlite/query-normalised.rst index 258c3dbb..b4aae3aa 100644 --- a/docs/save-data/sqlite/query-normalised.rst +++ b/docs/save-data/sqlite/query-normalised.rst @@ -5,7 +5,7 @@ Query normalised data .. literalinclude:: query_normalised.py :language: python - :lines: 7-13 + :lines: 7-11 :lineno-start: 7 .. code-block:: pycon @@ -27,8 +27,8 @@ Query normalised data .. literalinclude:: query_normalised.py :language: python - :lines: 16-24 - :lineno-start: 16 + :lines: 14-22 + :lineno-start: 14 .. code-block:: pycon diff --git a/docs/save-data/sqlite/query_normalised.py b/docs/save-data/sqlite/query_normalised.py index aa0cb0c6..c012fcf1 100644 --- a/docs/save-data/sqlite/query_normalised.py +++ b/docs/save-data/sqlite/query_normalised.py @@ -6,10 +6,8 @@ def select_all_records_ordered_by_language_number(cursor): print("All books ordered by language id and title:") - for row in cursor.execute( - """SELECT language_code, author, title FROM books - ORDER BY language_code,title""" - ): + for row in cursor.execute("""SELECT language_code, author, title FROM books + ORDER BY language_code,title"""): print(row) diff --git a/docs/save-data/sqlite/test_sqlite.py b/docs/save-data/sqlite/test_sqlite.py index 18392453..c8d70300 100644 --- a/docs/save-data/sqlite/test_sqlite.py +++ b/docs/save-data/sqlite/test_sqlite.py @@ -19,18 +19,14 @@ def setUp(self): self.conn = sqlite3.connect(":memory:") cursor = self.conn.cursor() - cursor.execute( - """CREATE TABLE books + cursor.execute("""CREATE TABLE books (title text, language text, author text, license text, release_date text) - """ - ) + """) - cursor.execute( - """INSERT INTO books + cursor.execute("""INSERT INTO books VALUES ('Python basics', 'en', 'Veit Schiele', 'BSD', - '2021-10-28')""" - ) + '2021-10-28')""") def test_func_like(self): self.conn = sqlite3.connect(":memory:") diff --git a/docs/test/pytest/command-line-options.rst b/docs/test/pytest/command-line-options.rst new file mode 100644 index 00000000..7175a653 --- /dev/null +++ b/docs/test/pytest/command-line-options.rst @@ -0,0 +1,212 @@ +Command line options +==================== + +In :ref:`dynamic-fixture-scope`, we have already seen how the fixture scope can +be changed using a command line option. Now let’s take a closer look at the +command line options. + +Passing different values to a test function +------------------------------------------- + +Suppose you want to write a test that depends on a command line option. You can +achieve this using the following pattern: + +.. code-block:: python + :caption: test_example.py + + def test_db(items_db, db_path, cmdopt): + if cmdopt == "json": + print("Save as JSON file") + elif cmdopt == "sqlite": + print("Save in a SQLite database") + assert items_db.path() == db_path + +For this to work, the command line option must be added and ``cmdopt`` must be +provided via a fixture function: + +.. code-block:: python + :caption: conftest.py + + import pytest + + + def pytest_addoption(parser): + parser.addoption( + "--cmdopt", + action="store", + default="json", + help="Store data as JSON file or in a SQLite database", + ) + + + @pytest.fixture + def cmdopt(request): + return request.config.getoption("--cmdopt") + +You can then call up your tests, for example, with: + +.. code-block:: console + + $ pytest --sqlite + +In addition, you can add a simple validation of the input by listing the +options: + +.. code-block:: python + :caption: conftest.py + :emphasize-lines: 7 + + def pytest_addoption(parser): + parser.addoption( + "--cmdopt", + action="store", + default="json", + help="Store data as JSON file or in a SQLite database", + choices=("json", "sqlite"), + ) + +This is how we receive feedback on an incorrect argument: + +.. code-block:: console + + $ pytest --postgresql + ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...] + pytest: error: argument --cmdopt: invalid choice: 'postgresql' (choose from json, sqlite) + +If you want to provide more detailed error messages, you can use the ``type`` +parameter and raise ``pytest.UsageError``: + +.. code-block:: python + :caption: conftest.py + :emphasize-lines: -6, 15 + + def type_checker(value): + msg = "cmdopt must specify json or sqlite" + if not value.startswith("json" or "sqlite"): + raise pytest.UsageError(msg) + + return value + + + def pytest_addoption(parser): + parser.addoption( + "--cmdopt", + action="store", + default="json", + help="Store data as JSON file or in a SQLite database", + type=type_checker, + ) + +However, command line options often need to be processed outside of the test and +more complex objects need to be passed. + +Adding command line options dynamically +--------------------------------------- + +With :ref:`addopts`, you can add static command line options to your project. +However, you can also change the command line arguments dynamically before they +are processed: + +.. code-block:: python + :caption: conftest.py + + import sys + + + def pytest_load_initial_conftests(args): + if "xdist" in sys.modules: + import multiprocessing + + num = max(multiprocessing.cpu_count() / 2, 1) + args[:] = ["-n", str(num)] + args + +If you have installed the :ref:`xdist-plugin` plugin, test runs will always be +performed with a number of subprocesses close to your CPU. + +Command line option for skipping tests +-------------------------------------- + +Below, we add a :file:`conftest.py` file with a command line option +``--runslow`` to control the skipping of tests marked with ``pytest.mark.slow``: + +.. code-block:: python + :caption: conftest.py + + import pytest + + + def pytest_addoption(parser): + parser.addoption( + "--runslow", action="store_true", default=False, help="run slow tests" + ) + + + def pytest_collection_modifyitems(config, items): + if config.getoption("--runslow"): + # If --runslow is specified on the CLI, slow tests are not skipped. + return + skip_slow = pytest.mark.skip(reason="need --runslow option to run") + for item in items: + if "slow" in item.keywords: + item.add_marker(skip_slow) + +If we now write a test with the ``@pytest.mark.slow`` decorator, a skipped +‘slow’ test will be displayed when pytest is called: + +.. code-block:: pytest + + $ uv run pytest + ============================= test session starts ============================== + ... + test_example.py s. [100%] + + =========================== short test summary info ============================ + SKIPPED [1] test_example.py:8: need --runslow option to run + ========================= 1 passed, 1 skipped in 0.05s ========================= + +Extend test report header +------------------------- + +Additional information can be easily provided in a ``pytest -v`` run: + +.. code-block:: python + :caption: conftest.py + + import sys + + + def pytest_report_header(config): + gil = sys._is_gil_enabled() + return f"Is GIL enabled? {gil}" + + +.. code-block:: pytest + :emphasize-lines: 5 + + $ uv run pytest -v + ============================= test session starts ============================== + platform darwin -- Python 3.14.0b4, pytest-8.4.1, pluggy-1.6.0 + cachedir: .pytest_cache + Is GIL enabled? False + rootdir: /Users/veit/sandbox/items + configfile: pyproject.toml + plugins: anyio-4.9.0, Faker-37.4.0, cov-6.2.1 + ... + ============================== 2 passed in 0.04s =============================== + +Determine test duration +----------------------- + +If you have a large test suite that runs slowly, you will probably want to use +``-vv --durations`` to find out which tests are the slowest. + +.. code-block:: pytest + + $ uv run pytest -vv --durations=3 + ============================= test session starts ============================== + ... + ============================= slowest 3 durations ============================== + 0.02s setup tests/api/test_add.py::test_add_from_empty + 0.00s call tests/cli/test_help.py::test_help[add] + 0.00s call tests/cli/test_help.py::test_help[update] + ============================== 83 passed in 0.17s ============================== diff --git a/docs/test/pytest/config.rst b/docs/test/pytest/config.rst index 0da37fb8..37645c12 100644 --- a/docs/test/pytest/config.rst +++ b/docs/test/pytest/config.rst @@ -76,7 +76,9 @@ marker per line is permitted. This example is a simple :file:`pytest.ini` file that I use in almost all my projects. Let’s briefly go through the individual lines: -``addopts =`` +.. _addopts: + +``addopts`` allows you to specify the pytest options that we always want to execute in this project. ``--strict-markers`` diff --git a/docs/test/pytest/fixtures.rst b/docs/test/pytest/fixtures.rst index f655fcd5..184cd104 100644 --- a/docs/test/pytest/fixtures.rst +++ b/docs/test/pytest/fixtures.rst @@ -557,6 +557,8 @@ We have seen how different fixture scopes work and how different scopes can be used in different fixtures. However, you may need to define a scope at runtime. This is possible with dynamic scoping. +.. _dynamic-fixture-scope: + Set fixture scope dynamically ----------------------------- @@ -660,6 +662,9 @@ scope: The database is now set up before each test function and then dismantled again. +.. seealso:: + * :doc:`command-line-options` + ``autouse`` for fixtures that are always used --------------------------------------------- diff --git a/docs/test/pytest/index.rst b/docs/test/pytest/index.rst index f320f9f0..65cf541d 100644 --- a/docs/test/pytest/index.rst +++ b/docs/test/pytest/index.rst @@ -60,5 +60,6 @@ You can install pytest in `virtual environments ` with: markers plugins config + command-line-options debug coverage diff --git a/docs/test/pytest/markers.rst b/docs/test/pytest/markers.rst index 034616fd..7c8c1678 100644 --- a/docs/test/pytest/markers.rst +++ b/docs/test/pytest/markers.rst @@ -873,6 +873,147 @@ Let’s run the tests now to make sure everything is working properly: ============================== 3 passed in 0.09s =============================== +Generating markers +------------------ + +Suppose you have a test suite that marks tests for specific platforms, namely +``pytest.mark.darwin``, ``pytest.mark.win32``, and so on, and you also have +tests that run on all platforms and do not have a specific marker. If you are +looking for a way to run only the tests for your specific platform, you can use +the following: + +.. code-block:: python + :caption: conftest.py + + import sys + + import pytest + + ALL = {"win32", "darwin", "linux"} + + + def pytest_setup(item): + supported_platforms = ALL.intersection( + mark.name for mark in item.iter_markers() + ) + pf = sys.platform + if supported_platforms and pf not in supported_platforms: + pytest.skip(f"cannot run on platform {pf}") + +This means that tests are skipped if they have been specified for another +platform. Now let's create a small test file to show what this looks like: + +.. code-block:: python + :caption: test_platform.py + + import pytest + + + def test_foo_everywhere(): + pass + + + @pytest.mark.win32 + def test_foo_on_win32(): + pass + + + @pytest.mark.darwin + def test_foo_on_darwin(): + pass + + + @pytest.mark.linux + def test_foo_on_linux(): + pass + +Now we can run pytest and see the reasons for the skipped tests: + +.. code-block:: pytest + + $ uv run pytest -rs tests/test_platform.py + ============================= test session starts ============================== + platform darwin -- Python 3.14.0b4, pytest-8.4.1, pluggy-1.6.0 + ... + collected 4 items + + tests/test_platform.py ..ss [100%] + + =========================== short test summary info ============================ + SKIPPED [2] tests/conftest.py:20: cannot run on platform darwin + ========================= 2 passed, 2 skipped in 0.03s ========================= + +or more specifically: + +.. code-block:: pytest + + $ uv run pytest -m darwin -rs tests/test_platform.py + ============================= test session starts ============================== + platform darwin -- Python 3.14.0b4, pytest-8.4.1, pluggy-1.6.0 + ... + collected 4 items / 3 deselected / 1 selected + + tests/test_platform.py . [100%] + + ======================= 1 passed, 3 deselected in 0.02s ======================== + +Markers based on test names +--------------------------- + +Alternatively, markers can also be specified using the names of the test +functions by implementing a hook that automatically defines markers: + +.. code-block:: python + :caption: test_platform.py + + def test_foo_everywhere(): + pass + + + def test_foo_on_win32(): + pass + + + def test_foo_on_darwin(): + pass + + + def test_foo_on_linux(): + pass + +Now we dynamically define three markers in :file:`conftest.py` in +`pytest_collection_modifyitems +`_: + +.. code-block:: python + :caption: conftest.py + + import pytest + + + def pytest_collection_modifyitems(items): + for item in items: + if "win32" in item.nodeid: + item.add_marker(pytest.mark.win32) + elif "darwin" in item.nodeid: + item.add_marker(pytest.mark.darwin) + elif "linux" in item.nodeid: + item.add_marker(pytest.mark.linux) + +Now we can use the ``-m`` option to select a set: + +.. code-block:: pytest + + $ uv run pytest -m darwin + ============================= test session starts ============================== + platform darwin -- Python 3.14.0, pytest-9.0.1, pluggy-1.6.0 + ... + collected 4 items / 3 deselected / 1 selected + + tests/test_platform.py . [100%] + + ======================= 1 passed, 3 deselected in 0.00s ======================== + List markers ------------ diff --git a/docs/test/pytest/params.rst b/docs/test/pytest/params.rst index 9fff1f18..d9217d68 100644 --- a/docs/test/pytest/params.rst +++ b/docs/test/pytest/params.rst @@ -355,6 +355,150 @@ We have now become familiar with three ways of :term:`parameterising function in the :samp:`{finish()}` example, parameterisation can generate a large number of test cases. +.. seealso:: + * `Basic pytest_generate_tests example + `_ + * `Generating parameters combinations, depending on command line + `_ + * `A quick port of “testscenarios” + `_ + * `Deferring the setup of parametrized resources + `_ + * `Parametrizing test methods through per-class configuration + `_ + +Postponing the setup of parameterised resources +----------------------------------------------- + +Test functions are parameterised at the time of recording. It is therefore +advisable to only set up complex resources such as database connections or +sub-processes when the actual test is being executed. Here is a simple example +of how you can achieve this. + +.. code-block:: python + :caption: test_backends.py + + import pytest + + + def test_db_initialised(items_db): + # An example test + if items_db.__class__.__name__ == "Sqlite": + pytest.fail("Deliberately failing for demonstration purposes") + +We can now add a test configuration that generates two calls to the +``test_db_initialised`` function and also implements a factory that creates a +database object for the actual test calls: + +.. code-block:: python + :caption: conftest.py + + import pytest + + + def pytest_generate_tests(metafunc): + if "items_db" in metafunc.fixturenames: + metafunc.parametrize("items_db", ["json", "sqlite"], indirect=True) + + + class Json: + "JSON object" + + + class Sqlite: + "Sqlite database object" + + + @pytest.fixture + def items_db(request): + if request.param == "json": + return Json() + elif request.param == "sqlite": + return Sqlite() + else: + raise ValueError("Invalid internal test config") + +First, let’s take a look at what it looks like at the time of setup: + +.. code-block:: pytest + :emphasize-lines: 12-13 + + $ uv run pytest tests/test_backends.py --collect-only + ============================= test session starts ============================== + platform darwin -- Python 3.14.0b4, pytest-8.4.1, pluggy-1.6.0 + rootdir: /Users/veit/sandbox/items + configfile: pyproject.toml + plugins: anyio-4.9.0, Faker-37.4.0, cov-6.2.1 + collected 2 items + + + + + + + + ========================== 2 test collected in 0.01s =========================== + +.. code-block:: pytest + + $ uv run pytest -q tests/test_backends.py + .F [100%] + ================================= FAILURES ================================= + _______________________ test_db_initialised[sqlite] ________________________ + + db = + + def test_db_initialised(items_db): + # An example test + if db.__class__.__name__ == "Sqlite": + > pytest.fail("Deliberately failing for demo purposes") + E Failed: Deliberately failing for demo purposes + + test_backends.py:8: Failed + ========================= short test summary info ========================== + FAILED tests/test_backends.py::test_db_initialised[sqlite] - Failed: deli... + 1 failed, 1 passed in 0.03s + +Parametrised exceptions +----------------------- + +`pytest.raises() `_ + can be used with the decorator ``pytest.mark.parametrize`` to write + parametrised tests in which some tests raise exceptions and others do not. + +`contextlib.nullcontext `_ + can be used to test test cases that are not expected to raise exceptions but + should return a specific value. The value is specified as the + ``enter_result`` parameter, which is available as the target of the ``with`` + statement. + +Example of parameterised exceptions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + from contextlib import nullcontext + + import pytest + + + @pytest.mark.parametrize( + "divisor, expectation", + [ + (3, nullcontext(2)), + (2, nullcontext(3)), + (1, nullcontext(6)), + (0, pytest.raises(ZeroDivisionError)), + ], + ) + def test_division(divisor, expectation): + """Test expected division results.""" + with expectation as e: + assert (6 / divisor) == e + +The first three test cases should run without exceptions, while the fourth +should raise a ``ZeroDivisionError`` exception, as expected by pytest. + ---- .. [#] https://docs.pytest.org/en/latest/reference/reference.html#metafunc diff --git a/docs/test/pytest/plugins.rst b/docs/test/pytest/plugins.rst index 2c8a7d4b..8a722998 100644 --- a/docs/test/pytest/plugins.rst +++ b/docs/test/pytest/plugins.rst @@ -45,6 +45,8 @@ each test is executed in the order in which it appears in the file. However, it can sometimes be useful to change this order. The following plugins change the usual sequence of a test: +.. _xdist-plugin: + `pytest-xdist `_ executes tests in parallel, either with several CPUs on one machine or several remote machines. @@ -62,22 +64,22 @@ usual sequence of a test: :alt: License :target: https://github.com/pytest-dev/pytest-xdist?tab=MIT-1-ov-file#readme -`pytest-freethreaded `_ - for checking whether tests and libraries are thread-safe with Python 3.13’s - experimental freethreaded mode. +`pytest-randomly `_ + runs the tests in random order, first by file, then by class, then by test + file. - .. image:: https://raster.shields.io/github/stars/tonybaloney/pytest-freethreaded + .. image:: https://raster.shields.io/github/stars/pytest-dev/pytest-randomly :alt: Stars - :target: https://github.com/tonybaloney/pytest-freethreaded/stargazers - .. image:: https://raster.shields.io/github/contributors/tonybaloney/pytest-freethreaded + :target: https://github.com/pytest-dev/pytest-randomly/stargazers + .. image:: https://raster.shields.io/github/contributors/pytest-dev/pytest-randomly :alt: Contributors - :target: https://github.com/tonybaloney/pytest-freethreaded/graphs/contributors - .. image:: https://raster.shields.io/github/commit-activity/y/tonybaloney/pytest-freethreaded + :target: https://github.com/pytest-dev/pytest-randomly/graphs/contributors + .. image:: https://raster.shields.io/github/commit-activity/y/pytest-dev/pytest-randomly :alt: Commit activity - :target: https://github.com/tonybaloney/pytest-freethreaded/graphs/commit-activity - .. image:: https://raster.shields.io/github/license/tonybaloney/pytest-freethreaded + :target: https://github.com/pytest-dev/pytest-randomly/graphs/commit-activity + .. image:: https://raster.shields.io/github/license/pytest-dev/pytest-randomly :alt: License - :target: https://github.com/tonybaloney/pytest-freethreaded?tab=MIT-1-ov-file#readme + :target: https://github.com/pytest-dev/pytest-randomly?tab=MIT-1-ov-file#readme `pytest-rerunfailures `_ re-executes failed tests and is particularly helpful in the case of faulty @@ -96,22 +98,6 @@ usual sequence of a test: :alt: License :target: https://github.com/pytest-dev/pytest-rerunfailures?tab=License-1-ov-file#readme -`pytest-repeat `_ - makes it easy to repeat one or more tests. - - .. image:: https://raster.shields.io/github/stars/pytest-dev/pytest-repeat - :alt: Stars - :target: https://github.com/pytest-dev/pytest-repeat/stargazers - .. image:: https://raster.shields.io/github/contributors/pytest-dev/pytest-repeat - :alt: Contributors - :target: https://github.com/pytest-dev/pytest-repeat/graphs/contributors - .. image:: https://raster.shields.io/github/commit-activity/y/pytest-dev/pytest-repeat - :alt: Commit activity - :target: https://github.com/pytest-dev/pytest-repeat/graphs/commit-activity - .. image:: https://raster.shields.io/github/license/pytest-dev/pytest-repeat - :alt: License - :target: https://github.com/pytest-dev/pytest-repeat?tab=License-1-ov-file#readme - `pytest-order `_ enables the order to be defined using :doc:`markers`. @@ -128,63 +114,45 @@ usual sequence of a test: :alt: License :target: https://github.com/pytest-dev/pytest-xdist?tab=MIT-1-ov-file#readme -`pytest-randomly `_ - runs the tests in random order, first by file, then by class, then by test - file. +`pytest-repeat `_ + makes it easy to repeat one or more tests. - .. image:: https://raster.shields.io/github/stars/pytest-dev/pytest-randomly + .. image:: https://raster.shields.io/github/stars/pytest-dev/pytest-repeat :alt: Stars - :target: https://github.com/pytest-dev/pytest-randomly/stargazers - .. image:: https://raster.shields.io/github/contributors/pytest-dev/pytest-randomly + :target: https://github.com/pytest-dev/pytest-repeat/stargazers + .. image:: https://raster.shields.io/github/contributors/pytest-dev/pytest-repeat :alt: Contributors - :target: https://github.com/pytest-dev/pytest-randomly/graphs/contributors - .. image:: https://raster.shields.io/github/commit-activity/y/pytest-dev/pytest-randomly + :target: https://github.com/pytest-dev/pytest-repeat/graphs/contributors + .. image:: https://raster.shields.io/github/commit-activity/y/pytest-dev/pytest-repeat :alt: Commit activity - :target: https://github.com/pytest-dev/pytest-randomly/graphs/commit-activity - .. image:: https://raster.shields.io/github/license/pytest-dev/pytest-randomly + :target: https://github.com/pytest-dev/pytest-repeat/graphs/commit-activity + .. image:: https://raster.shields.io/github/license/pytest-dev/pytest-repeat :alt: License - :target: https://github.com/pytest-dev/pytest-randomly?tab=MIT-1-ov-file#readme - -… modified output -~~~~~~~~~~~~~~~~~ - -The normal pytest output mainly shows dots for passed tests and characters for -other output. If you pass ``-v``, you will see a list of test names with the -result. However, there are plugins that change the output even further: + :target: https://github.com/pytest-dev/pytest-repeat?tab=License-1-ov-file#readme -`pytest-instafail `_ - adds a ``--instafail`` option that reports tracebacks and output from failed - tests immediately after the failure. Normally, pytest reports tracebacks and - output from failed tests only after all tests have completed. +`pytest-freethreaded `_ + for checking whether tests and libraries are thread-safe with Python 3.13’s + experimental freethreaded mode. - .. image:: https://raster.shields.io/github/stars/pytest-dev/pytest-instafail + .. image:: https://raster.shields.io/github/stars/tonybaloney/pytest-freethreaded :alt: Stars - :target: https://github.com/pytest-dev/pytest-instafail/stargazers - .. image:: https://raster.shields.io/github/contributors/pytest-dev/pytest-instafail + :target: https://github.com/tonybaloney/pytest-freethreaded/stargazers + .. image:: https://raster.shields.io/github/contributors/tonybaloney/pytest-freethreaded :alt: Contributors - :target: https://github.com/pytest-dev/pytest-instafail/graphs/contributors - .. image:: https://raster.shields.io/github/commit-activity/y/pytest-dev/pytest-instafail + :target: https://github.com/tonybaloney/pytest-freethreaded/graphs/contributors + .. image:: https://raster.shields.io/github/commit-activity/y/tonybaloney/pytest-freethreaded :alt: Commit activity - :target: https://github.com/pytest-dev/pytest-instafail/graphs/commit-activity - .. image:: https://raster.shields.io/github/license/pytest-dev/pytest-instafail + :target: https://github.com/tonybaloney/pytest-freethreaded/graphs/commit-activity + .. image:: https://raster.shields.io/github/license/tonybaloney/pytest-freethreaded :alt: License - :target: https://github.com/pytest-dev/pytest-rerunfailures?tab=License-1-ov-file#readme + :target: https://github.com/tonybaloney/pytest-freethreaded?tab=MIT-1-ov-file#readme -`pytest-edit `_ - opens an editor after a failed test. +… modified output +~~~~~~~~~~~~~~~~~ - .. image:: https://raster.shields.io/github/stars/mrmino/pytest-edit - :alt: Stars - :target: https://github.com/mrmino/pytest-edit/stargazers - .. image:: https://raster.shields.io/github/contributors/mrmino/pytest-edit - :alt: Contributors - :target: https://github.com/mrmino/pytest-edit - .. image:: https://raster.shields.io/github/commit-activity/y/mrmino/pytest-edit - :alt: Commit activity - :target: https://github.com/mrmino/pytest-edit/graphs/commit-activity - .. image:: https://raster.shields.io/github/license/mrmino/pytest-edit - :alt: License - :target: https://github.com/mrmino/pytest-edit?tab=MIT-1-ov-file#readme +The normal pytest output mainly shows dots for passed tests and characters for +other output. If you pass ``-v``, you will see a list of test names with the +result. However, there are plugins that change the output even further: `pytest-sugar `_ shows green checkmarks instead of dots for passed tests and has a nice @@ -220,22 +188,24 @@ result. However, there are plugins that change the output even further: :alt: License :target: https://github.com/pytest-dev/pytest-html?tab=License-1-ov-file#readme -`pytest-icdiff `_ - improves diffs in the error messages of the pytest assertion with `ICDiff - `_. +`pytest-check `_ + allows multiple errors per test. - .. image:: https://raster.shields.io/github/stars/hjwp/pytest-icdiff + .. image:: https://raster.shields.io/github/stars/okken/pytest-check :alt: Stars - :target: https://github.com/hjwp/pytest-icdiff/stargazers - .. image:: https://raster.shields.io/github/contributors/hjwp/pytest-icdiff + :target: https://github.com/okken/pytest-check/stargazers + + .. image:: https://raster.shields.io/github/contributors/okken/pytest-check :alt: Contributors - :target: https://github.com/hjwp/pytest-icdiff/graphs/contributors - .. image:: https://raster.shields.io/github/commit-activity/y/hjwp/pytest-icdiff + :target: https://github.com/okken/pytest-check/graphs/contributors + + .. image:: https://raster.shields.io/github/commit-activity/y/okken/pytest-check :alt: Commit activity - :target: https://github.com/hjwp/pytest-icdiff/graphs/commit-activity - .. image:: https://raster.shields.io/github/license/hjwp/pytest-icdiff - :alt: License - :target: https://github.com/hjwp/pytest-icdiff?tab=MIT-1-ov-file#readme + :target: https://github.com/okken/pytest-check/graphs/commit-activity + + .. image:: https://raster.shields.io/github/license/okken/pytest-check + :alt: Lizenz + :target: https://github.com/okken/pytest-check?tab=MIT-1-ov-file#readme … web development ~~~~~~~~~~~~~~~~~ @@ -243,24 +213,6 @@ result. However, there are plugins that change the output even further: pytest is used extensively for testing web projects and there is a long list of plugins that further simplify testing: - -`pytest-httpx `_ - facilitates the testing of `HTTPX `_ and - `FastAPI `_ applications. - - .. image:: https://raster.shields.io/github/stars/Colin-b/pytest_httpx - :alt: Stars - :target: https://github.com/Colin-b/pytest_httpx/stargazers - .. image:: https://raster.shields.io/github/contributors/Colin-b/pytest_httpx - :alt: Contributors - :target: https://github.com/Colin-b/pytest_httpx/graphs/contributors - .. image:: https://raster.shields.io/github/commit-activity/y/Colin-b/pytest_httpx - :alt: Commit activity - :target: https://github.com/Colin-b/pytest_httpx/graphs/commit-activity - .. image:: https://raster.shields.io/github/license/Colin-b/pytest_httpx - :alt: License - :target: https://github.com/Colin-b/pytest_httpx?tab=MIT-1-ov-file#readme - `Playwright for Python `_ was specially developed for end-to-end testing. Playwright supports all modern rendering engines such as Chromium, WebKit and Firefox with a single @@ -279,21 +231,22 @@ plugins that further simplify testing: :alt: License :target: https://github.com/Microsoft/playwright-python?tab=MIT-1-ov-file#readme -`pyleniumio `_ - is a thin Python wrapper around Selenium with simple and clear syntax. +`pytest-httpx `_ + facilitates the testing of `HTTPX `_ and + `FastAPI `_ applications. - .. image:: https://raster.shields.io/github/stars/ElSnoMan/pyleniumio + .. image:: https://raster.shields.io/github/stars/Colin-b/pytest_httpx :alt: Stars - :target: https://github.com/ElSnoMan/pyleniumio/stargazers - .. image:: https://raster.shields.io/github/contributors/ElSnoMan/pyleniumio + :target: https://github.com/Colin-b/pytest_httpx/stargazers + .. image:: https://raster.shields.io/github/contributors/Colin-b/pytest_httpx :alt: Contributors - :target: https://github.com/ElSnoMan/pyleniumio/graphs/contributors - .. image:: https://raster.shields.io/github/commit-activity/y/ElSnoMan/pyleniumio + :target: https://github.com/Colin-b/pytest_httpx/graphs/contributors + .. image:: https://raster.shields.io/github/commit-activity/y/Colin-b/pytest_httpx :alt: Commit activity - :target: https://github.com/ElSnoMan/pyleniumio/graphs/commit-activity - .. image:: https://raster.shields.io/github/license/ElSnoMan/pyleniumio + :target: https://github.com/Colin-b/pytest_httpx/graphs/commit-activity + .. image:: https://raster.shields.io/github/license/Colin-b/pytest_httpx :alt: License - :target: https://github.com/ElSnoMan/pyleniumio?tab=MIT-1-ov-file#readme + :target: https://github.com/Colin-b/pytest_httpx?tab=MIT-1-ov-file#readme `pytest-selenium `_ provides fixtures that enable simple configuration of browser-based tests @@ -312,6 +265,22 @@ plugins that further simplify testing: :alt: License :target: https://github.com/pytest-dev/pytest-selenium?tab=License-1-ov-file#readme +`pyleniumio `_ + is a thin Python wrapper around Selenium with simple and clear syntax. + + .. image:: https://raster.shields.io/github/stars/ElSnoMan/pyleniumio + :alt: Stars + :target: https://github.com/ElSnoMan/pyleniumio/stargazers + .. image:: https://raster.shields.io/github/contributors/ElSnoMan/pyleniumio + :alt: Contributors + :target: https://github.com/ElSnoMan/pyleniumio/graphs/contributors + .. image:: https://raster.shields.io/github/commit-activity/y/ElSnoMan/pyleniumio + :alt: Commit activity + :target: https://github.com/ElSnoMan/pyleniumio/graphs/commit-activity + .. image:: https://raster.shields.io/github/license/ElSnoMan/pyleniumio + :alt: License + :target: https://github.com/ElSnoMan/pyleniumio?tab=MIT-1-ov-file#readme + .. _fake_plugins: … fake data @@ -416,37 +385,21 @@ therefore not surprising that there are several plugins that fulfil this need: :alt: License :target: https://github.com/pytest-dev/pytest-cov?tab=MIT-1-ov-file#readme -`pytest-benchmark `_ - performs benchmark timing for code within tests. - - .. image:: https://raster.shields.io/github/stars/ionelmc/pytest-benchmark - :alt: Stars - :target: https://github.com/ionelmc/pytest-benchmark/stargazers - .. image:: https://raster.shields.io/github/contributors/ionelmc/pytest-benchmark - :alt: Contributors - :target: https://github.com/ionelmc/pytest-benchmark/graphs/contributors - .. image:: https://raster.shields.io/github/commit-activity/y/ionelmc/pytest-benchmark - :alt: Commit activity - :target: https://github.com/ionelmc/pytest-benchmark/graphs/commit-activity - .. image:: https://raster.shields.io/github/license/ionelmc/pytest-benchmark - :alt: License - :target: https://github.com/ionelmc/pytest-benchmark?tab=BSD-2-Clause-1-ov-file#readme - -`pytest-timeout `_ - prevents tests from running too long. +`pytest-mock `_ + is a thin wrapper around the :doc:`unittest.mock <../mock>` patching API. - .. image:: https://raster.shields.io/github/stars/pytest-dev/pytest-timeout + .. image:: https://raster.shields.io/github/stars/pytest-dev/pytest-mock :alt: Stars - :target: https://github.com/pytest-dev/pytest-timeout/stargazers - .. image:: https://raster.shields.io/github/contributors/pytest-dev/pytest-timeout + :target: https://github.com/pytest-dev/pytest-mock/stargazers + .. image:: https://raster.shields.io/github/contributors/pytest-dev/pytest-mock :alt: Contributors - :target: https://github.com/pytest-dev/pytest-timeout/graphs/contributors - .. image:: https://raster.shields.io/github/commit-activity/y/pytest-dev/pytest-timeout + :target: https://github.com/pytest-dev/pytest-mock/graphs/contributors + .. image:: https://raster.shields.io/github/commit-activity/y/pytest-dev/pytest-mock :alt: Commit activity - :target: https://github.com/pytest-dev/pytest-timeout/graphs/commit-activity - .. image:: https://raster.shields.io/github/license/pytest-dev/pytest-timeout + :target: https://github.com/pytest-dev/pytest-mock/graphs/commit-activity + .. image:: https://raster.shields.io/github/license/pytest-dev/pytest-mock :alt: License - :target: https://github.com/pytest-dev/pytest-timeout?tab=MIT-1-ov-file#readme + :target: https://github.com/pytest-dev/pytest-mock?tab=MIT-1-ov-file#readme `pytest-asyncio `_ tests asynchronous functions. @@ -464,37 +417,75 @@ therefore not surprising that there are several plugins that fulfil this need: :alt: License :target: https://github.com/pytest-dev/pytest-asyncio?tab=MIT-1-ov-file#readme -`pytest-mock `_ - is a thin wrapper around the :doc:`unittest.mock <../mock>` patching API. +`pytest-benchmark `_ + performs benchmark timing for code within tests. - .. image:: https://raster.shields.io/github/stars/pytest-dev/pytest-mock + .. image:: https://raster.shields.io/github/stars/ionelmc/pytest-benchmark :alt: Stars - :target: https://github.com/pytest-dev/pytest-mock/stargazers - .. image:: https://raster.shields.io/github/contributors/pytest-dev/pytest-mock + :target: https://github.com/ionelmc/pytest-benchmark/stargazers + .. image:: https://raster.shields.io/github/contributors/ionelmc/pytest-benchmark :alt: Contributors - :target: https://github.com/pytest-dev/pytest-mock/graphs/contributors - .. image:: https://raster.shields.io/github/commit-activity/y/pytest-dev/pytest-mock + :target: https://github.com/ionelmc/pytest-benchmark/graphs/contributors + .. image:: https://raster.shields.io/github/commit-activity/y/ionelmc/pytest-benchmark :alt: Commit activity - :target: https://github.com/pytest-dev/pytest-mock/graphs/commit-activity - .. image:: https://raster.shields.io/github/license/pytest-dev/pytest-mock + :target: https://github.com/ionelmc/pytest-benchmark/graphs/commit-activity + .. image:: https://raster.shields.io/github/license/ionelmc/pytest-benchmark :alt: License - :target: https://github.com/pytest-dev/pytest-mock?tab=MIT-1-ov-file#readme + :target: https://github.com/ionelmc/pytest-benchmark?tab=BSD-2-Clause-1-ov-file#readme -`pytest-patterns `_ - provides a pattern matching engine optimised for tests. +`pytest-bdd `_ + writes :abbr:`BDD (Behavior Driven Development)` tests with pytest. - .. image:: https://raster.shields.io/github/stars/flyingcircusio/pytest-patterns + .. image:: https://raster.shields.io/github/stars/pytest-dev/pytest-bdd :alt: Stars - :target: https://github.com/flyingcircusio/pytest-patterns/stargazers - .. image:: https://raster.shields.io/github/contributors/flyingcircusio/pytest-patterns + :target: https://github.com/pytest-dev/pytest-bdd/stargazers + .. image:: https://raster.shields.io/github/contributors/pytest-dev/pytest-bdd :alt: Contributors - :target: https://github.com/flyingcircusio/pytest-patterns/graphs/contributors - .. image:: https://raster.shields.io/github/commit-activity/y/flyingcircusio/pytest-patterns + :target: https://github.com/pytest-dev/pytest-bdd/graphs/contributors + .. image:: https://raster.shields.io/github/commit-activity/y/pytest-dev/pytest-bdd :alt: Commit activity - :target: https://github.com/flyingcircusio/pytest-patterns/graphs/commit-activity - .. image:: https://raster.shields.io/github/license/flyingcircusio/pytest-patterns + :target: https://github.com/pytest-dev/pytest-bdd/graphs/commit-activity + .. image:: https://raster.shields.io/github/license/pytest-dev/pytest-bdd :alt: License - :target: https://github.com/flyingcircusio/pytest-patterns?tab=MIT-1-ov-file#readme + :target: https://github.com/pytest-dev/pytest-bdd?tab=MIT-1-ov-file#readme + +.. _pytest_memray: + +`pytest-memray `_ + pytest plugin for integrating :doc:`Python4DataScience:performance/memray` + into your test suite. + + .. image:: https://raster.shields.io/github/stars/bloomberg/pytest-memray + :alt: Stars + :target: https://github.com/bloomberg/pytest-memray/stargazers + + .. image:: https://raster.shields.io/github/contributors/bloomberg/pytest-memray + :alt: Contributors + :target: https://github.com/bloomberg/pytest-memray/graphs/contributors + + .. image:: https://raster.shields.io/github/commit-activity/y/bloomberg/pytest-memray + :alt: Commit activity + :target: https://github.com/bloomberg/pytest-memraygraphs/commit-activity + + .. image:: https://raster.shields.io/github/license/bloomberg/pytest-memray + :alt: Lizenz + :target: https://github.com/ionelmc/pytest-benchmark?tab=BSD-2-Clause-1-ov-file#readme + +`pytest-timeout `_ + prevents tests from running too long. + + .. image:: https://raster.shields.io/github/stars/pytest-dev/pytest-timeout + :alt: Stars + :target: https://github.com/pytest-dev/pytest-timeout/stargazers + .. image:: https://raster.shields.io/github/contributors/pytest-dev/pytest-timeout + :alt: Contributors + :target: https://github.com/pytest-dev/pytest-timeout/graphs/contributors + .. image:: https://raster.shields.io/github/commit-activity/y/pytest-dev/pytest-timeout + :alt: Commit activity + :target: https://github.com/pytest-dev/pytest-timeout/graphs/commit-activity + .. image:: https://raster.shields.io/github/license/pytest-dev/pytest-timeout + :alt: License + :target: https://github.com/pytest-dev/pytest-timeout?tab=MIT-1-ov-file#readme :doc:`pytest-grpc ` is a Pytest plugin for @@ -513,22 +504,6 @@ therefore not surprising that there are several plugins that fulfil this need: :alt: License :target: https://github.com/kataev/pytest-grpc?tab=MIT-1-ov-file#readme -`pytest-bdd `_ - writes :abbr:`BDD (Behavior Driven Development)` tests with pytest. - - .. image:: https://raster.shields.io/github/stars/pytest-dev/pytest-bdd - :alt: Stars - :target: https://github.com/pytest-dev/pytest-bdd/stargazers - .. image:: https://raster.shields.io/github/contributors/pytest-dev/pytest-bdd - :alt: Contributors - :target: https://github.com/pytest-dev/pytest-bdd/graphs/contributors - .. image:: https://raster.shields.io/github/commit-activity/y/pytest-dev/pytest-bdd - :alt: Commit activity - :target: https://github.com/pytest-dev/pytest-bdd/graphs/commit-activity - .. image:: https://raster.shields.io/github/license/pytest-dev/pytest-bdd - :alt: License - :target: https://github.com/pytest-dev/pytest-bdd?tab=MIT-1-ov-file#readme - Own plugins ----------- diff --git a/docs/test/tox.rst b/docs/test/tox.rst index 04fd286e..4ed0149f 100644 --- a/docs/test/tox.rst +++ b/docs/test/tox.rst @@ -18,8 +18,9 @@ to test with different dependency configurations and different configurations for different operating systems. tox uses project information from the :file:`setup.py` or :file:`pyproject.toml` file for the package under test to create an installable :doc:`distribution of your package -<../packs/distribution>`. It searches the :file:`tox.ini` file for a list of -environments and then performs the following steps for each: +<../packs/distribution>`. It searches for a list of environments in the +``[tool.tox]`` section of the :file:`pyproject.toml` file, and then performs the +following steps for each one: #. creates a :term:`virtual environment ` #. installs some dependencies with :term:`pip` @@ -35,60 +36,30 @@ To accelerate this process with :term:`uv`, we don’t use tox directly, but Setting up tox -------------- -Until now, we had the items code in a :file:`src/` directory and the tests in -:file:`tests/api/` and :file:`tests/cli/`. Now we will add a :file:`tox.ini` -file so that the structure looks like this: +Previously, tox was usually configured in the :file:`tox.ini` file. However, +since tox 4.44.0, its functionality has been frozen and future configuration +parameters will probably only be provided in a +:doc:`Python4DataScience:data-processing/serialisation-formats/toml/index` file, +for example in the :ref:`pyproject-toml` file. Let’s take a look at a simple +configuration in the :file:`pyproject.toml` file: -.. code-block:: console - :emphasize-lines: 16 - - items - ├── … - ├── pyproject.toml - ├── src - │ └── items - │ └── … - ├── tests - │ ├── api - │ │ ├── __init__.py - │ │ ├── conftest.py - │ │ └── test_….py - │ └── cli - │ ├── __init__.py - │ ├── conftest.py - │ └── test_….py - └── tox.ini - -This is a typical layout for many projects. Let’s take a look at a simple -:file:`tox.ini` file in the Items project: - -.. code-block:: ini - - [tox] - envlist = py313 - isolated_build = True - - [testenv] - deps = - pytest>=6.0 - faker - commands = pytest - -In the ``[tox]`` section, we have defined ``envlist = py313``. This is a -shortcut that tells tox to run our tests with Python version 3.13. We will be -adding more Python versions shortly, but using one version helps to understand -the flow of tox. - -Also note the line ``isolated_build = True``: This is required for all packages -configured with :file:`pyproject.toml`. However, for all projects configured -with :file:`setup.py` that use the :term:`setuptools` library, this line can be -omitted. - -In the ``[testenv]`` section, ``pytest`` and ``faker`` are listed as -dependencies under ``deps``. So tox knows that we need these two tools for -testing. If you wish, you can also specify which version should be used, for -example ``pytest>=6.0``. Finally, commands instruct tox to execute ``pytest`` in -every environment. +.. code-block:: toml + + [tool.tox] + env_list = ["py313"] + + [tool.tox.env_run_base] + dependency_groups = [ "tests" ] + commands = [[ "pytest"]] + +In the ``[tool.tox]`` section, we defined ``env_list = ["py313"]``. This is a +shorthand that instructs tox to run our tests using Python version 3.13. We will +add more Python versions shortly, but using one version helps us better +understand how tox works. + +In the ``[tool.tox.env_run_base]`` section, ``dependency_groups`` specifies +``tests``. This tells tox that the corresponding libraries should be installed +in this environment. Finally, ``commands`` instructs tox to run ``pytest``. Executing tox ------------- @@ -112,15 +83,20 @@ To run tox, simply start tox: .. code-block:: pytest $ uv run tox - py313: install_package> .venv/bin/uv pip install --reinstall --no-deps items@/Users/veit/cusy/prj/items/.tox/.tmp/package/57/items-0.1.0.tar.gz + .pkg: _optional_hooks> python /Users/veit/cusy/prj/items/.venv/lib/python3.13/site-packages/pyproject_api/_backend.py True hatchling.build + .pkg: get_requires_for_build_sdist> python /Users/veit/cusy/prj/items/.venv/lib/python3.13/site-packages/pyproject_api/_backend.py True hatchling.build + .pkg: build_sdist> python /Users/veit/cusy/prj/items/.venv/lib/python3.13/site-packages/pyproject_api/_backend.py True hatchling.build + py313: install_package> .venv/bin/uv pip install --reinstall --no-deps items@/Users/veit/cusy/prj/items/.tox/.tmp/package/18/items-0.1.0.tar.gz py313: commands[0]> python --version --version + Python 3.13.0 (main, Oct 7 2024, 23:47:22) [Clang 18.1.8 ] + py313: commands[1]> coverage run -m pytest ============================= test session starts ============================== - platform darwin -- Python 3.13.0, pytest-8.4.1, pluggy-1.6.0 + platform darwin -- Python 3.13.0, pytest-9.0.2, pluggy-1.6.0 cachedir: .tox/py313/.pytest_cache rootdir: /Users/veit/cusy/prj/items configfile: pyproject.toml testpaths: tests - plugins: anyio-4.9.0, Faker-37.4.0, cov-6.2.1 + plugins: Faker-40.1.0, cov-7.0.0 collected 83 items tests/api/test_add.py ...... [ 7%] @@ -149,68 +125,111 @@ To run tox, simply start tox: tests/cli/test_update.py . [ 98%] tests/cli/test_version.py . [100%] - ============================== 83 passed in 0.27s ============================== - py313: OK ✔ in 1.17 seconds + ============================== 83 passed in 0.35s ============================== + .pkg: _exit> python /Users/veit/cusy/prj/items/.venv/lib/python3.13/site-packages/pyproject_api/_backend.py True hatchling.build + py313: OK (1.19=setup[0.45]+cmd[0.01,0.72] seconds) + congratulations :) (1.23 seconds) Testing multiple Python versions -------------------------------- -To do this, we extend ``envlist`` in the :file:`tox.ini` file to add further -Python versions: +To do this, we extend ``envlist`` in the :file:`pyproject.toml` file to add +further Python versions: -.. code-block:: ini - :emphasize-lines: 2, 4 +.. code-block:: toml - [tox] - envlist = py3{9,10,11,12,13,13t,14,14t} - isolated_build = True - skip_missing_interpreters = True + [tool.tox] + env_list = [ + "py3{10-14}", + "py{13-14}t", + ] + skip_missing_interpreters = true -We will now test Python versions from 3.8 to 3.11. In addition, we have also -added the setting ``skip_missing_interpreters = True`` so that tox does not fail +We will now test Python versions from 3.10 to 3.14. In addition, we have also +added the setting ``skip_missing_interpreters = true`` so that tox does not fail if one of the listed Python versions is missing on your system. If the value is -set to ``True``, tox will run the tests with every available Python version, but +set to ``true``, tox will run the tests with every available Python version, but will skip versions it doesn’t find without failing. The output is very similar, although I will only highlight the differences in the following illustration: .. code-block:: pytest - :emphasize-lines: 3-4, 8-12, 16-20, 24-28, 32- - - $ uv run tox - ... - py39: install_package> python -I -m pip install --force-reinstall --no-deps /Users/veit/cusy/prj/items/.tox/.tmp/package/17/items-0.1.0.tar.gz - py39: commands[0]> coverage run -m pytest - ============================= test session starts ============================== - ... - ============================== 49 passed in 0.16s ============================== - py39: OK ✔ in 2.17 seconds - py310: skipped because could not find python interpreter with spec(s): py310 - py310: SKIP ⚠ in 0.01 seconds - py311: install_package> python -I -m pip install --force-reinstall --no-deps /Users/veit/cusy/prj/items/.tox/.tmp/package/18/items-0.1.0.tar.gz - py311: commands[0]> coverage run -m pytest - ============================= test session starts ============================== - ... - ============================== 49 passed in 0.15s ============================== - py311: OK ✔ in 1.41 seconds - py312: install_package> python -I -m pip install --force-reinstall --no-deps /Users/veit/cusy/prj/items/.tox/.tmp/package/19/items-0.1.0.tar.gz - py312: commands[0]> coverage run -m pytest - ============================= test session starts ============================== - ... - ============================== 49 passed in 0.15s ============================== - py312: OK ✔ in 1.43 seconds - py313: install_package> python -I -m pip install --force-reinstall --no-deps /Users/veit/cusy/prj/items/.tox/.tmp/package/20/items-0.1.0.tar.gz - py313: commands[0]> coverage run -m pytest - ============================= test session starts ============================== - ... - ============================== 49 passed in 0.16s ============================== - .pkg: _exit> python /Users/veit/cusy/prj/items/.venv/lib/python3.13/site-packages/pyproject_api/_backend.py True hatchling.build - py313: OK ✔ in 1.48 seconds - py39: OK (2.17=setup[1.54]+cmd[0.63] seconds) - py310: SKIP (0.01 seconds) - py311: OK (1.41=setup[0.81]+cmd[0.60] seconds) - py312: OK (1.43=setup[0.82]+cmd[0.61] seconds) - py313: OK (1.48=setup[0.82]+cmd[0.66] seconds) - congratulations :) (10.46 seconds) + :emphasize-lines: 3-6, 10-14, 18-22, 26-30, 34-38, 42-46, 50-54, 59- + + $ uv run tox + ... + py310: install_package> .venv/bin/uv pip install --reinstall --no-deps items@/Users/veit/cusy/prj/items/.tox/.tmp/package/19/items-0.1.0.tar.gz + py310: commands[0]> python --version --version + Python 3.10.17 (main, Apr 9 2025, 03:47:39) [Clang 20.1.0 ] + py310: commands[1]> coverage run -m pytest + ============================= test session starts ============================== + ... + ============================== 83 passed in 0.35s ============================== + py310: OK ✔ in 1.3 seconds + py311: install_package> .venv/bin/uv pip install --reinstall --no-deps items@/Users/veit/cusy/prj/items/.tox/.tmp/package/20/items-0.1.0.tar.gz + py311: commands[0]> python --version --version + Python 3.11.11 (main, Feb 5 2025, 18:58:27) [Clang 19.1.6 ] + py311: commands[1]> coverage run -m pytest + ============================= test session starts ============================== + ... + ============================== 83 passed in 0.36s ============================== + py311: OK ✔ in 1.16 seconds + py312: install_package> .venv/bin/uv pip install --reinstall --no-deps items@/Users/veit/cusy/prj/items/.tox/.tmp/package/21/items-0.1.0.tar.gz + py312: commands[0]> python --version --version + Python 3.12.12 (main, Oct 14 2025, 21:38:21) [Clang 20.1.4 ] + py312: commands[1]> coverage run -m pytest + ============================= test session starts ============================== + ... + ============================== 83 passed in 0.55s ============================== + py312: OK ✔ in 1.79 seconds + py313: install_package> .venv/bin/uv pip install --reinstall --no-deps items@/Users/veit/cusy/prj/items/.tox/.tmp/package/22/items-0.1.0.tar.gz + py313: commands[0]> python --version --version + Python 3.13.0 (main, Oct 7 2024, 23:47:22) [Clang 18.1.8 ] + py313: commands[1]> coverage run -m pytest + ============================= test session starts ============================== + ... + ============================== 83 passed in 0.35s ============================== + py313: OK ✔ in 1.07 seconds + py314: install_package> .venv/bin/uv pip install --reinstall --no-deps items@/Users/veit/cusy/prj/items/.tox/.tmp/package/23/items-0.1.0.tar.gz + py314: commands[0]> python --version --version + Python 3.14.0 (main, Oct 14 2025, 21:10:22) [Clang 20.1.4 ] + py314: commands[1]> coverage run -m pytest + ============================= test session starts ============================== + ... + ============================== 83 passed in 0.36s ============================== + py314: OK ✔ in 1.28 seconds + py313t: install_package> .venv/bin/uv pip install --reinstall --no-deps items@/Users/veit/cusy/prj/items/.tox/.tmp/package/24/items-0.1.0.tar.gz + py313t: commands[0]> python --version --version + Python 3.13.0 experimental free-threading build (main, Oct 16 2024, 08:24:33) [Clang 18.1.8 ] + py313t: commands[1]> coverage run -m pytest + ============================= test session starts ============================== + ... + ============================== 83 passed in 0.49s ============================== + py313t: OK ✔ in 1.51 seconds + py314t: install_package> .venv/bin/uv pip install --reinstall --no-deps items@/Users/veit/cusy/prj/items/.tox/.tmp/package/25/items-0.1.0.tar.gz + py314t: commands[0]> python --version --version + Python 3.14.0b4 free-threading build (main, Jul 8 2025, 21:06:49) [Clang 20.1.4 ] + py314t: commands[1]> coverage run -m pytest + ============================= test session starts ============================== + ... + ============================== 83 passed in 0.39s ============================== + .pkg: _exit> python /Users/veit/cusy/prj/items/.venv/lib/python3.13/site-packages/pyproject_api/_backend.py True hatchling.build + py310: OK (1.30=setup[0.54]+cmd[0.01,0.75] seconds) + py311: OK (1.16=setup[0.38]+cmd[0.01,0.76] seconds) + py312: OK (1.79=setup[0.42]+cmd[0.01,1.36] seconds) + py313: OK (1.07=setup[0.34]+cmd[0.01,0.71] seconds) + py314: OK (1.28=setup[0.42]+cmd[0.01,0.85] seconds) + py313t: OK (1.51=setup[0.44]+cmd[0.01,1.05] seconds) + py314t: OK (1.34=setup[0.44]+cmd[0.01,0.89] seconds) + congratulations :) (9.48 seconds) + +.. versionchanged:: tox≥4.25.0 + Before tox 4.25.0 dated 27 March 2025, the versions had to be specified one + by one: + + .. code-block:: toml + + [tool.tox] + envlist = [py3{10,11,12,13,14,13t,14t}] Running Tox environments in parallel ------------------------------------ @@ -221,17 +240,20 @@ other. It is also possible to run them in parallel with the ``-p`` option: .. code-block:: pytest $ uv run tox -p - py310: SKIP ⚠ in 0.09 seconds - py312: OK ✔ in 2.08 seconds - py313: OK ✔ in 2.18 seconds - py311: OK ✔ in 2.23 seconds - py39: OK ✔ in 2.91 seconds - py39: OK (2.91=setup[2.17]+cmd[0.74] seconds) - py310: SKIP (0.09 seconds) - py311: OK (2.23=setup[1.27]+cmd[0.96] seconds) - py312: OK (2.08=setup[1.22]+cmd[0.86] seconds) - py313: OK (2.18=setup[1.23]+cmd[0.95] seconds) - congratulations :) (3.05 seconds) + py311: OK ✔ in 1.7 seconds + py310: OK ✔ in 1.8 seconds + py313: OK ✔ in 1.8 seconds + py314t: OK ✔ in 1.89 seconds + py314: OK ✔ in 1.91 seconds + py313t: OK ✔ in 2.24 seconds + py310: OK (1.80=setup[0.62]+cmd[0.02,1.16] seconds) + py311: OK (1.70=setup[0.54]+cmd[0.02,1.15] seconds) + py312: OK (2.28=setup[0.58]+cmd[0.01,1.69] seconds) + py313: OK (1.80=setup[0.60]+cmd[0.02,1.18] seconds) + py314: OK (1.91=setup[0.62]+cmd[0.02,1.28] seconds) + py313t: OK (2.24=setup[0.72]+cmd[0.02,1.51] seconds) + py314t: OK (1.89=setup[0.61]+cmd[0.02,1.26] seconds) + congratulations :) (2.33 seconds) .. note:: The output is not abbreviated; this is the full output you will see if @@ -240,40 +262,50 @@ other. It is also possible to run them in parallel with the ``-p`` option: Add coverage report in tox -------------------------- -The configuration of coverage reports can easily be added to the :file:`tox.ini` -file. To do this, we need to add ``pytest-cov`` to the ``deps`` settings so that -the ``pytest-cov`` plugin is installed in the tox test environments. Including -``pytest-cov`` also includes all its dependencies, such as ``coverage``. We then -extend commands to ``pytest --cov=items``: - -.. code-block:: - :emphasize-lines: 12- - - [tox] - envlist = py3{9,10,11,12,13,13t,14,14t} - isolated_build = True - skip_missing_interpreters = True - - [testenv] - deps = - pytest>=6.0 - faker - commands = pytest - - [testenv:coverage-report] - description = Report coverage over all test runs. - deps = coverage[toml] - skip_install = true - allowlist_externals = coverage - commands = - coverage combine - coverage report +The configuration of coverage reports can easily be added to the +:file:`pyproject.toml` file. To do this, we need to add ``pytest-cov`` to the +``tests`` dependency group so that the ``pytest-cov`` plugin is also installed +in the tox test environments. Including ``pytest-cov`` also includes all other +dependencies, such as ``coverage``. We then add +:samp:`env.coverage-report.{OPTIONS}` and change ``env_run_base.commands``: + +.. code-block:: toml + :emphasize-lines: 6, 16-23, 26- + + [dependency-groups] + ... + tests = [ + "faker", + "pytest>=6", + "pytest-cov", + ] + + [tool.tox] + requires = [ "tox>=4" ] + env_list = [ + "py3{10-14}", + "py{13-14}t", + ] + skip_missing_interpreters = true + env.coverage-report.description = "Report coverage over all test runs." + env.coverage-report.deps = [ "coverage[toml]" ] + env.coverage-report.depends = [ "py" ] + env.coverage-report.skip_install = true + env.coverage-report.commands = [ + [ "coverage combine" ], + [ "coverage report" ], + ] + env_run_base.dependency_groups = [ "tests" ] + env_run_base.deps = [ "coverage[toml]" ] + env_run_base.commands = [ + [ "coverage", "run", "-m", "pytest" ], + ] When using Coverage with ``tox``, it can sometimes be useful to add a section in the :file:`pyproject.toml` file to tell Coverage which source code paths should be considered identical: -.. code-block:: ini +.. code-block:: toml [tool.coverage.paths] source = ["src", ".tox/py*/**/site-packages"] @@ -288,23 +320,20 @@ example. $ uv run tox ... - coverage-report: commands[0]> coverage combine - Combined data file .coverage.fay.local.19539.XpQXpsGx - coverage-report: commands[1]> coverage report - Name Stmts Miss Branch BrPart Cover Missing - -------------------------------------------------------------- - src/items/api.py 68 1 12 1 98% 88 - -------------------------------------------------------------- - TOTAL 428 1 32 1 99% - - 26 files skipped due to complete coverage. - py39: OK (2.12=setup[1.49]+cmd[0.63] seconds) - py310: SKIP (0.01 seconds) - py311: OK (1.41=setup[0.80]+cmd[0.62] seconds) - py312: OK (1.43=setup[0.81]+cmd[0.62] seconds) - py313: OK (1.46=setup[0.83]+cmd[0.62] seconds) - coverage-report: OK (0.16=setup[0.00]+cmd[0.07,0.09] seconds) - congratulations :) (10.26 seconds) + Name Stmts Miss Branch BrPart Cover Missing + --------------------------------------------------- + TOTAL 540 0 32 0 100% + + 33 files skipped due to complete coverage. + py310: OK (1.10=setup[0.44]+cmd[0.01,0.64] seconds) + py311: OK (0.98=setup[0.31]+cmd[0.01,0.66] seconds) + py312: OK (1.59=setup[0.34]+cmd[0.01,1.24] seconds) + py313: OK (1.06=setup[0.34]+cmd[0.01,0.71] seconds) + py314: OK (1.10=setup[0.35]+cmd[0.01,0.74] seconds) + py313t: OK (1.36=setup[0.40]+cmd[0.01,0.95] seconds) + py314t: OK (1.31=setup[0.44]+cmd[0.01,0.86] seconds) + coverage-report: OK (1.55=setup[0.37]+cmd[1.08,0.10] seconds) + congratulations :) (10.09 seconds) Set minimum coverage -------------------- @@ -335,25 +364,25 @@ Passing pytest parameters to tox We can also call individual tests with tox by making another change so that :term:`parameters ` can be passed to pytest: -.. code-block:: ini - :emphasize-lines: 17 - - [tox] - envlist = - pre-commit - docs - py3{9,10,11,12,13,13t,14,14t} - coverage-report - isolated_build = True - skip_missing_interpreters = True - - [testenv] - dependency_groups = tests - deps = - tests: coverage[toml] - allowlist_externals = coverage - commands = - coverage run -m pytest {posargs} +.. code-block:: toml + :emphasize-lines: 15 + + [tool.tox] + requires = [ "tox>=4" ] + env_list = [ + "pre-commit", + "docs", + "py3{10-14}", + "py{13-14}t", + "coverage-report", + ] + skip_missing_interpreters = true + env_run_base.dependency_groups = [ "tests" ] + env_run_base.deps = [ "coverage[toml]" ] + env_run_base.commands = [ + [ "python", "--version", "--version" ], + [ "coverage", "run", "-m", "pytest", "{posargs}" ], + ] To pass arguments to pytest, insert them between the tox arguments and the pytest arguments. In this case, we select ``test_version`` tests with the ``-k`` @@ -413,16 +442,16 @@ of environments are available for GitHub actions: if: always() steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version-file: .python-version - uses: hynek/setup-cached-uv@v2 - name: Download coverage data - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: pattern: coverage-data-* merge-multiple: true @@ -561,18 +590,21 @@ You can install ``tox`` and ``tox-uv`` with: ``uv.lock`` support ~~~~~~~~~~~~~~~~~~~ -If you want to use ``uv sync`` with a ``uv.lock`` file for a Tox environment, -you must change the runner for this Tox environment to ``uv-venv-lock-runner``. -You should also use the dependency_groups configuration in such environments -to instruct ``uv`` to install the specified dependency group, for example: +If you want to use ``uv sync`` with a :file:`uv.lock` file for a Tox +environment, you must change the runner for this Tox environment to +``uv-venv-lock-runner``, for example: + +.. code-block:: toml + :caption: pyproject.toml -.. code-block:: ini - :caption: tox.ini + env.app.dependency_groups = [ "tests" ] + env.app.runner = "uv-venv-lock-runner" + commands = [[ "pytest"]] - [testenv] - runner = uv-venv-lock-runner - dependency_groups = dev - commands = pytest +The ``app`` environment uses the ``uv-venv-lock-runner`` and utilises ``uv sync +--locked`` to install the dependencies in the versions specified in the +:file:`uv.lock` file. -``dev`` uses the ``uv-venv-lock-runner`` and uses ``uv sync`` to install -dependencies in the environment with the ``dev`` dependency group. +.. seealso:: + * `uv.lock support + `_ diff --git a/docs/types/dicts.rst b/docs/types/dicts.rst index b5598f1d..ce4886d8 100644 --- a/docs/types/dicts.rst +++ b/docs/types/dicts.rst @@ -13,12 +13,12 @@ Values can be any type of object, including mutable types such as .. code-block:: pycon - >>> dict = { + >>> timeseries = { ... "2022-01-31": -0.751442, ... "2022-02-01": 0.816935, ... "2022-02-02": -0.272546, ... } - >>> dict["2022-02-03"] = -0.268295 + >>> timeseries["2022-02-03"] = -0.268295 If you try to access the value of a key that is not contained in the dictionary, a ``KeyError`` :doc:`/control-flow/exceptions` is thrown. To avoid this error, @@ -27,17 +27,17 @@ is not contained in a dictionary. .. code-block:: pycon - >>> dict["2022-02-03"] + >>> timeseries["2022-02-03"] -0.268295 - >>> dict["2022-02-04"] + >>> timeseries["2022-02-04"] Traceback (most recent call last): File "", line 1, in - dict["2022-02-04"] + timeseries["2022-02-04"] ~~~~^^^^^^^^^^^^^^ KeyError: '2022-02-04' - >>> dict.get("2022-02-03", "Messwert nicht vorhanden") + >>> timeseries.get("2022-02-03", "Messwert nicht vorhanden") -0.268295 - >>> dict.get("2022-02-04", "Messwert nicht vorhanden") + >>> timeseries.get("2022-02-04", "Messwert nicht vorhanden") 'Messwert nicht vorhanden' Other Dict methods @@ -58,7 +58,7 @@ that they become a list in these examples: .. code-block:: pycon - >>> list(dict.keys()) + >>> list(timeseries.keys()) ['2022-01-31', '2022-02-01', '2022-02-02', '2022-02-03'] As of Python 3.6, dictionaries retain the order in which the keys were created, @@ -135,8 +135,8 @@ Checks ``("Veit", "Tim", "Monique")`` * You can use a :doc:`dictionary ` and use it like a spreadsheet - sheet by using :doc:`tuples ` as key row and - column values. Write sample code to add and retrieve values. + by using :doc:`tuples ` as key row and column + values. Write sample code to add and retrieve values. * How can you remove all duplicates from a list without changing the order of the elements in the list? diff --git a/docs/types/sequences-sets/sets.rst b/docs/types/sequences-sets/sets.rst index e1dadab3..0c7c3a29 100644 --- a/docs/types/sequences-sets/sets.rst +++ b/docs/types/sequences-sets/sets.rst @@ -88,6 +88,8 @@ Difference or remainder set >>> x.difference(y) {1, 2} + >>> y.difference(x) + {5} .. _frozenset: diff --git a/docs/types/sequences-sets/tuples.rst b/docs/types/sequences-sets/tuples.rst index 94f19a57..5bdf1d27 100644 --- a/docs/types/sequences-sets/tuples.rst +++ b/docs/types/sequences-sets/tuples.rst @@ -102,7 +102,7 @@ the right-hand side of the assignment operator. Here is a simple example: .. code-block:: pycon - >>> (v, w, x, y, z) = (1, "2.", 3.0, ["4a", "4b"], (5.1, 5.2)) + >>> v, w, x, y, z = (1, "2.", 3.0, ["4a", "4b"], (5.1, 5.2)) >>> v 1 >>> w @@ -146,24 +146,34 @@ A list can be converted into a tuple using the built-in ``tuple`` function: .. code-block:: pycon - >>> x = [1, 2, 3, 5] - >>> tuple(x) - (1, 2, 3, 5) + >>> dates = [ + ... "2025-11-09", + ... "2025-11-10", + ... "2025-11-11", + ... "2025-11-12", + ... ] + >>> tuple(dates) + ('2025-11-09', '2025-11-10', '2025-11-11', '2025-11-12') Conversely, a tuple can be converted into a list using the built-in ``list`` function: .. code-block:: pycon - >>> x = (1, 2, 3, 4) - >>> list(x) - [1, 2, 3, 4] + >>> dates = ( + ... "2025-11-09", + ... "2025-11-10", + ... "2025-11-11", + ... "2025-11-12", + ... ) + >>> list(dates) + ['2025-11-09', '2025-11-10', '2025-11-11', '2025-11-12'] The advantages of tuples over :doc:`lists ` are: * Tuples are faster than lists. - If you want to define a constant set of values and just cycle through them, + If you want to define a constant set of values and just iterate through them, you should use a tuple instead of a list. * Tuples can not be modified and are therefore *write-protected*. diff --git a/docs/types/strings/encodings.rst b/docs/types/strings/encodings.rst index 4366d2c0..8e58ff86 100644 --- a/docs/types/strings/encodings.rst +++ b/docs/types/strings/encodings.rst @@ -30,11 +30,11 @@ Here are other characters you can get with the escape character: +--------------------------+--------------------------+--------------------------+ | ``\t`` | | Tabulator (``TAB``) | +--------------------------+--------------------------+--------------------------+ -| :samp:`\u{00B5}` | ``µ`` | Unicode 16 bit | +| :samp:`\\u{00B5}` | ``µ`` | Unicode 16 bit | +--------------------------+--------------------------+--------------------------+ -| :samp:`\U{000000B5}` | ``µ`` | Unicode 32 bit | +| :samp:`\\U{000000B5}` | ``µ`` | Unicode 32 bit | +--------------------------+--------------------------+--------------------------+ -| :samp:`\N{{SNAKE}}` | ``🐍`` | Unicode Emoji name | +| :samp:`\\N{{SNAKE}}` | ``🐍`` | Unicode Emoji name | +--------------------------+--------------------------+--------------------------+ Lines 1–7 @@ -111,6 +111,23 @@ defines several different encodings from a single character set. UTF-8 is an encoding scheme for representing Unicode characters as binary data with one or more bytes per character. +.. versionadded:: 3.15 + Python 3.15 uses UTF-8 as the default encoding, regardless of the system + environment. This means that I/O operations without explicit encoding, for + example :samp:`open("{EXAMPLE.TXT}")`, use UTF-8. This only applies if no + encoding is specified. + + To ensure compatibility between different Python versions, an explicit + encoding should always be specified. Opt-in :ref:`io-encoding-warning` can be + used to identify code that may be affected by this change. + + To keeep the previous behaviour, Python’s UTF-8 mode can be disabled with the + environment variable ``PYTHONUTF8=0`` or the command line option ``-X + utf8=0``. + + .. seealso:: + :pep:`686` + Encoding and decoding --------------------- diff --git a/docs/types/strings/index.rst b/docs/types/strings/index.rst index 503ab22e..d0afe714 100644 --- a/docs/types/strings/index.rst +++ b/docs/types/strings/index.rst @@ -57,13 +57,6 @@ marks without backslashes ``\`` as escape characters. Checks ------ -* What use cases can you imagine in which the :mod:`python3:struct` module would - be useful for reading or writing binary data? - - * when reading and writing a binary file - * when reading from an external interface, where the data should be stored - exactly as it was transmitted - * Which regular expression would you use to find strings that represent the numbers between -3 and +3? diff --git a/docs/types/strings/print.rst b/docs/types/strings/print.rst index 31ef962f..2d131195 100644 --- a/docs/types/strings/print.rst +++ b/docs/types/strings/print.rst @@ -205,18 +205,42 @@ F-string: >>> print(f"My name is {uid.capitalize()=}") My name is uid.capitalize()='Veit' -Formatting date and time formats and IP addresses -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Formatting date and time formats +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :py:mod:`datetime` supports the formatting of strings using the same syntax as -the :py:meth:`strftime ` method for these objects. +the :py:meth:`datetime.strftime ` method for these +objects. .. code-block:: pycon >>> import datetime + >>> import locale + >>> locale.setlocale(locale.LC_TIME, "de_DE.utf-8") + ... "de_DE.utf-8" + 'de_DE.utf-8' >>> today = datetime.date.today() - >>> print(f"Today is {today:%d %B %Y}.") - Today is 26 November 2023. + >>> print(f"Heute ist {today:%A, %d. %B %Y}.") + Heute ist Freitag, 11. Juli 2025. + +Conversely, you can also use :meth:`datetime.strptime +` to convert strings into ``datetime`` objects: + +.. code-block:: pycon + + >>> today_string = "Fri, 11 Jul 2025 18:46:49" + >>> today = datetime.datetime.strptime(today_string, "%A, %d. %B %Y") + +.. csv-table:: Häufige Formatierungen + :header: "Beschreibung", "Beispiel", "Format" + + ISO 8601, "2025-07-11T18:46:49", "%Y-%m-%dT%H:%M:%S" + ISO 8601 with time zone, "2025-07-11T18:46:49+0100", "%Y-%m-%dT%H:%M:%S%z" + RFC 2822, "Fr, 11 Jul 2025 18:46:49", "%a, %d %b %Y %H:%M:%S" + RFC 3339 with time zone, "2025-07-11 18:46:49+0100", "%Y-%m-%d %H:%M:%S%z" + +Formatting IP addresses +~~~~~~~~~~~~~~~~~~~~~~~ The :py:mod:`ipaddress` module of Python also supports the formatting of ``IPv4Address`` and ``IPv6Address`` objects. diff --git a/pyproject.toml b/pyproject.toml index 40140c56..347b5aba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,18 +8,17 @@ license-files = [ "LICENSE" ] authors = [ { name = "Veit Schiele", email = "veit@cusy.io" }, ] -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] -dependencies = [ ] - +dependencies = [] urls."Bug Tracker" = "https://github.com/veit/python-basics-tutorial/issues" urls."Homepage" = "https://github.com/veit/python-basics-tutorial/" @@ -37,6 +36,7 @@ docs = [ "matplotlib", # matplotlib is required for social cards "nbsphinx", "pygments-pytest", + "sphinx<8.2", # furo requires sphinx < 8.2 "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-lint", @@ -48,4 +48,4 @@ docs = [ ] [tool.codespell] -ignore-words-list = "ist, symbl, allo" +ignore-words-list = "allo, ist, Juli, symbl"