diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 56e51b1..998a1e3 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/README.md b/README.md index 88bad64..6c46d2d 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,8 @@ You should then be able to list all available tasks using: >>> nox --list Sessions defined in \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 @@ -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`. diff --git a/ci_tools/nox_utils.py b/ci_tools/nox_utils.py index d4f9572..521d268 100644 --- a/ci_tools/nox_utils.py +++ b/ci_tools/nox_utils.py @@ -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" @@ -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 diff --git a/docs/changelog.md b/docs/changelog.md index 4a2d94a..07b3608 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -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) @@ -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. diff --git a/noxfile.py b/noxfile.py index f42aa31..de98bd5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -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" @@ -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": ""}} } @@ -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.""" @@ -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""" @@ -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""" diff --git a/pytest_harvest/fixture_cache.py b/pytest_harvest/fixture_cache.py index f83cc10..2fd7205 100644 --- a/pytest_harvest/fixture_cache.py +++ b/pytest_harvest/fixture_cache.py @@ -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 @@ -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: diff --git a/pytest_harvest/plugin.py b/pytest_harvest/plugin.py index c289d67..633b584 100644 --- a/pytest_harvest/plugin.py +++ b/pytest_harvest/plugin.py @@ -3,7 +3,6 @@ from logging import warning from shutil import rmtree import pytest -import six try: from pathlib import Path @@ -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) @@ -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) diff --git a/pytest_harvest/results_bags.py b/pytest_harvest/results_bags.py index d0333db..713957b 100644 --- a/pytest_harvest/results_bags.py +++ b/pytest_harvest/results_bags.py @@ -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 @@ -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): diff --git a/pytest_harvest/results_session.py b/pytest_harvest/results_session.py index 17beff2..6e36249 100644 --- a/pytest_harvest/results_session.py +++ b/pytest_harvest/results_session.py @@ -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 @@ -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: @@ -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: @@ -543,7 +527,7 @@ def _get_filterset(filter): :param filter: :return: """ - if isinstance(filter, string_types): + if isinstance(filter, str): filter = {filter} else: try: diff --git a/pytest_harvest/tests/test_all_raw_with_meta_check.py b/pytest_harvest/tests/test_all_raw_with_meta_check.py index 1eb1710..2a23dc9 100644 --- a/pytest_harvest/tests/test_all_raw_with_meta_check.py +++ b/pytest_harvest/tests/test_all_raw_with_meta_check.py @@ -4,7 +4,6 @@ from os.path import join, dirname, pardir import pytest -import six # Make the list of all tests that we will have to execute (each in an independent pytest runner) THIS_DIR = dirname(__file__) @@ -60,6 +59,6 @@ def test_run_all_tests(test_to_run, testdir): # Here we check that everything is ok try: result.assert_outcomes(**asserts_dct) - except Exception as e: + except Exception: print("Error while asserting that %s results in %s" % (test_to_run, str(asserts_dct))) - six.raise_from(e, e) + raise diff --git a/pytest_harvest/tests/test_lazy_and_harvest.py b/pytest_harvest/tests/test_lazy_and_harvest.py index d5b729d..fa45daf 100644 --- a/pytest_harvest/tests/test_lazy_and_harvest.py +++ b/pytest_harvest/tests/test_lazy_and_harvest.py @@ -1,14 +1,10 @@ import pytest -from distutils.version import LooseVersion from pytest_cases import lazy_value, fixture_ref, parametrize, fixture from pytest_harvest import get_session_synthesis_dct -pytest2 = LooseVersion(pytest.__version__) < LooseVersion("3.0.0") - - @fixture def b(): return 1, "hello" @@ -56,7 +52,7 @@ def test_synthesis(request, module_results_df): dct = get_session_synthesis_dct(request, filter=test_foo2, test_id_format="function") assert len(dct) == 1 - name = "test_foo2[foo]" if not pytest2 else "test_foo2[foo[0]-foo[1]]" + name = "test_foo2[foo]" i_param = dct[name]["pytest_params"]["i"] assert not isinstance(i_param, int) assert str(i_param) == "foo[0]" diff --git a/setup.cfg b/setup.cfg index 28490b5..9dc9194 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,13 +20,11 @@ classifiers = Topic :: Software Development :: Libraries :: Python Modules Topic :: Software Development :: Testing Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Framework :: Pytest [options] @@ -37,22 +35,15 @@ setup_requires = install_requires = decopatch makefun>=1.5 + pytest>=6 # note: pytest, too :) # note: do not use double quotes in these, this triggers a weird bug in PyCharm in debug mode only - funcsigs;python_version<'3.3' - # TODO remove and use common_mini_six.py - six - pathlib2;python_version<'3.2' tests_require = pytest numpy pandas tabulate pytest-cases>=2.3.0 - # for some reason these pytest dependencies were not declared in old versions of pytest - six;python_version<'3.6' - attr;python_version<'3.6' - pluggy;python_version<'3.6' # test_suite = tests --> no need apparently # @@ -86,7 +77,7 @@ pytest11 = [bdist_wheel] # Code is written to work on both Python 2 and Python 3. -universal=1 +universal=0 # ------------- Others ------------- # In order to be able to execute 'python setup.py test' diff --git a/setup.py b/setup.py index 7ca6821..3d4f749 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ pkg_resources.require("setuptools>=39.2") pkg_resources.require("setuptools_scm") +pkg_resources.require("packaging") # (2) Generate download url using git version @@ -32,6 +33,7 @@ # (3) Call setup() with as little args as possible setup( download_url=DOWNLOAD_URL, + python_requires='>=3.8', use_scm_version={ "write_to": "pytest_harvest/_version.py" }, # we can't put `use_scm_version` in setup.cfg yet unfortunately