Skip to content

MAINT: Updates for pytest 8.1 and python 3.12 #67

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/workflows/base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
list_nox_test_sessions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v1
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: 3.7
python-version: "3.7"
architecture: x64

- name: Install noxfile requirements
Expand All @@ -43,14 +43,14 @@ jobs:
name: ${{ matrix.os }} ${{ matrix.nox_session }} # ${{ matrix.name_suffix }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

# Conda install
- name: Install conda v3.7
- name: Install conda v3.12
uses: conda-incubator/setup-miniconda@v2
with:
# auto-update-conda: true
python-version: 3.7
python-version: "3.12"
activate-environment: noxenv
- run: conda info
shell: bash -l {0} # so that conda works
Expand Down Expand Up @@ -84,7 +84,7 @@ jobs:
GITHUB_CONTEXT: ${{ toJSON(github) }}
run: echo "$GITHUB_CONTEXT"

- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0 # so that gh-deploy works

Expand All @@ -100,7 +100,7 @@ jobs:
uses: conda-incubator/setup-miniconda@v2
with:
# auto-update-conda: true
python-version: 3.7
python-version: "3.12"
activate-environment: noxenv
- run: conda info
shell: bash -l {0} # so that conda works
Expand Down
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,8 @@ You should then be able to list all available tasks using:
>>> nox --list
Sessions defined in <path>\noxfile.py:

