Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-pytest for openSUSE:Factory 
checked in at 2023-09-17 19:28:40
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-pytest (Old)
 and      /work/SRC/openSUSE:Factory/.python-pytest.new.1766 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-pytest"

Sun Sep 17 19:28:40 2023 rev:79 rq:1111052 version:7.4.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-pytest/python-pytest.changes      
2023-07-27 16:50:59.285875150 +0200
+++ /work/SRC/openSUSE:Factory/.python-pytest.new.1766/python-pytest.changes    
2023-09-17 19:28:41.636288656 +0200
@@ -1,0 +2,15 @@
+Thu Sep  7 08:50:26 UTC 2023 - Dirk Müller <dmuel...@suse.com>
+
+- update to 7.4.1:
+  * Fixed bug where fake intermediate
+    modules generated by ``--import-mode=importlib`` would not
+    include the child modules as attributes of the parent modules.
+  * Fixed error assertion handling in
+    :func:`pytest.approx` when ``None`` is an expected or
+    received value when comparing dictionaries.
+  * Fixed issue when using
+    ``--import-mode=importlib`` together with ``--doctest-
+    modules`` that caused modules to be imported more than once,
+    causing problems with modules that have import side effects.
+
+-------------------------------------------------------------------

Old:
----
  pytest-7.4.0.tar.gz

New:
----
  pytest-7.4.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-pytest.spec ++++++
--- /var/tmp/diff_new_pack.KnkbY3/_old  2023-09-17 19:28:44.272382608 +0200
+++ /var/tmp/diff_new_pack.KnkbY3/_new  2023-09-17 19:28:44.276382750 +0200
@@ -33,7 +33,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-pytest%{psuffix}
-Version:        7.4.0
+Version:        7.4.1
 Release:        0
 Summary:        Simple powerful testing with Python
 License:        MIT

++++++ pytest-7.4.0.tar.gz -> pytest-7.4.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/.github/workflows/deploy.yml 
new/pytest-7.4.1/.github/workflows/deploy.yml
--- old/pytest-7.4.0/.github/workflows/deploy.yml       2023-06-23 
13:16:47.000000000 +0200
+++ new/pytest-7.4.1/.github/workflows/deploy.yml       2023-09-02 
16:59:58.000000000 +0200
@@ -1,26 +1,23 @@
 name: deploy
 
 on:
-  push:
-    tags:
-      # These tags are protected, see:
-      # https://github.com/pytest-dev/pytest/settings/tag_protection
-      - "[0-9]+.[0-9]+.[0-9]+"
-      - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
+  workflow_dispatch:
+    inputs:
+      version:
+        description: 'Release version'
+        required: true
+        default: '1.2.3'
 
 
 # Set permissions at the job level.
 permissions: {}
 
 jobs:
-
-  deploy:
-    if: github.repository == 'pytest-dev/pytest'
-
+  package:
     runs-on: ubuntu-latest
-    timeout-minutes: 30
-    permissions:
-      contents: write
+    env:
+      SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
+    timeout-minutes: 10
 
     steps:
     - uses: actions/checkout@v3
@@ -31,6 +28,15 @@
     - name: Build and Check Package
       uses: hynek/build-and-inspect-python-package@v1.5
 
+  deploy:
+    if: github.repository == 'pytest-dev/pytest'
+    needs: [package]
+    runs-on: ubuntu-latest
+    environment: deploy
+    timeout-minutes: 30
+    permissions:
+      id-token: write
+    steps:
     - name: Download Package
       uses: actions/download-artifact@v3
       with:
@@ -38,14 +44,35 @@
         path: dist
 
     - name: Publish package to PyPI
-      uses: pypa/gh-action-pypi-publish@release/v1
+      uses: pypa/gh-action-pypi-publish@v1.8.5
+
+    - name: Push tag
+      run: |
+        git config user.name "pytest bot"
+        git config user.email "pytest...@gmail.com"
+        git tag --annotate --message=v${{ github.event.inputs.version }} v${{ 
github.event.inputs.version }} ${{ github.sha }}
+        git push origin v${{ github.event.inputs.version }}
+
+  release-notes:
+
+    # todo: generate the content in the build  job
+    #       the goal being of using a github action script to push the release 
data
+    #       after success instead of creating a complete python/tox env
+    needs: [deploy]
+    runs-on: ubuntu-latest
+    timeout-minutes: 30
+    permissions:
+      contents: write
+    steps:
+    - uses: actions/checkout@v3
       with:
-        password: ${{ secrets.pypi_token }}
+        fetch-depth: 0
+        persist-credentials: false
 
     - name: Set up Python
       uses: actions/setup-python@v4
       with:
-        python-version: "3.7"
+        python-version: "3.10"
 
     - name: Install tox
       run: |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/.github/workflows/test.yml 
new/pytest-7.4.1/.github/workflows/test.yml
--- old/pytest-7.4.0/.github/workflows/test.yml 2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/.github/workflows/test.yml 2023-09-02 16:59:58.000000000 
+0200
@@ -27,7 +27,19 @@
 permissions: {}
 
 jobs:
+  package:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+      with:
+        fetch-depth: 0
+        persist-credentials: false
+    - name: Build and Check Package
+      uses: hynek/build-and-inspect-python-package@v1.5
+
   build:
+    needs: [package]
+
     runs-on: ${{ matrix.os }}
     timeout-minutes: 45
     permissions:
@@ -38,17 +50,17 @@
       matrix:
         name: [
           "windows-py37",
-          "windows-py37-pluggy",
           "windows-py38",
+          "windows-py38-pluggy",
           "windows-py39",
           "windows-py310",
           "windows-py311",
           "windows-py312",
 
           "ubuntu-py37",
-          "ubuntu-py37-pluggy",
           "ubuntu-py37-freeze",
           "ubuntu-py38",
+          "ubuntu-py38-pluggy",
           "ubuntu-py39",
           "ubuntu-py310",
           "ubuntu-py311",
@@ -60,7 +72,6 @@
           "macos-py310",
           "macos-py312",
 
-          "docs",
           "doctesting",
           "plugins",
         ]
@@ -70,15 +81,15 @@
             python: "3.7"
             os: windows-latest
             tox_env: "py37-numpy"
-          - name: "windows-py37-pluggy"
-            python: "3.7"
-            os: windows-latest
-            tox_env: "py37-pluggymain-pylib-xdist"
           - name: "windows-py38"
             python: "3.8"
             os: windows-latest
             tox_env: "py38-unittestextras"
             use_coverage: true
+          - name: "windows-py38-pluggy"
+            python: "3.8"
+            os: windows-latest
+            tox_env: "py38-pluggymain-pylib-xdist"
           - name: "windows-py39"
             python: "3.9"
             os: windows-latest
@@ -101,10 +112,6 @@
             os: ubuntu-latest
             tox_env: "py37-lsof-numpy-pexpect"
             use_coverage: true
-          - name: "ubuntu-py37-pluggy"
-            python: "3.7"
-            os: ubuntu-latest
-            tox_env: "py37-pluggymain-pylib-xdist"
           - name: "ubuntu-py37-freeze"
             python: "3.7"
             os: ubuntu-latest
@@ -113,6 +120,10 @@
             python: "3.8"
             os: ubuntu-latest
             tox_env: "py38-xdist"
+          - name: "ubuntu-py38-pluggy"
+            python: "3.8"
+            os: ubuntu-latest
+            tox_env: "py38-pluggymain-pylib-xdist"
           - name: "ubuntu-py39"
             python: "3.9"
             os: ubuntu-latest
@@ -159,10 +170,6 @@
             os: ubuntu-latest
             tox_env: "plugins"
 
-          - name: "docs"
-            python: "3.7"
-            os: ubuntu-latest
-            tox_env: "docs"
           - name: "doctesting"
             python: "3.7"
             os: ubuntu-latest
@@ -175,6 +182,12 @@
         fetch-depth: 0
         persist-credentials: false
 
+    - name: Download Package
+      uses: actions/download-artifact@v3
+      with:
+        name: Packages
+        path: dist
+
     - name: Set up Python ${{ matrix.python }}
       uses: actions/setup-python@v4
       with:
@@ -188,11 +201,13 @@
 
     - name: Test without coverage
       if: "! matrix.use_coverage"