* tests-2.7 -> Run the test suite, including test reports generation and coverage reports.
* tests-3.5 -> Run the test suite, including test reports generation and coverage reports.
* tests-3.6 -> Run the test suite, including test reports generation and coverage reports.
* tests-3.12 -> Run the test suite, including test reports generation and coverage reports.
* tests-3.8 -> Run the test suite, including test reports generation and coverage reports.
* tests-3.7 -> Run the test suite, including test reports generation and coverage reports.
- docs-3.7 -> Generates the doc and serves it on a local http server. Pass '-- build' to build statically instead.
- publish-3.7 -> Deploy the docs+reports on github pages. Note: this rebuilds the docs
- release-3.7 -> Create a release on github corresponding to the latest tag
Expand All @@ -49,7 +46,7 @@ This project uses `pytest` so running `pytest` at the root folder will execute a
nox
```

Tests and coverage reports are automatically generated under `./docs/reports` for one of the sessions (`tests-3.7`).
Tests and coverage reports are automatically generated under `./docs/reports` for one of the sessions (`tests-3.7`).

If you wish to execute tests on a specific environment, use explicit session names, e.g. `nox -s tests-3.6`.

Expand Down
14 changes: 9 additions & 5 deletions ci_tools/nox_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
nox_logger = logging.getLogger("nox")


PY27, PY35, PY36, PY37, PY38 = "2.7", "3.5", "3.6", "3.7", "3.8"
PY38, PY312 = "3.8", "3.12"
DONT_INSTALL = "dont_install"


Expand Down Expand Up @@ -715,11 +715,15 @@ async def async_popen():
outlines = []
await asyncio.wait([
# process out is only redirected to STDOUT if not silent
_read_stream(process.stdout, lambda l: tee(l, sinklist=outlines, sinkstream=log_file_stream,
quiet=silent, verbosepipe=sys.stdout)),
asyncio.create_task(
_read_stream(process.stdout, lambda l: tee(l, sinklist=outlines, sinkstream=log_file_stream,
quiet=silent, verbosepipe=sys.stdout)),
),
# process err is always redirected to STDOUT (quiet=False) with a specific label
_read_stream(process.stderr, lambda l: tee(l, sinklist=outlines, sinkstream=log_file_stream,
quiet=False, verbosepipe=sys.stdout, label="ERR:"))
asyncio.create_task(
_read_stream(process.stderr, lambda l: tee(l, sinklist=outlines, sinkstream=log_file_stream,
quiet=False, verbosepipe=sys.stdout, label="ERR:"))
),
])
return_code = await process.wait() # make sur the process has ended and retrieve its return code
return return_code, outlines
Expand Down
16 changes: 10 additions & 6 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

### 2.0.0 - updates for Python 3.8+

- Modernized for Python 3.8+ and pytest 6+ support only.

### 1.10.4 - python 3.5 xdist bugfix

- Fixed issue with `pytest-xdist` and python 3.5: `pathlib` objects were not properly handled by other stdlib modules in this python version. Fixed [#59](https://github.com/smarie/python-pytest-harvest/issues/59)
Expand Down Expand Up @@ -144,31 +148,31 @@ Fixed pytest ordering issue, by relying on [place_as](https://github.com/pytest-

### 1.0.0 - new methods for pytest session analysis

New methods are provided to analyse pytest session results:
New methods are provided to analyse pytest session results:
- `filter_session_items(session, filter=None)` is the filtering method used behind several functions in this package - it can be used independently. `pytest_item_matches_filter` is the inner method used to test if a single item matches the filter.
- `get_all_pytest_param_names(session, filter=None, filter_incomplete=False)` lists all unique parameter names used in pytest session items, with optional filtering capabilities. Fixes [#12](https://github.com/smarie/python-pytest-harvest/issues/12)
- `is_pytest_incomplete(item)`, `get_pytest_status(item)`, `get_pytest_param_names(item)` and `get_pytest_params(item)` allow users to analyse a specific item.
- `is_pytest_incomplete(item)`, `get_pytest_status(item)`, `get_pytest_param_names(item)` and `get_pytest_params(item)` allow users to analyse a specific item.


### 0.9.0 - `get_session_synthesis_dct`: filter bugfix + test id formatter

* `get_session_synthesis_dct`:

- `filter` now correctly handles class methods. Fixed [#11](https://github.com/smarie/python-pytest-harvest/issues/11)
- new `test_id_format` option to process test ids. Fixed [#9](https://github.com/smarie/python-pytest-harvest/issues/9)

### 0.8.0 - Documentation + better filters in `get_session_synthesis_dct`

* Documentation: added a section about creating the synthesis table from *inside* a test function (fixes [#4](https://github.com/smarie/python-pytest-harvest/issues/4)). Also, added a link to a complete example file.

* `get_session_synthesis_dct`: `filter` argument can now contain module names (fixed [#7](https://github.com/smarie/python-pytest-harvest/issues/7)). Also now the function filters out incomplete tests by default. A new `filter_incomplete` argument can be used to display them again (fixed [#8](https://github.com/smarie/python-pytest-harvest/issues/8)).

### 0.7.0 - Documentation + `get_session_synthesis_dct` improvements 2

* Results bags do not measure execution time anymore since this is much less accurate than pytest duration. Fixes [#6](https://github.com/smarie/python-pytest-harvest/issues/6)

* `get_session_synthesis_dct` does not output the stage by stage details (setup/call/teardown) anymore by default, but a new option `status_details` allows users to enable them. Fixes [#5](https://github.com/smarie/python-pytest-harvest/issues/5)

* `get_session_synthesis_dct` has also 2 new options `durations_in_ms` and `pytest_prefix` to better control the output.

* Improved documentation.
Expand Down
43 changes: 11 additions & 32 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# add parent folder to python path so that we can import noxfile_utils.py
# note that you need to "pip install -r noxfile-requiterements.txt" for this file to work.
sys.path.append(str(Path(__file__).parent / "ci_tools"))
from nox_utils import PY27, PY37, PY36, PY35, PY38, power_session, rm_folder, rm_file, PowerSession, DONT_INSTALL # noqa
from nox_utils import PY38, PY312, power_session, rm_folder, rm_file, PowerSession, DONT_INSTALL # noqa


pkg_name = "pytest_harvest"
Expand All @@ -18,35 +18,14 @@


ENVS = {
# python 3.8 - put first to detect easy issues faster.
(PY38, "pytest2.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<3", "pytest-asyncio": DONT_INSTALL}}, # "pytest-html": "1.9.0",
(PY38, "pytest3.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<4"}},
(PY38, "pytest4.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<5"}},
(PY38, "pytest5.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<6"}},
(PY38, "pytest-latest"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": ""}},
# python 2.7
(PY27, "pytest2.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<3", "pytest-asyncio": DONT_INSTALL}}, # "pytest-html": "1.9.0",
(PY27, "pytest3.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<4"}},
(PY27, "pytest4.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<5"}},
# python 3.5
(PY35, "pytest2.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<3", "pytest-asyncio": DONT_INSTALL}}, # "pytest-html": "1.9.0",
(PY35, "pytest3.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<4"}},
(PY35, "pytest4.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<5"}},
(PY35, "pytest5.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<6"}},
(PY35, "pytest-latest"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": ""}},
# python 3.6
(PY36, "pytest2.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<3", "pytest-asyncio": DONT_INSTALL}}, # "pytest-html": "1.9.0",
(PY36, "pytest3.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<4"}},
(PY36, "pytest4.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<5"}},
(PY36, "pytest5.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<6"}},
(PY36, "pytest-latest"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": ""}},
# python 3.7
(PY37, "pytest2.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<3", "pytest-asyncio": DONT_INSTALL}}, # "pytest-html": "1.9.0",
(PY37, "pytest3.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<4"}},
(PY37, "pytest4.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<5"}},
(PY37, "pytest5.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<6"}},
# python 3.12
(PY312, "pytest7.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<8"}},
# (PY312, "pytest-latest"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": ""}},
# python 3.8
(PY38, "pytest6.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<7"}},
(PY38, "pytest7.x"): {"coverage": False, "pkg_specs": {"pip": ">19", "pytest": "<8"}},
# IMPORTANT: this should be last so that the folder docs/reports is not deleted afterwards
(PY37, "pytest-latest"): {"coverage": True, "pkg_specs": {"pip": ">19", "pytest": ""}}
# (PY38, "pytest-latest"): {"coverage": True, "pkg_specs": {"pip": ">19", "pytest": ""}}
}


Expand Down Expand Up @@ -156,7 +135,7 @@ def tests(session: PowerSession, coverage, pkg_specs):
session.run2("python ci_tools/generate-junit-badge.py 100 %s" % Folders.test_reports)


@power_session(python=[PY37])
@power_session(python=[PY38])
def docs(session: PowerSession):
"""Generates the doc and serves it on a local http server. Pass '-- build' to build statically instead."""

Expand All @@ -169,7 +148,7 @@ def docs(session: PowerSession):
session.run2("mkdocs serve -f ./docs/mkdocs.yml")


@power_session(python=[PY37])
@power_session(python=[PY38])
def publish(session: PowerSession):
"""Deploy the docs+reports on github pages. Note: this rebuilds the docs"""

Expand All @@ -194,7 +173,7 @@ def publish(session: PowerSession):
# session.run2('codecov -t %s -f %s' % (codecov_token, Folders.coverage_xml))


@power_session(python=[PY37])
@power_session(python=[PY38])
def release(session: PowerSession):
"""Create a release on github corresponding to the latest tag"""

Expand Down
3 changes: 1 addition & 2 deletions pytest_harvest/fixture_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

from decopatch import DECORATED, function_decorator
from makefun import wraps, add_signature_parameters
from six import string_types

from pytest_harvest.common import get_scope

Expand Down Expand Up @@ -81,7 +80,7 @@ def test_synthesis(fixture_store):
key = key or fixture_name

# is the store a fixture or an object ?
store_is_a_fixture = isinstance(store, string_types)
store_is_a_fixture = isinstance(store, str)

# if the store object is already available, we can ensure that it is initialized. Otherwise trust pytest for that
if not store_is_a_fixture:
Expand Down
15 changes: 7 additions & 8 deletions pytest_harvest/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from logging import warning
from shutil import rmtree
import pytest
import six

try:
from pathlib import Path
Expand Down Expand Up @@ -238,9 +237,9 @@ def get_session_results_df(session_or_request,
"""
try:
import pandas as pd # pylint: disable=import-outside-toplevel
except ImportError as e:
six.raise_from(Exception("There was an error importing `pandas` module. Fixture `session_results_df` and method"
"`get_session_results_df` can not be used in this session."), e)
except ImportError:
raise Exception("There was an error importing `pandas` module. Fixture `session_results_df` and method"
"`get_session_results_df` can not be used in this session.")

# in case of xdist, make sure persisted workers results have been reloaded
possibly_restore_xdist_workers_structs(session_or_request)
Expand Down Expand Up @@ -308,10 +307,10 @@ def get_filtered_results_df(session,
"""
try:
import pandas as pd # pylint: disable=import-outside-toplevel
except ImportError as e:
six.raise_from(Exception("There was an error importing `pandas` module. Fixture `session_results_df` and "
"methods `get_filtered_results_df` and `get_module_results_df` can not be used in this"
" session. "), e)
except ImportError:
raise Exception("There was an error importing `pandas` module. Fixture `session_results_df` and "
"methods `get_filtered_results_df` and `get_module_results_df` can not be used in this"
" session. ")

# in case of xdist, make sure persisted workers results have been reloaded
possibly_restore_xdist_workers_structs(session)
Expand Down
7 changes: 3 additions & 4 deletions pytest_harvest/results_bags.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from datetime import datetime

import pytest
from six import raise_from

try: # python 3+
from typing import Type, Set, Union, Any, Dict
Expand All @@ -25,19 +24,19 @@ def __setattr__(self, key, value):
# try: No exception can happen: key is always a string, and new entries are allowed in a dict
self[key] = value
# except KeyError as e:
# raise_from(AttributeError(key), e)
# raise (AttributeError(key)

def __getattr__(self, key):
try:
return self[key]
except KeyError as e:
raise_from(AttributeError(key), e)
raise AttributeError(key)

def __delattr__(self, key):
try:
del self[key]
except KeyError as e:
raise_from(AttributeError(key), e)
raise AttributeError(key)

# object base
def __str__(self):
Expand Down
40 changes: 12 additions & 28 deletions pytest_harvest/results_session.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,23 @@
from distutils.version import LooseVersion
from typing import Union, Iterable, Mapping, Any

import pytest
import sys
from collections import OrderedDict, namedtuple
from itertools import chain
from six import string_types


pytest53 = LooseVersion(pytest.__version__) >= LooseVersion("5.3.0")
if pytest53:
def is_lazy_value_or_tupleitem_with_int_base(o):
return False
else:
# In this version of pytest, pytest-cases creates LazyValue objects that inherit from int, which makes pandas
# believe that their dtype should be int instead of object when creating a dataframe. We'll remove the int base here
try:
from pytest_cases.common_pytest_lazy_values import is_lazy

def is_lazy_value_or_tupleitem_with_int_base(o):
return is_lazy(o) and isinstance(o, int)

except ImportError: # noqa
def is_lazy_value_or_tupleitem_with_int_base(o):
return False

try: # python 3.5+
from typing import Union, Iterable, Mapping, Any
except ImportError:
pass

from pytest_harvest.common import HARVEST_PREFIX
from _pytest.doctest import DoctestItem


# version_tuple is new in 7.0
pytest81 = getattr(pytest, "version_tuple", (0, 0, 0)) >= (8, 1)
PYTEST_OBJ_NAME = 'pytest_obj'


def is_lazy_value_or_tupleitem_with_int_base(o):
return False


def get_session_synthesis_dct(session_or_request,
test_id_format='full', # type: str
status_details=False, # type: bool
Expand Down Expand Up @@ -189,7 +171,7 @@ def test_id_format(test_id):
if flatten_more is not None:
if isinstance(flatten_more, dict):
flatten_more_prefixes_dct = flatten_more.items()
elif isinstance(flatten_more, string_types):
elif isinstance(flatten_more, str):
# single name ?
flatten_more_prefixes_dct = {flatten_more: ''}
else:
Expand Down Expand Up @@ -504,7 +486,9 @@ def get_pytest_params(item):
if is_lazy_value_or_tupleitem_with_int_base(param_value):
# remove the int base so that pandas does not interprete it as an int.
param_value = param_value.clone(remove_int_base=True)
if item.session._fixturemanager.getfixturedefs(param_name, item.nodeid) is not None:

arg = item if pytest81 else item.nodeid
if item.session._fixturemanager.getfixturedefs(param_name, arg) is not None:
# Fixture parameters have the same name than the fixtures themselves! change it
param_dct[param_name + '_param'] = param_value
else:
Expand Down Expand Up @@ -543,7 +527,7 @@ def _get_filterset(filter):
:param filter:
:return:
"""
if isinstance(filter, string_types):
if isinstance(filter, str):
filter = {filter}
else:
try:
Expand Down
Loading