-      run: "tox -e ${{ matrix.tox_env }}"
+      shell: bash
+      run: tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz`
 
     - name: Test with coverage
       if: "matrix.use_coverage"
-      run: "tox -e ${{ matrix.tox_env }}-coverage"
+      shell: bash
+      run: tox run -e ${{ matrix.tox_env }}-coverage --installpkg `find 
dist/*.tar.gz`
 
     - name: Generate coverage report
       if: "matrix.use_coverage"
@@ -206,10 +221,3 @@
         fail_ci_if_error: true
         files: ./coverage.xml
         verbose: true
-
-  check-package:
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/checkout@v3
-    - name: Build and Check Package
-      uses: hynek/build-and-inspect-python-package@v1.5
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/CONTRIBUTING.rst 
new/pytest-7.4.1/CONTRIBUTING.rst
--- old/pytest-7.4.0/CONTRIBUTING.rst   2023-06-23 13:16:47.000000000 +0200
+++ new/pytest-7.4.1/CONTRIBUTING.rst   2023-09-02 16:59:58.000000000 +0200
@@ -50,7 +50,7 @@
 --------
 
 Look through the `GitHub issues for bugs 
<https://github.com/pytest-dev/pytest/labels/type:%20bug>`_.
-See also the `"status: easy" issues 
<https://github.com/pytest-dev/pytest/labels/status%3A%20easy>`_
+See also the `"good first issue" issues 
<https://github.com/pytest-dev/pytest/labels/good%20first%20issue>`_
 that are friendly to new contributors.
 
 :ref:`Talk <contact>` to developers to find out how you can fix specific bugs. 
To indicate that you are going
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/PKG-INFO new/pytest-7.4.1/PKG-INFO
--- old/pytest-7.4.0/PKG-INFO   2023-06-23 13:17:10.120871300 +0200
+++ new/pytest-7.4.1/PKG-INFO   2023-09-02 17:00:18.888728000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: pytest
-Version: 7.4.0
+Version: 7.4.1
 Summary: pytest: simple powerful testing with Python
 Home-page: https://docs.pytest.org/en/latest/
 Author: Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, 
Brianna Laugher, Florian Bruhin and others
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/RELEASING.rst 
new/pytest-7.4.1/RELEASING.rst
--- old/pytest-7.4.0/RELEASING.rst      2023-06-23 13:16:47.000000000 +0200
+++ new/pytest-7.4.1/RELEASING.rst      2023-09-02 16:59:58.000000000 +0200
@@ -133,14 +133,11 @@
 
 Both automatic and manual processes described above follow the same steps from 
this point onward.
 
-#. After all tests pass and the PR has been approved, tag the release commit
-   in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish 
to PyPI::
+#. After all tests pass and the PR has been approved, trigger the ``deploy`` 
job
+   in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml.
 
-     git fetch upstream
-     git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
-     git push upstream MAJOR.MINOR.PATCH
-
-   Wait for the deploy to complete, then make sure it is `available on PyPI 
<https://pypi.org/project/pytest>`_.
+   This job will require approval from ``pytest-dev/core``, after which it 
will publish to PyPI
+   and tag the repository.
 
 #. Merge the PR. **Make sure it's not squash-merged**, so that the tagged 
commit ends up in the main branch.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/announce/index.rst 
new/pytest-7.4.1/doc/en/announce/index.rst
--- old/pytest-7.4.0/doc/en/announce/index.rst  2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/doc/en/announce/index.rst  2023-09-02 16:59:58.000000000 
+0200
@@ -6,6 +6,7 @@
    :maxdepth: 2
 
 
+   release-7.4.1
    release-7.4.0
    release-7.3.2
    release-7.3.1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/announce/release-7.4.1.rst 
new/pytest-7.4.1/doc/en/announce/release-7.4.1.rst
--- old/pytest-7.4.0/doc/en/announce/release-7.4.1.rst  1970-01-01 
01:00:00.000000000 +0100
+++ new/pytest-7.4.1/doc/en/announce/release-7.4.1.rst  2023-09-02 
16:59:58.000000000 +0200
@@ -0,0 +1,20 @@
+pytest-7.4.1
+=======================================
+
+pytest 7.4.1 has just been released to PyPI.
+
+This is a bug-fix release, being a drop-in replacement. To upgrade::
+
+  pip install --upgrade pytest
+
+The full changelog is available at 
https://docs.pytest.org/en/stable/changelog.html.
+
+Thanks to all of the contributors to this release:
+
+* Bruno Oliveira
+* Florian Bruhin
+* Ran Benita
+
+
+Happy testing,
+The pytest Development Team
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/builtin.rst 
new/pytest-7.4.1/doc/en/builtin.rst
--- old/pytest-7.4.0/doc/en/builtin.rst 2023-06-23 13:16:47.000000000 +0200
+++ new/pytest-7.4.1/doc/en/builtin.rst 2023-09-02 16:59:58.000000000 +0200
@@ -22,7 +22,7 @@
     cachedir: .pytest_cache
     rootdir: /home/sweet/project
     collected 0 items
-    cache -- .../_pytest/cacheprovider.py:528
+    cache -- .../_pytest/cacheprovider.py:532
         Return a cache object that can persist state between testing sessions.
 
         cache.get(key, default)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/changelog.rst 
new/pytest-7.4.1/doc/en/changelog.rst
--- old/pytest-7.4.0/doc/en/changelog.rst       2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/doc/en/changelog.rst       2023-09-02 16:59:58.000000000 
+0200
@@ -28,6 +28,23 @@
 
 .. towncrier release notes start
 
+pytest 7.4.1 (2023-09-02)
+=========================
+
+Bug Fixes
+---------
+
+- `#10337 <https://github.com/pytest-dev/pytest/issues/10337>`_: Fixed bug 
where fake intermediate modules generated by ``--import-mode=importlib`` would 
not include the
+  child modules as attributes of the parent modules.
+
+
+- `#10702 <https://github.com/pytest-dev/pytest/issues/10702>`_: Fixed error 
assertion handling in :func:`pytest.approx` when ``None`` is an expected or 
received value when comparing dictionaries.
+
+
+- `#10811 <https://github.com/pytest-dev/pytest/issues/10811>`_: Fixed issue 
when using ``--import-mode=importlib`` together with ``--doctest-modules`` that 
caused modules
+  to be imported more than once, causing problems with modules that have 
import side effects.
+
+
 pytest 7.4.0 (2023-06-23)
 =========================
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/example/nonpython/conftest.py 
new/pytest-7.4.1/doc/en/example/nonpython/conftest.py
--- old/pytest-7.4.0/doc/en/example/nonpython/conftest.py       2023-06-23 
13:16:47.000000000 +0200
+++ new/pytest-7.4.1/doc/en/example/nonpython/conftest.py       2023-09-02 
16:59:58.000000000 +0200
@@ -12,7 +12,7 @@
         # We need a yaml parser, e.g. PyYAML.
         import yaml
 
-        raw = yaml.safe_load(self.path.open())
+        raw = yaml.safe_load(self.path.open(encoding="utf-8"))
         for name, spec in sorted(raw.items()):
             yield YamlItem.from_parent(self, name=name, spec=spec)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/example/reportingdemo.rst 
new/pytest-7.4.1/doc/en/example/reportingdemo.rst
--- old/pytest-7.4.0/doc/en/example/reportingdemo.rst   2023-06-23 
13:16:47.000000000 +0200
+++ new/pytest-7.4.1/doc/en/example/reportingdemo.rst   2023-09-02 
16:59:58.000000000 +0200
@@ -554,13 +554,13 @@
     E       AssertionError: assert False
     E        +  where False = <built-in method startswith of str object at 
0xdeadbeef0027>('456')
     E        +    where <built-in method startswith of str object at 
0xdeadbeef0027> = '123'.startswith
-    E        +      where '123' = <function 
TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0029>()
-    E        +    and   '456' = <function 
TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef002a>()
+    E        +      where '123' = <function 
TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0006>()
+    E        +    and   '456' = <function 
TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef0029>()
 
     failure_demo.py:235: AssertionError
     _____________________ TestMoreErrors.test_global_func 
______________________
 
-    self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
+    self = <failure_demo.TestMoreErrors object at 0xdeadbeef002a>
 
         def test_global_func(self):
     >       assert isinstance(globf(42), float)
@@ -571,18 +571,18 @@
     failure_demo.py:238: AssertionError
     _______________________ TestMoreErrors.test_instance 
_______________________
 
-    self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
+    self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
 
         def test_instance(self):
             self.x = 6 * 7
     >       assert self.x != 42
     E       assert 42 != 42
-    E        +  where 42 = <failure_demo.TestMoreErrors object at 
0xdeadbeef002c>.x
+    E        +  where 42 = <failure_demo.TestMoreErrors object at 
0xdeadbeef002b>.x
 
     failure_demo.py:242: AssertionError
     _______________________ TestMoreErrors.test_compare 
________________________
 
-    self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
+    self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
 
         def test_compare(self):
     >       assert globf(10) < 5
@@ -592,7 +592,7 @@
     failure_demo.py:245: AssertionError
     _____________________ TestMoreErrors.test_try_finally 
______________________
 
-    self = <failure_demo.TestMoreErrors object at 0xdeadbeef002e>
+    self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
 
         def test_try_finally(self):
             x = 1
@@ -603,7 +603,7 @@
     failure_demo.py:250: AssertionError
     ___________________ TestCustomAssertMsg.test_single_line 
___________________
 
-    self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
+    self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002e>
 
         def test_single_line(self):
             class A:
@@ -618,7 +618,7 @@
     failure_demo.py:261: AssertionError
     ____________________ TestCustomAssertMsg.test_multiline 
____________________
 
-    self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
+    self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
 
         def test_multiline(self):
             class A:
@@ -637,7 +637,7 @@
     failure_demo.py:268: AssertionError
     ___________________ TestCustomAssertMsg.test_custom_repr 
___________________
 
-    self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0031>
+    self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
 
         def test_custom_repr(self):
             class JSON:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/example/simple.rst 
new/pytest-7.4.1/doc/en/example/simple.rst
--- old/pytest-7.4.0/doc/en/example/simple.rst  2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/doc/en/example/simple.rst  2023-09-02 16:59:58.000000000 
+0200
@@ -817,7 +817,7 @@
         # we only look at actual failing test calls, not setup/teardown
         if rep.when == "call" and rep.failed:
             mode = "a" if os.path.exists("failures") else "w"
-            with open("failures", mode) as f:
+            with open("failures", mode, encoding="utf-8") as f:
                 # let's also access a fixture for the fun of it
                 if "tmp_path" in item.fixturenames:
                     extra = " ({})".format(item.funcargs["tmp_path"])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/getting-started.rst 
new/pytest-7.4.1/doc/en/getting-started.rst
--- old/pytest-7.4.0/doc/en/getting-started.rst 2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/doc/en/getting-started.rst 2023-09-02 16:59:58.000000000 
+0200
@@ -22,7 +22,7 @@
 .. code-block:: bash
 
     $ pytest --version
-    pytest 7.4.0
+    pytest 7.4.1
 
 .. _`simpletest`:
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/how-to/assert.rst 
new/pytest-7.4.1/doc/en/how-to/assert.rst
--- old/pytest-7.4.0/doc/en/how-to/assert.rst   2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/doc/en/how-to/assert.rst   2023-09-02 16:59:58.000000000 
+0200
@@ -54,14 +54,13 @@
 idiomatic python constructs without boilerplate code while not losing
 introspection information.
 
-However, if you specify a message with the assertion like this:
+If a message is specified with the assertion like this:
 
 .. code-block:: python
 
     assert a % 2 == 0, "value was odd, should be even"
 
-then no assertion introspection takes places at all and the message
-will be simply shown in the traceback.
+it is printed alongside the assertion introspection in the traceback.
 
 See :ref:`assert-details` for more information on assertion introspection.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/how-to/cache.rst 
new/pytest-7.4.1/doc/en/how-to/cache.rst
--- old/pytest-7.4.0/doc/en/how-to/cache.rst    2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/doc/en/how-to/cache.rst    2023-09-02 16:59:58.000000000 
+0200
@@ -176,14 +176,21 @@
 Behavior when no tests failed in the last run
 ---------------------------------------------
 
-When no tests failed in the last run, or when no cached ``lastfailed`` data was
-found, ``pytest`` can be configured either to run all of the tests or no tests,
-using the ``--last-failed-no-failures`` option, which takes one of the 
following values:
+The ``--lfnf/--last-failed-no-failures`` option governs the behavior of 
``--last-failed``.
+Determines whether to execute tests when there are no previously (known)
+failures or when no cached ``lastfailed`` data was found.
+
+There are two options:
+
+* ``all``:  when there are no known test failures, runs all tests (the full 
test suite). This is the default.
+* ``none``: when there are no known test failures, just emits a message 
stating this and exit successfully.
+
+Example:
 
 .. code-block:: bash
 
-    pytest --last-failed --last-failed-no-failures all    # run all tests 
(default behavior)
-    pytest --last-failed --last-failed-no-failures none   # run no tests and 
exit
+    pytest --last-failed --last-failed-no-failures all    # runs the full test 
suite (default behavior)
+    pytest --last-failed --last-failed-no-failures none   # runs no tests and 
exits successfully
 
 The new config.cache object
 --------------------------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/how-to/fixtures.rst 
new/pytest-7.4.1/doc/en/how-to/fixtures.rst
--- old/pytest-7.4.0/doc/en/how-to/fixtures.rst 2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/doc/en/how-to/fixtures.rst 2023-09-02 16:59:58.000000000 
+0200
@@ -1698,7 +1698,7 @@
     class TestDirectoryInit:
         def test_cwd_starts_empty(self):
             assert os.listdir(os.getcwd()) == []
-            with open("myfile", "w") as f:
+            with open("myfile", "w", encoding="utf-8") as f:
                 f.write("hello")
 
         def test_cwd_again_starts_empty(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/how-to/tmp_path.rst 
new/pytest-7.4.1/doc/en/how-to/tmp_path.rst
--- old/pytest-7.4.0/doc/en/how-to/tmp_path.rst 2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/doc/en/how-to/tmp_path.rst 2023-09-02 16:59:58.000000000 
+0200
@@ -24,8 +24,8 @@
         d = tmp_path / "sub"
         d.mkdir()
         p = d / "hello.txt"
-        p.write_text(CONTENT)
-        assert p.read_text() == CONTENT
+        p.write_text(CONTENT, encoding="utf-8")
+        assert p.read_text(encoding="utf-8") == CONTENT
         assert len(list(tmp_path.iterdir())) == 1
         assert 0
 
@@ -51,8 +51,8 @@
             d = tmp_path / "sub"
             d.mkdir()
             p = d / "hello.txt"
-            p.write_text(CONTENT)
-            assert p.read_text() == CONTENT
+            p.write_text(CONTENT, encoding="utf-8")
+            assert p.read_text(encoding="utf-8") == CONTENT
             assert len(list(tmp_path.iterdir())) == 1
     >       assert 0
     E       assert 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/how-to/unittest.rst 
new/pytest-7.4.1/doc/en/how-to/unittest.rst
--- old/pytest-7.4.0/doc/en/how-to/unittest.rst 2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/doc/en/how-to/unittest.rst 2023-09-02 16:59:58.000000000 
+0200
@@ -207,10 +207,10 @@
         @pytest.fixture(autouse=True)
         def initdir(self, tmp_path, monkeypatch):
             monkeypatch.chdir(tmp_path)  # change to pytest-provided temporary 
directory
-            tmp_path.joinpath("samplefile.ini").write_text("# testdata")
+            tmp_path.joinpath("samplefile.ini").write_text("# testdata", 
encoding="utf-8")
 
         def test_method(self):
-            with open("samplefile.ini") as f:
+            with open("samplefile.ini", encoding="utf-8") as f:
                 s = f.read()
             assert "testdata" in s
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/how-to/usage.rst 
new/pytest-7.4.1/doc/en/how-to/usage.rst
--- old/pytest-7.4.0/doc/en/how-to/usage.rst    2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/doc/en/how-to/usage.rst    2023-09-02 16:59:58.000000000 
+0200
@@ -173,7 +173,8 @@
 
 this acts as if you would call "pytest" from the command line.
 It will not raise :class:`SystemExit` but return the :ref:`exit code 
<exit-codes>` instead.
-You can pass in options and arguments:
+If you don't pass it any arguments, ``main`` reads the arguments from the 
command line arguments of the process (:data:`sys.argv`), which may be 
undesirable.
+You can pass in options and arguments explicitly:
 
 .. code-block:: python
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/index.rst 
new/pytest-7.4.1/doc/en/index.rst
--- old/pytest-7.4.0/doc/en/index.rst   2023-06-23 13:16:47.000000000 +0200
+++ new/pytest-7.4.1/doc/en/index.rst   2023-09-02 16:59:58.000000000 +0200
@@ -2,8 +2,8 @@
 
 .. sidebar:: Next Open Trainings
 
-   - `pytest tips and tricks for a better testsuite 
<https://ep2023.europython.eu/session/pytest-tips-and-tricks-for-a-better-testsuite>`_,
 at `Europython 2023 <https://ep2023.europython.eu/>`_, July 18th (3h), 
Prague/Remote
-   - `Professional Testing with Python 
<https://python-academy.com/courses/python_course_testing.html>`_, via `Python 
Academy <https://www.python-academy.com/>`_, March 5th to 7th 2024 (3 day 
in-depth training), Leipzig/Remote
+   - `pytest: Professionelles Testen (nicht nur) für Python 
<https://workshoptage.ch/workshops/2023/pytest-professionelles-testen-nicht-nur-fuer-python-2/>`_,
 at `Workshoptage 2023 <https://workshoptage.ch/>`_, **September 5th**, `OST 
<https://www.ost.ch/en>`_ Campus **Rapperswil, Switzerland**
+   - `Professional Testing with Python 
<https://python-academy.com/courses/python_course_testing.html>`_, via `Python 
Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day 
in-depth training), **Leipzig, Germany / Remote**
 
    Also see :doc:`previous talks and blogposts <talks>`.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/doc/en/reference/reference.rst 
new/pytest-7.4.1/doc/en/reference/reference.rst
--- old/pytest-7.4.0/doc/en/reference/reference.rst     2023-06-23 
13:16:47.000000000 +0200
+++ new/pytest-7.4.1/doc/en/reference/reference.rst     2023-09-02 
16:59:58.000000000 +0200
@@ -82,6 +82,8 @@
 pytest.main
 ~~~~~~~~~~~
 
+**Tutorial**: :ref:`pytest.main-usage`
+
 .. autofunction:: pytest.main
 
 pytest.param
@@ -783,26 +785,18 @@
 .. autofunction:: pytest_leave_pdb
 
 
-Objects
--------
+Collection tree objects
+-----------------------
 
-Full reference to objects accessible from :ref:`fixtures <fixture>` or 
:ref:`hooks <hook-reference>`.
+These are the collector and item classes (collectively called "nodes") which
+make up the collection tree.
 
+Node
+~~~~
 
-CallInfo
-~~~~~~~~
-
-.. autoclass:: pytest.CallInfo()
+.. autoclass:: _pytest.nodes.Node()
     :members:
 
-
-Class
-~~~~~
-
-.. autoclass:: pytest.Class()
-    :members:
-    :show-inheritance:
-
 Collector
 ~~~~~~~~~
 
@@ -810,52 +804,52 @@
     :members:
     :show-inheritance:
 
-CollectReport
-~~~~~~~~~~~~~
+Item
+~~~~
 
-.. autoclass:: pytest.CollectReport()
+.. autoclass:: pytest.Item()
     :members:
     :show-inheritance:
-    :inherited-members:
 
-Config
-~~~~~~
+File
+~~~~
 
-.. autoclass:: pytest.Config()
+.. autoclass:: pytest.File()
     :members:
+    :show-inheritance:
 
-ExceptionInfo
-~~~~~~~~~~~~~
+FSCollector
+~~~~~~~~~~~
 
-.. autoclass:: pytest.ExceptionInfo()
+.. autoclass:: _pytest.nodes.FSCollector()
     :members:
+    :show-inheritance:
 
+Session
+~~~~~~~
 
-ExitCode
-~~~~~~~~
-
-.. autoclass:: pytest.ExitCode
+.. autoclass:: pytest.Session()
     :members:
+    :show-inheritance:
 
-File
-~~~~
+Package
+~~~~~~~
 
-.. autoclass:: pytest.File()
+.. autoclass:: pytest.Package()
     :members:
     :show-inheritance:
 
+Module
+~~~~~~
 
-FixtureDef
-~~~~~~~~~~
-
-.. autoclass:: _pytest.fixtures.FixtureDef()
+.. autoclass:: pytest.Module()
     :members:
     :show-inheritance:
 
-FSCollector
-~~~~~~~~~~~
+Class
+~~~~~
 
-.. autoclass:: _pytest.nodes.FSCollector()
+.. autoclass:: pytest.Class()
     :members:
     :show-inheritance:
 
@@ -873,10 +867,52 @@
     :members:
     :show-inheritance:
 
-Item
-~~~~
 
-.. autoclass:: pytest.Item()
+Objects
+-------
+
+Objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks 
<hook-reference>`
+or importable from ``pytest``.
+
+
+CallInfo
+~~~~~~~~
+
+.. autoclass:: pytest.CallInfo()
+    :members:
+
+CollectReport
+~~~~~~~~~~~~~
+
+.. autoclass:: pytest.CollectReport()
+    :members:
+    :show-inheritance:
+    :inherited-members:
+
+Config
+~~~~~~
+
+.. autoclass:: pytest.Config()
+    :members:
+
+ExceptionInfo
+~~~~~~~~~~~~~
+
+.. autoclass:: pytest.ExceptionInfo()
+    :members:
+
+
+ExitCode
+~~~~~~~~
+
+.. autoclass:: pytest.ExitCode
+    :members:
+
+
+FixtureDef
+~~~~~~~~~~
+
+.. autoclass:: _pytest.fixtures.FixtureDef()
     :members:
     :show-inheritance:
 
@@ -907,19 +943,6 @@
 .. autoclass:: pytest.Metafunc()
     :members:
 
-Module
-~~~~~~
-
-.. autoclass:: pytest.Module()
-    :members:
-    :show-inheritance:
-
-Node
-~~~~
-
-.. autoclass:: _pytest.nodes.Node()
-    :members:
-
 Parser
 ~~~~~~
 
@@ -941,13 +964,6 @@
     :inherited-members:
     :show-inheritance:
 
-Session
-~~~~~~~
-
-.. autoclass:: pytest.Session()
-    :members:
-    :show-inheritance:
-
 TestReport
 ~~~~~~~~~~
 
@@ -962,10 +978,10 @@
 .. autoclass:: pytest.TestShortLogReport()
     :members:
 
-_Result
+Result
 ~~~~~~~
 
-Result object used within :ref:`hook wrappers <hookwrapper>`, see 
:py:class:`_Result in the pluggy documentation <pluggy._callers._Result>` for 
more information.
+Result object used within :ref:`hook wrappers <hookwrapper>`, see 
:py:class:`Result in the pluggy documentation <pluggy.Result>` for more 
information.
 
 Stash
 ~~~~~
@@ -1871,8 +1887,12 @@
                             tests. Optional argument: glob (default: '*').
       --cache-clear         Remove all cache contents at start of test run
       --lfnf={all,none}, --last-failed-no-failures={all,none}
-                            Which tests to run with no previously (known)
-                            failures
+                            With ``--lf``, determines whether to execute tests
+                            when there are no previously (known) failures or
+                            when no cached ``lastfailed`` data was found.
+                            ``all`` (the default) runs the full test suite
+                            again. ``none`` just emits a message about no known
+                            failures and exits successfully.
       --sw, --stepwise      Exit on test failure and continue from last failing
                             test next time
       --sw-skip, --stepwise-skip
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/src/_pytest/_version.py 
new/pytest-7.4.1/src/_pytest/_version.py
--- old/pytest-7.4.0/src/_pytest/_version.py    2023-06-23 13:17:09.000000000 
+0200
+++ new/pytest-7.4.1/src/_pytest/_version.py    2023-09-02 17:00:18.000000000 
+0200
@@ -1,4 +1,4 @@
 # file generated by setuptools_scm
 # don't change, don't track in version control
-__version__ = version = '7.4.0'
-__version_tuple__ = version_tuple = (7, 4, 0)
+__version__ = version = '7.4.1'
+__version_tuple__ = version_tuple = (7, 4, 1)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/src/_pytest/cacheprovider.py 
new/pytest-7.4.1/src/_pytest/cacheprovider.py
--- old/pytest-7.4.0/src/_pytest/cacheprovider.py       2023-06-23 
13:16:47.000000000 +0200
+++ new/pytest-7.4.1/src/_pytest/cacheprovider.py       2023-09-02 
16:59:58.000000000 +0200
@@ -505,7 +505,11 @@
         dest="last_failed_no_failures",
         choices=("all", "none"),
         default="all",
-        help="Which tests to run with no previously (known) failures",
+        help="With ``--lf``, determines whether to execute tests when there "
+        "are no previously (known) failures or when no "
+        "cached ``lastfailed`` data was found. "
+        "``all`` (the default) runs the full test suite again. "
+        "``none`` just emits a message about no known failures and exits 
successfully.",
     )
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/src/_pytest/config/__init__.py 
new/pytest-7.4.1/src/_pytest/config/__init__.py
--- old/pytest-7.4.0/src/_pytest/config/__init__.py     2023-06-23 
13:16:47.000000000 +0200
+++ new/pytest-7.4.1/src/_pytest/config/__init__.py     2023-09-02 
16:59:58.000000000 +0200
@@ -137,7 +137,9 @@
 ) -> Union[int, ExitCode]:
     """Perform an in-process test run.
 
-    :param args: List of command line arguments.
+    :param args:
+        List of command line arguments. If `None` or not given, defaults to 
reading
+        arguments directly from the process command line (:data:`sys.argv`).
     :param plugins: List of plugin objects to be auto-registered during 
initialization.
 
     :returns: An exit code.
@@ -442,10 +444,10 @@
         # so we avoid accessing possibly non-readable attributes
         # (see issue #1073).
         if not name.startswith("pytest_"):
-            return
+            return None
         # Ignore names which can not be hooks.
         if name == "pytest_plugins":
-            return
+            return None
 
         opts = super().parse_hookimpl_opts(plugin, name)
         if opts is not None:
@@ -454,9 +456,9 @@
         method = getattr(plugin, name)
         # Consider only actual functions for hooks (#3775).
         if not inspect.isroutine(method):
-            return
+            return None
         # Collect unmarked hooks as long as they have the `pytest_' prefix.
-        return _get_legacy_hook_marks(
+        return _get_legacy_hook_marks(  # type: ignore[return-value]
             method, "impl", ("tryfirst", "trylast", "optionalhook", 
"hookwrapper")
         )
 
@@ -465,7 +467,7 @@
         if opts is None:
             method = getattr(module_or_class, name)
             if name.startswith("pytest_"):
-                opts = _get_legacy_hook_marks(
+                opts = _get_legacy_hook_marks(  # type: ignore[assignment]
                     method,
                     "spec",
                     ("firstresult", "historic"),
@@ -1063,9 +1065,10 @@
             fin()
 
     def get_terminal_writer(self) -> TerminalWriter:
-        terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
+        terminalreporter: Optional[TerminalReporter] = 
self.pluginmanager.get_plugin(
             "terminalreporter"
         )
+        assert terminalreporter is not None
         return terminalreporter._tw
 
     def pytest_cmdline_parse(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/src/_pytest/helpconfig.py 
new/pytest-7.4.1/src/_pytest/helpconfig.py
--- old/pytest-7.4.0/src/_pytest/helpconfig.py  2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/src/_pytest/helpconfig.py  2023-09-02 16:59:58.000000000 
+0200
@@ -11,6 +11,7 @@
 from _pytest.config import ExitCode
 from _pytest.config import PrintHelp
 from _pytest.config.argparsing import Parser
+from _pytest.terminal import TerminalReporter
 
 
 class HelpAction(Action):
@@ -159,7 +160,10 @@
 def showhelp(config: Config) -> None:
     import textwrap
 
-    reporter = config.pluginmanager.get_plugin("terminalreporter")
+    reporter: Optional[TerminalReporter] = config.pluginmanager.get_plugin(
+        "terminalreporter"
+    )
+    assert reporter is not None
     tw = reporter._tw
     tw.write(config._parser.optparser.format_help())
     tw.line()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/src/_pytest/logging.py 
new/pytest-7.4.1/src/_pytest/logging.py
--- old/pytest-7.4.0/src/_pytest/logging.py     2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/src/_pytest/logging.py     2023-09-02 16:59:58.000000000 
+0200
@@ -660,6 +660,8 @@
         )
         if self._log_cli_enabled():
             terminal_reporter = 
config.pluginmanager.get_plugin("terminalreporter")
+            # Guaranteed by `_log_cli_enabled()`.
+            assert terminal_reporter is not None
             capture_manager = config.pluginmanager.get_plugin("capturemanager")
             # if capturemanager plugin is disabled, live logging still works.
             self.log_cli_handler: Union[
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/src/_pytest/main.py 
new/pytest-7.4.1/src/_pytest/main.py
--- old/pytest-7.4.0/src/_pytest/main.py        2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/src/_pytest/main.py        2023-09-02 16:59:58.000000000 
+0200
@@ -462,6 +462,11 @@
 
 @final
 class Session(nodes.FSCollector):
+    """The root of the collection tree.
+
+    ``Session`` collects the initial paths given as arguments to pytest.
+    """
+
     Interrupted = Interrupted
     Failed = Failed
     # Set on the session by runner.pytest_sessionstart.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/src/_pytest/nodes.py 
new/pytest-7.4.1/src/_pytest/nodes.py
--- old/pytest-7.4.0/src/_pytest/nodes.py       2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/src/_pytest/nodes.py       2023-09-02 16:59:58.000000000 
+0200
@@ -157,10 +157,11 @@
 
 
 class Node(metaclass=NodeMeta):
-    """Base class for Collector and Item, the components of the test
-    collection tree.
+    r"""Base class of :class:`Collector` and :class:`Item`, the components of
+    the test collection tree.
 
-    Collector subclasses have children; Items are leaf nodes.
+    ``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are 
the
+    leaf nodes.
     """
 
     # Implemented in the legacypath plugin.
@@ -525,15 +526,17 @@
 
 
 class Collector(Node):
-    """Collector instances create children through collect() and thus
-    iteratively build a tree."""
+    """Base class of all collectors.
+
+    Collector create children through `collect()` and thus iteratively build
+    the collection tree.
+    """
 
     class CollectError(Exception):
         """An error during collection, contains a custom message."""
 
     def collect(self) -> Iterable[Union["Item", "Collector"]]:
-        """Return a list of children (items and collectors) for this
-        collection node."""
+        """Collect children (items and collectors) for this collector."""
         raise NotImplementedError("abstract")
 
     # TODO: This omits the style= parameter which breaks Liskov Substitution.
@@ -577,6 +580,8 @@
 
 
 class FSCollector(Collector):
+    """Base class for filesystem collectors."""
+
     def __init__(
         self,
         fspath: Optional[LEGACY_PATH] = None,
@@ -660,7 +665,7 @@
 
 
 class Item(Node):
-    """A basic test invocation item.
+    """Base class of all test invocation items.
 
     Note that for a single function there might be multiple test invocation 
items.
     """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/src/_pytest/pathlib.py 
new/pytest-7.4.1/src/_pytest/pathlib.py
--- old/pytest-7.4.0/src/_pytest/pathlib.py     2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/src/_pytest/pathlib.py     2023-09-02 16:59:58.000000000 
+0200
@@ -523,6 +523,8 @@
 
     if mode is ImportMode.importlib:
         module_name = module_name_from_path(path, root)
+        with contextlib.suppress(KeyError):
+            return sys.modules[module_name]
 
         for meta_importer in sys.meta_path:
             spec = meta_importer.find_spec(module_name, [str(path.parent)])
@@ -633,6 +635,9 @@
     otherwise "src.tests.test_foo" is not importable by ``__import__``.
     """
     module_parts = module_name.split(".")
+    child_module: Union[ModuleType, None] = None
+    module: Union[ModuleType, None] = None
+    child_name: str = ""
     while module_name:
         if module_name not in modules:
             try:
@@ -642,13 +647,22 @@
                 # ourselves to fall back to creating a dummy module.
                 if not sys.meta_path:
                     raise ModuleNotFoundError
-                importlib.import_module(module_name)
+                module = importlib.import_module(module_name)
             except ModuleNotFoundError:
                 module = ModuleType(
                     module_name,
                     doc="Empty module created by pytest's 
importmode=importlib.",
                 )
+        else:
+            module = modules[module_name]
+        if child_module:
+            # Add child attribute to the parent that can reference the child
+            # modules.
+            if not hasattr(module, child_name):
+                setattr(module, child_name, child_module)
                 modules[module_name] = module
+        # Keep track of the child module while moving up the tree.
+        child_module, child_name = module, module_name.rpartition(".")[-1]
         module_parts.pop(-1)
         module_name = ".".join(module_parts)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/src/_pytest/pytester.py 
new/pytest-7.4.1/src/_pytest/pytester.py
--- old/pytest-7.4.0/src/_pytest/pytester.py    2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/src/_pytest/pytester.py    2023-09-02 16:59:58.000000000 
+0200
@@ -752,7 +752,7 @@
 
     def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> 
HookRecorder:
         """Create a new :class:`HookRecorder` for a 
:class:`PytestPluginManager`."""
-        pluginmanager.reprec = reprec = HookRecorder(pluginmanager, 
_ispytest=True)
+        pluginmanager.reprec = reprec = HookRecorder(pluginmanager, 
_ispytest=True)  # type: ignore[attr-defined]
         self._request.addfinalizer(reprec.finish_recording)
         return reprec
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/src/_pytest/python.py 
new/pytest-7.4.1/src/_pytest/python.py
--- old/pytest-7.4.0/src/_pytest/python.py      2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/src/_pytest/python.py      2023-09-02 16:59:58.000000000 
+0200
@@ -522,7 +522,7 @@
 
 
 class Module(nodes.File, PyCollector):
-    """Collector for test classes and functions."""
+    """Collector for test classes and functions in a Python module."""
 
     def _getobj(self):
         return self._importtestmodule()
@@ -659,6 +659,9 @@
 
 
 class Package(Module):
+    """Collector for files and directories in a Python packages -- directories
+    with an `__init__.py` file."""
+
     def __init__(
         self,
         fspath: Optional[LEGACY_PATH],
@@ -788,7 +791,7 @@
 
 
 class Class(PyCollector):
-    """Collector for test methods."""
+    """Collector for test methods (and nested classes) in a Python class."""
 
     @classmethod
     def from_parent(cls, parent, *, name, obj=None, **kw):
@@ -1149,7 +1152,7 @@
         arg2scope = self._arg2scope.copy()
         for arg, val in zip(argnames, valset):
             if arg in params or arg in funcargs:
-                raise ValueError(f"duplicate {arg!r}")
+                raise ValueError(f"duplicate parametrization of {arg!r}")
             valtype_for_arg = valtypes[arg]
             if valtype_for_arg == "params":
                 params[arg] = val
@@ -1240,8 +1243,9 @@
         during the collection phase. If you need to setup expensive resources
         see about setting indirect to do it rather than at test setup time.
 
-        Can be called multiple times, in which case each call parametrizes all
-        previous parametrizations, e.g.
+        Can be called multiple times per test function (but only on different
+        argument names), in which case each call parametrizes all previous
+        parametrizations, e.g.
 
         ::
 
@@ -1673,7 +1677,7 @@
 
 
 class Function(PyobjMixin, nodes.Item):
-    """An Item responsible for setting up and executing a Python test function.
+    """Item responsible for setting up and executing a Python test function.
 
     :param name:
         The full function name, including any decorations like those
@@ -1830,10 +1834,8 @@
 
 
 class FunctionDefinition(Function):
-    """
-    This class is a step gap solution until we evolve to have actual function 
definition nodes
-    and manage to get rid of ``metafunc``.
-    """
+    """This class is a stop gap solution until we evolve to have actual 
function
+    definition nodes and manage to get rid of ``metafunc``."""
 
     def runtest(self) -> None:
         raise RuntimeError("function definitions are not supposed to be run as 
tests")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/src/_pytest/python_api.py 
new/pytest-7.4.1/src/_pytest/python_api.py
--- old/pytest-7.4.0/src/_pytest/python_api.py  2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/src/_pytest/python_api.py  2023-09-02 16:59:58.000000000 
+0200
@@ -266,19 +266,20 @@
             approx_side_as_map.items(), other_side.values()
         ):
             if approx_value != other_value:
-                max_abs_diff = max(
-                    max_abs_diff, abs(approx_value.expected - other_value)
-                )
-                if approx_value.expected == 0.0:
-                    max_rel_diff = math.inf
-                else:
-                    max_rel_diff = max(
-                        max_rel_diff,
-                        abs(
-                            (approx_value.expected - other_value)
-                            / approx_value.expected
-                        ),
+                if approx_value.expected is not None and other_value is not 
None:
+                    max_abs_diff = max(
+                        max_abs_diff, abs(approx_value.expected - other_value)
                     )
+                    if approx_value.expected == 0.0:
+                        max_rel_diff = math.inf
+                    else:
+                        max_rel_diff = max(
+                            max_rel_diff,
+                            abs(
+                                (approx_value.expected - other_value)
+                                / approx_value.expected
+                            ),
+                        )
                 different_ids.append(approx_key)
 
         message_data = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/src/pytest.egg-info/PKG-INFO 
new/pytest-7.4.1/src/pytest.egg-info/PKG-INFO
--- old/pytest-7.4.0/src/pytest.egg-info/PKG-INFO       2023-06-23 
13:17:09.000000000 +0200
+++ new/pytest-7.4.1/src/pytest.egg-info/PKG-INFO       2023-09-02 
17:00:18.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: pytest
-Version: 7.4.0
+Version: 7.4.1
 Summary: pytest: simple powerful testing with Python
 Home-page: https://docs.pytest.org/en/latest/
 Author: Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, 
Brianna Laugher, Florian Bruhin and others
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/src/pytest.egg-info/SOURCES.txt 
new/pytest-7.4.1/src/pytest.egg-info/SOURCES.txt
--- old/pytest-7.4.0/src/pytest.egg-info/SOURCES.txt    2023-06-23 
13:17:10.000000000 +0200
+++ new/pytest-7.4.1/src/pytest.egg-info/SOURCES.txt    2023-09-02 
17:00:18.000000000 +0200
@@ -232,6 +232,7 @@
 doc/en/announce/release-7.3.1.rst
 doc/en/announce/release-7.3.2.rst
 doc/en/announce/release-7.4.0.rst
+doc/en/announce/release-7.4.1.rst
 doc/en/announce/sprint2016.rst
 doc/en/example/attic.rst
 doc/en/example/conftest.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/testing/acceptance_test.py 
new/pytest-7.4.1/testing/acceptance_test.py
--- old/pytest-7.4.0/testing/acceptance_test.py 2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/testing/acceptance_test.py 2023-09-02 16:59:58.000000000 
+0200
@@ -1317,3 +1317,38 @@
     )
     res = pytester.runpytest()
     res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of 
`return`?*"])
+
+
+def test_doctest_and_normal_imports_with_importlib(pytester: Pytester) -> None:
+    """
+    Regression test for #10811: previously import_path with 
ImportMode.importlib would
+    not return a module if already in sys.modules, resulting in modules being 
imported
+    multiple times, which causes problems with modules that have import side 
effects.
+    """
+    # Uses the exact reproducer form #10811, given it is very minimal
+    # and illustrates the problem well.
+    pytester.makepyfile(
+        **{
+            "pmxbot/commands.py": "from . import logging",
+            "pmxbot/logging.py": "",
+            "tests/__init__.py": "",
+            "tests/test_commands.py": """
+                import importlib
+                from pmxbot import logging
+
+                class TestCommands:
+                    def test_boo(self):
+                        assert importlib.import_module('pmxbot.logging') is 
logging
+                """,
+        }
+    )
+    pytester.makeini(
+        """
+        [pytest]
+        addopts=
+            --doctest-modules
+            --import-mode importlib
+        """
+    )
+    result = pytester.runpytest_subprocess()
+    result.stdout.fnmatch_lines("*1 passed*")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/testing/python/approx.py 
new/pytest-7.4.1/testing/python/approx.py
--- old/pytest-7.4.0/testing/python/approx.py   2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/testing/python/approx.py   2023-09-02 16:59:58.000000000 
+0200
@@ -123,6 +123,23 @@
         )
 
         assert_approx_raises_regex(
+            {"a": 1.0, "b": None, "c": None},
+            {
+                "a": None,
+                "b": 1000.0,
+                "c": None,
+            },
+            [
+                r"  comparison failed. Mismatched elements: 2 / 3:",
+                r"  Max absolute difference: -inf",
+                r"  Max relative difference: -inf",
+                r"  Index \| Obtained\s+\| Expected\s+",
+                rf"  a     \| {SOME_FLOAT} \| None",
+                rf"  b     \| None\s+\| {SOME_FLOAT} ± {SOME_FLOAT}",
+            ],
+        )
+
+        assert_approx_raises_regex(
             [1.0, 2.0, 3.0, 4.0],
             [1.0, 3.0, 3.0, 5.0],
             [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/testing/test_pathlib.py 
new/pytest-7.4.1/testing/test_pathlib.py
--- old/pytest-7.4.0/testing/test_pathlib.py    2023-06-23 13:16:47.000000000 
+0200
+++ new/pytest-7.4.1/testing/test_pathlib.py    2023-09-02 16:59:58.000000000 
+0200
@@ -7,6 +7,7 @@
 from types import ModuleType
 from typing import Any
 from typing import Generator
+from typing import Iterator
 
 import pytest
 from _pytest.monkeypatch import MonkeyPatch
@@ -282,29 +283,36 @@
             import_path(tmp_path / "invalid.py", root=tmp_path)
 
     @pytest.fixture
-    def simple_module(self, tmp_path: Path) -> Path:
-        fn = tmp_path / "_src/tests/mymod.py"
+    def simple_module(
+        self, tmp_path: Path, request: pytest.FixtureRequest
+    ) -> Iterator[Path]:
+        name = f"mymod_{request.node.name}"
+        fn = tmp_path / f"_src/tests/{name}.py"
         fn.parent.mkdir(parents=True)
         fn.write_text("def foo(x): return 40 + x", encoding="utf-8")
-        return fn
+        module_name = module_name_from_path(fn, root=tmp_path)
+        yield fn
+        sys.modules.pop(module_name, None)
 
-    def test_importmode_importlib(self, simple_module: Path, tmp_path: Path) 
-> None:
+    def test_importmode_importlib(
+        self, simple_module: Path, tmp_path: Path, request: 
pytest.FixtureRequest
+    ) -> None:
         """`importlib` mode does not change sys.path."""
         module = import_path(simple_module, mode="importlib", root=tmp_path)
         assert module.foo(2) == 42  # type: ignore[attr-defined]
         assert str(simple_module.parent) not in sys.path
         assert module.__name__ in sys.modules
-        assert module.__name__ == "_src.tests.mymod"
+        assert module.__name__ == f"_src.tests.mymod_{request.node.name}"
         assert "_src" in sys.modules
         assert "_src.tests" in sys.modules
 
-    def test_importmode_twice_is_different_module(
+    def test_remembers_previous_imports(
         self, simple_module: Path, tmp_path: Path
     ) -> None:
-        """`importlib` mode always returns a new module."""
+        """`importlib` mode called remembers previous module (#10341, 
#10811)."""
         module1 = import_path(simple_module, mode="importlib", root=tmp_path)
         module2 = import_path(simple_module, mode="importlib", root=tmp_path)
-        assert module1 is not module2
+        assert module1 is module2
 
     def test_no_meta_path_found(
         self, simple_module: Path, monkeypatch: MonkeyPatch, tmp_path: Path
@@ -317,6 +325,9 @@
         # mode='importlib' fails if no spec is found to load the module
         import importlib.util
 
+        # Force module to be re-imported.
+        del sys.modules[module.__name__]
+
         monkeypatch.setattr(
             importlib.util, "spec_from_file_location", lambda *args: None
         )
@@ -592,3 +603,15 @@
         modules = {}
         insert_missing_modules(modules, "")
         assert modules == {}
+
+    def test_parent_contains_child_module_attribute(
+        self, monkeypatch: MonkeyPatch, tmp_path: Path
+    ):
+        monkeypatch.chdir(tmp_path)
+        # Use 'xxx' and 'xxy' as parent names as they are unlikely to exist and
+        # don't end up being imported.
+        modules = {"xxx.tests.foo": ModuleType("xxx.tests.foo")}
+        insert_missing_modules(modules, "xxx.tests.foo")
+        assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"]
+        assert modules["xxx"].tests is modules["xxx.tests"]
+        assert modules["xxx.tests"].foo is modules["xxx.tests.foo"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/pytest-7.4.0/testing/test_pluginmanager.py 
new/pytest-7.4.1/testing/test_pluginmanager.py
--- old/pytest-7.4.0/testing/test_pluginmanager.py      2023-06-23 
13:16:47.000000000 +0200
+++ new/pytest-7.4.1/testing/test_pluginmanager.py      2023-09-02 
16:59:58.000000000 +0200
@@ -242,8 +242,12 @@
         mod = types.ModuleType("temp")
         mod.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"]
         pytestpm.consider_module(mod)
-        assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1"
-        assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2"
+        p1 = pytestpm.get_plugin("pytest_p1")
+        assert p1 is not None
+        assert p1.__name__ == "pytest_p1"
+        p2 = pytestpm.get_plugin("pytest_p2")
+        assert p2 is not None
+        assert p2.__name__ == "pytest_p2"
 
     def test_consider_module_import_module(
         self, pytester: Pytester, _config_for_test: Config
@@ -336,6 +340,7 @@
         len2 = len(pytestpm.get_plugins())
         assert len1 == len2
         plugin1 = pytestpm.get_plugin("pytest_hello")
+        assert plugin1 is not None
         assert plugin1.__name__.endswith("pytest_hello")
         plugin2 = pytestpm.get_plugin("pytest_hello")
         assert plugin2 is plugin1
@@ -351,6 +356,7 @@
         pluginname = "pkg.plug"
         pytestpm.import_plugin(pluginname)
         mod = pytestpm.get_plugin("pkg.plug")
+        assert mod is not None
         assert mod.x == 3
 
     def test_consider_conftest_deps(

Reply via email to