Skip to content
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

bpo-37324: Remove ABC aliases from collections #23754

Merged
merged 1 commit into from
Jan 12, 2021

Conversation

hugovk
Copy link
Member

@hugovk hugovk commented Dec 13, 2020

Remove deprecated aliases to Abstract Base Classes from the collections module.

This is an updated version of @vstinner's #14171, but for Python 3.10.

PR #14171 was originally for 3.9, but closed as it was decided to postpone the removal to 3.10 (#18545) to give 3rd-party modules like html5lib more time to update.

html5lib updated in html5lib/html5lib-python#403 and released in version 1.1 (June 2020) https://github.com/html5lib/html5lib-python/releases/tag/1.1.

@vstinner I just put your name in Doc/whatsnew/3.10.rst because this is an updated version of your PR, let me know if I should update it.

https://bugs.python.org/issue37324

Remove deprecated aliases to Abstract Base Classes from the
collections module.
@vstinner
Copy link
Member

Right now, we are currently fighting with many incompatible changes of Python 3.10, so I would prefer to not add another incompatible change in Python 3.10. Maybe we can attempt that in an early stage of Python 3.11. Or maybe the deprecated alias can stay. I don't know.

There are many Fedora Python packages broken by Python 3.10:

cc @pablogsal (Python 3.10 release manager)

@rhettinger rhettinger requested review from serhiy-storchaka and removed request for rhettinger December 19, 2020 04:25
@rhettinger
Copy link
Contributor

FWIW, this has been pending for a long time. I don't think more time will help. Also, if something breaks, the fix is trivially simple.

@serhiy-storchaka
Copy link
Member

See also #20774.

@rhettinger, merge please one of these PRs.

@rhettinger
Copy link
Contributor

That isn't up to me. Try @pablogsal

@vstinner
Copy link
Member

I'm not against the change, but I would prefer to wait for Python 3.11 to push it. As I wrote previously, 3.10 already contains multiple incompatible changes and it's already painful to handle to deal with them. It's inconvenient to get new incompatible changes between alpha releases.

The ABC aliases in collections are deprecated, IMO they can stay for one more release. I propose to wait until 3.11 development cycle starts to push these changes (before Python 3.11.0 alpha1).


But I'm also fine if some people disagree and consider that it's still ok to push such changes after 3.10.0 alpha 1.

Hopefully, since the change was included in early Python 3.9 alpha/beta versions, many projects have already been fixed to use collections.abc. Moreover, most projects now dropped Python 2.7 support and so don't need the try/except ImportError dance to support Python 2 and Python 3 in the same code base. For example, pip was blocked by html5lib, but after months of stale PR, html5lib got released with the fix, and pip was updated to get the fix (it vendors a copy of html5lib).

More generally, I rely on @pablogsal wise decision :-) I understood that Pablo will also be Python 3.11 release manager, so he will have to deal with this change anyway :-)

@pablogsal
Copy link
Member

I have reviewed the PRs and previous discussing and my opinion is that is ok to go ahead with this for Python 3.10 release. This deprecation was scheduled for Python 3.8 and it has been deprecated 8 years ago. We also made an extension in Python 3.9. I think there is not that much value in making another extension. But as a Release Manager, I have no power or authority to decide what to do or what not to do in this situation, so to move forward the best thing is to discuss this in the Steering Council so we can discuss this in the first meeting and unblock the situation.

I will make comment back here with the result.

@pablogsal
Copy link
Member

We had an SC meeting on 2021-01-11 where this matter was discussed and the SC determined that as this was deprecated since Python 3.3 and was delayed twice (Python3.8 and Python 3.9) and the reason for the extension was to make the Python 2 deprecation a bit easier, we should go ahead and proceed with the removal. We understand and respect the will to not accumulate backwards incompatible changes in the Python 3.10 release but we think that another delay will not be very helpful in this aspect, given as well that the migration from the deprecated code is straightforward enough.

Thanks to everyone that participated in the discussion and thanks for exposing your views on the matter.

Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Ok, I respect the SC decision, let's remove it one more time ;-)

@vstinner vstinner merged commit c47c78b into python:master Jan 12, 2021
@vstinner
Copy link
Member

Thanks @hugovk, I merged your PR ;-) Welcome to the bright future with no deprecated aliases :-D

@vstinner
Copy link
Member

This incompatible change was announced at the beginning of the What's New in Python 3.9 document: https://docs.python.org/3.9/whatsnew/3.9.html#you-should-check-for-deprecationwarning-in-your-code

Aliases to Abstract Base Classes in the collections module, like collections.Mapping alias to collections.abc.Mapping, are kept for one last release for backward compatibility. They will be removed from Python 3.10.

adorilson pushed a commit to adorilson/cpython that referenced this pull request Mar 13, 2021
Remove deprecated aliases to Abstract Base Classes from the
collections module.
johnyf added a commit to tulip-control/tulip-control that referenced this pull request Jun 10, 2021
for compatibility with Python 3.10.
The package `nose` is not compatible with Python 3.10,
because it imports abstract base classes from the
module `collections`,

[`nose == 1.3.7`](https://pypi.org/project/nose/1.3.7/#history)
imports in Python 3.10 without error, but `nosetests` fails,
due to imports from the module `collections` of classes that
have moved to the module `collections.abc`. The error raised contains:

```
  File ".../.virtualenvs/.../lib/python3.10/site-packages/nose/suite.py", line 106, in _set_tests
    if isinstance(tests, collections.Callable) and not is_suite:
AttributeError: module 'collections' has no attribute 'Callable'
```

Relevant information about CPython changes in `collections` (removal of
ABCs):
- python/cpython#23754
- https://bugs.python.org/issue37324
- https://docs.python.org/3.10/library/collections.abc.html#collections-abstract-base-classes
- Deprecation since Python 3.3, present until Python 3.9:
      https://docs.python.org/3.9/library/collections.html#module-collections
- Removed in Python 3.10:
      https://docs.python.org/3.10/whatsnew/3.10.html#removed

The script `run_tests.py` is rewritten, based on `pytest` functionality.

The file `test/transys_mathset_test.py` has been changed to mode 100644
in order to not need an equivalent `pytest` option for option `--exe`
of `nose`.

In the test script `tests/mathfunc_test.py`, the function
`_get_state_input_output_pair_test` has been renamed to
`_get_state_input_pair` to avoid its name being matched by the
pattern `python_functions = test_* *_test` defined in the
configuration file `tests/pytest.ini`.
johnyf added a commit to tulip-control/tulip-control that referenced this pull request Jun 20, 2021
for compatibility with Python 3.10.
The package `nose` is not compatible with Python 3.10,
because it imports abstract base classes from the
module `collections`,

[`nose == 1.3.7`](https://pypi.org/project/nose/1.3.7/#history)
imports in Python 3.10 without error, but `nosetests` fails,
due to imports from the module `collections` of classes that
have moved to the module `collections.abc`. The error raised contains:

```
  File ".../.virtualenvs/.../lib/python3.10/site-packages/nose/suite.py", line 106, in _set_tests
    if isinstance(tests, collections.Callable) and not is_suite:
AttributeError: module 'collections' has no attribute 'Callable'
```

Relevant information about CPython changes in `collections` (removal of
ABCs):
- python/cpython#23754
- https://bugs.python.org/issue37324
- https://docs.python.org/3.10/library/collections.abc.html#collections-abstract-base-classes
- Deprecation since Python 3.3, present until Python 3.9:
      https://docs.python.org/3.9/library/collections.html#module-collections
- Removed in Python 3.10:
      https://docs.python.org/3.10/whatsnew/3.10.html#removed

The script `run_tests.py` is rewritten, based on `pytest` functionality.

The file `test/transys_mathset_test.py` has been changed to mode 100644
in order to not need an equivalent `pytest` option for option `--exe`
of `nose`.

In the test script `tests/mathfunc_test.py`, the function
`_get_state_input_output_pair_test` has been renamed to
`_get_state_input_pair` to avoid its name being matched by the
pattern `python_functions = test_* *_test` defined in the
configuration file `tests/pytest.ini`.
johnyf added a commit to tulip-control/dd that referenced this pull request Jun 20, 2021
for compatibility with Python 3.10.

- add `pytest >= 4.6.11` to argument `tests_require` of
  the function `setuptools.setup` in script `setup.py`
  This lower bound has been selected to ensure compatibility
  with both Python 3 and Python 2.7. See below for details.

- add `pytest >= 4.6.11` to file `requirements.txt`

- remove `nose` from argument `tests_require` of
  the function `setuptools.setup` in script `setup.py`

- remove `nose` from file `requirements.txt`

- update file `.travis.yml`

- remove the collection of coverage measurements on Travis CI,
  because coverage measurement is incorrect (lower than the
  real coverage, and even lower after switching to `pytest`)
  due to Cython coverage not being correctly collected with
  the build configuration currently used for testing.

  Also, negligible changes in coverage measurements affected
  the status of commits on GitHub, turning it to a red "X",
  which can give the false impression that the tests failed
  when they passed, and requires clicking on the "X" in order
  to see more information that clarifies that the tests
  actually passed.

- remove `coveralls` from file `requirements.txt`,
  because `coveralls` was used only on Travis CI.

- add a configuration file `tests/pytest.ini`
  and include it in `MANIFEST.in`

- ignore `.pytest_cache/` in `.gitignore`


Motivation
==========

The change from [`nose == 1.3.7`](
    https://pypi.org/project/nose/1.3.7/#history)
to [`pytest`](
    https://pypi.org/project/pytest)
is motivated by compatibility with Python 3.10.


`nose` is incompatible with Python 3.10
=======================================

The package `nose`, which was used to run the tests of `dd`,
is not compatible with Python 3.10 (for details,
read the last section below). And `nose` is unmaintained.

(Also, `nose` uses the `imp` module from Python's standard library,
which [is deprecated](https://docs.python.org/3.10/library/imp.html),
so may be removed in some future Python version.)


Summary of transition to `pytest`
=================================

In summary, using `pytest` with the existing tests requires adding
a [configuration file `tests/pytest.ini`](
    https://docs.pytest.org/en/latest/reference/customize.html#configuration-file-formats)
to tell `pytest` which functions, classes, and methods to collect tests
from (called "discovery" of tests).

The [parameter `--continue-on-collection-errors`](
    https://docs.pytest.org/en/latest/reference/reference.html#command-line-flags)
tells `pytest` to not stop in case any test module fails to import,
and continue with running the tests. The ability to run the tests when
some `dd` C extension modules are not installed is necessary.

After transitioning the tests to `pytest`, the tests have been confirmed
to run successfully:
- on Python 2.7 with `pytest == 4.6.11`, and
- on Python 3.9 with `pytest == 6.2.4`.


Failed attempts to use `unittest`
=================================

First, I tried to use [`unittest`](
    https://docs.python.org/3/library/unittest.html)
(part of CPython's standard library).
For writing `dd` tests, `unittest` suffices.

For *discovering* the tests, `unittest` seems to require that tests be
methods of subclasses of the class `unittest.TestCase`.
This is not the case in the tests of `dd` tests.
Using `pytest` allows changing the test runner from `nosetests`
with minimal changes to the tests themselves.

Test discovery using `unittest` could possibly be implemented by adding
a file `tests/__init__.py`, and defining in that file a function
`load_tests`, following the [documentation of `unittest`](
    https://docs.python.org/3/library/unittest.html#unittest.TestLoader.discover).
In any case, it is simpler to use `pytest`, which requires only
a configuration file.

If `unittest` encounters an `ImportError` during collection of the tests
(i.e., when it tries to import test modules), then it stops. There does
not appear to be any way to tell `unittest` to continue and run the rest of
the test modules (those that *could* be imported).


Usage of `nose`
===============

The dependence on `nose` is minimal.
Only one function is used from `nose`:
the function `nose.tools.assert_raises`.
The function `assert_raises` is dynamically defined
[in the module `nose.tools.trivial`](
    https://github.com/nose-devs/nose/blob/release_1.3.7/nose/tools/trivial.py#L32-L54)
by instantiating the class `unittest.TestCase`,
and setting `assert_raises` to equal the
[bound method `assertRaises`](
    https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaises)
of the instance of `TestCase`.

So the function `assert_raises` from `nose` is just a PEP8-compliant
binding for the method `unittest.TestCase.assertRaises`.
Reading the code of `unittest`:
- https://github.com/python/cpython/blob/6fdc4d37f3fdbc1bd51f841be6e5e4708a3b8798/Lib/unittest/case.py#L156-L243
- https://github.com/python/cpython/blob/6fdc4d37f3fdbc1bd51f841be6e5e4708a3b8798/Lib/unittest/case.py#L704-L735

it follows that the existing usage:

```python
with nose.tools.assert_raises(AssertionError):
    foo(1, 2)
```

is equivalent to the following code
(the use of `AssertionError` here is just as an example):

```python
with unittest.TestCase().assertRaises(AssertionError):
    foo(1, 2)
```


Replacing usage of `nose` with `pytest` in test code
====================================================

The [context manager `pytest.raises`](
    https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises)
is a [drop-in replacement](
    https://en.wikipedia.org/wiki/Drop-in_replacement)
for the function `nose.tools.assert_raises`:

```python
with pytest.raises(AssertionError):
    foo(1, 2)
```

Also, the tests can still be run with `nosetests` on Python versions
where `nose` is still available.


Replacing the test runner `nosetests` with `pytest`
===================================================

- `pytest` correctly recognized the test files by default

- `pytest` does not recognize by default methods of classes that do not
  start with "Test" as test methods, even if the methods start with `test_`.
  The configuration file is necessary to change this behavior of `pytest`
  (in particular the command-line parameter `-k` did not seem to work
  for classes). Relevant documentation:
  - https://docs.pytest.org/en/latest/explanation/goodpractices.html#conventions-for-python-test-discovery
  - https://docs.pytest.org/en/latest/example/pythoncollection.html#changing-naming-conventions
  - https://docs.pytest.org/en/latest/how-to/nose.html
  - https://docs.pytest.org/en/latest/reference/reference.html#confval-python_classes
  - https://docs.pytest.org/en/latest/reference/reference.html#confval-python_functions
  - https://docs.pytest.org/en/latest/reference/reference.html#confval-python_files

- The call `pytest tests/foo_test.py` imports the package `dd` from
  `site-packages` (assuming that the module `foo_test.py` contains the
  statement `import dd`). So the default behavior of `pytest` is as
desired.

  In contrast, `nosetests tests/foo_test.py` imports the package `dd`
from
  the local directory `dd/`, even though `dd` *is* installed under
  `site-packages`.

  In any case, `pytest` is called from within the directory `tests/`,
  as was done for `nosetests`.

  `python -m pytest tests/foo_test.py` and
  `PYTHONPATH=. pytest tests/foo_test.py` both result in importing `dd`
  from the local directory `dd/`. Relevant documentation:
  - https://docs.pytest.org/en/latest/explanation/pythonpath.html#invoking-pytest-versus-python-m-pytest
  - https://docs.pytest.org/en/latest/how-to/usage.html#invoke-python

As remarked above, the `pytest` [parameter `--continue-on-collection-errors`](
    https://docs.pytest.org/en/latest/reference/reference.html#command-line-flags)
needs to be used for running the tests when some of the C extension modules are
not installed, for example:

```
cd tests/
pytest -v --continue-on-collection-errors .
```

or, to also activate [Python's development mode](
    https://docs.python.org/3/library/devmode.html):

```
cd tests/
python -X dev -m pytest -vvv --continue-on-collection-errors .
```


Colored output of test results from `pytest`
============================================

With `nose`, I used to use [`rednose`](https://pypi.org/project/rednose/)
for coloring test results, which was convenient.
`pytest` colors its output by default, no plugin is required.

This capability is an optional way of viewing test results,
so the coloring comparison is mentioned only for completeness.

Observations about `pytest`:
- shows colored source code that includes more source lines
- detects assertions that failed, and marks their source lines
- avoids the deprecated `imp` module (standard library) that `nose` uses
  (and thus the associated `DeprecationWarning`)
  https://docs.python.org/3.10/library/imp.html
- running the tests of `dd` with `pytest` revealed several
  `DeprecationWarnings` that were previously hidden by `nose`
  (these warnings were about invalid escape sequences due to
   backslashes appearing in non-raw strings).


Further remarks
===============

[`pytest == 6.2.4`](https://pypi.org/project/pytest/6.2.4) is not
compatible with Python 2.7.
[`pytest == 4.6.11`](https://pypi.org/project/pytest/4.6.11/)
is the latest version of `pytest` that is compatible with Python 2.7
(released on June 5, 2020).

`pytest` specifies `python_requires` [PEP 345](
    https://www.python.org/dev/peps/pep-0345/#requires-python),
[PEP 503](
    https://www.python.org/dev/peps/pep-0503/):
- https://github.com/pytest-dev/pytest/blob/4.6.11/setup.cfg#L48
- https://github.com/pytest-dev/pytest/blob/5.0.0/setup.cfg#L43

So including `pytest>=4.6.11` in the file `requirements.txt` suffices to
install, on each Python version, the latest version of `pytest` that is
compatible with that Python version.

This simplifies testing on CI, and packaging.
In other words, conditional installations in the file `.travis.yml` are
not needed for `pytest`, neither conditional definition of `tests_require`
in the script `setup.py`.

This approach leaves implicit the upper bound on `pytest` in
`tests_require`. This upper bound is specified explicitly by
`pytest` itself, depending on the Python version of the interpreter.

It appears that `pip == 9.0.0` and `setuptools == 24.2.0` are required
to correctly implement `python_requires`:
- https://pip.pypa.io/en/stable/news/#v9-0-0
- https://setuptools.readthedocs.io/en/latest/history.html#v24-2-1


How replacing usage of `nose` with `unittest` would have looked like
====================================================================

A way to replace `nose` could have been to add a module `tests/utils.py`
containing:

```python
"""Common functionality for tests."""
import unittest

_test_case = unittest.TestCase()
assert_raises = _test_case.assertRaises
```

which is close to what `nose` does. The function `assert_raises` could
then be imported from the module `utils` in test modules, and used.

Using `pytest` avoids the need for this workaround.


Details about the incompatibility of `nose` with Python 3.10
============================================================

[`nose == 1.3.7`](https://pypi.org/project/nose/1.3.7/#history)
imports in Python 3.10 fine, but `nosetests` fails, due to imports
from the module `collections` of classes that have moved to the
module `collections.abc`.

Relevant information about CPython changes in `collections`
(removal of ABCs):
- python/cpython#23754
- https://bugs.python.org/issue37324
- https://docs.python.org/3.10/library/collections.abc.html#collections-abstract-base-classes
- Deprecation since Python 3.3, present until Python 3.9:
      https://docs.python.org/3.9/library/collections.html#module-collections
- Removed in Python 3.10:
      https://docs.python.org/3.10/whatsnew/3.10.html#removed


About skipping tests
====================

The decorator `unittest.skipIf` is recognized by `pytest`, and skipped
tests are correctly recorded and reported. In any case, note also
the `pytest` test-skipping facilities:
- https://docs.pytest.org/en/latest/how-to/skipping.html
- https://docs.pytest.org/en/latest/how-to/unittest.html
- https://docs.pytest.org/en/latest/example/simple.html#control-skipping-of-tests-according-to-command-line-option


About passing `-X dev` to `python` in the `Makefile`
====================================================

The argument `dev` is available for the `python` option
[`-X` only on Python 3.7 and higher](
    https://docs.python.org/3/library/devmode.html#devmode).

So the `Makefile` rules where `-X dev` appears are
not compatible with ealier Python versions supported by `dd`.
This is not an issue: the development environment is
intended to be Python 3.9 or higher, so there is no issue
with using `-X dev`.


Avoiding interaction between tests via class attributes
=======================================================

Avoid class attributes in test classes. Use [data attributes][1]
instead. Initialize the data attributes in setup methods of the
test classes, as is common practice. This approach avoids
interaction (via class attributes) between test scripts that
import the same modules of common tests.

With `nose`, this kind of interaction apparently did not
occur, as observed by test failures that were expected to happen.
However, `pytest` apparently runs tests in a way that
changes to imported modules (e.g., class attributes)
persist between different test scripts.

This `pytest` behavior was observed by the disappearance of
test failures when running with `pytest` (the test failures
were observable with `pytest` only when telling `pytest` to
run individual test scripts, instead of collecting tests from
all test scripts.

The cause of the issue with `pytest` was the modification of
class attributes (not [data attributes][1]) from the importing
module, of classes in the imported module. The modifications
were done by setting the class attribute `DD` that defines the
BDD or ZDD manager class. Both the scripts `cudd_test.py` and
`autoref_test.py` made modifications. The end result was
`autoref` tests being run using the class `dd.cudd.BDD`.

Using data attributes instead of class attributes,
and subclassing, avoids this kind of erroneous testing.
This approach is explicit [PEP 20][4].

Also, note that the `pytest` extension packages
[`pytest-xdist`][2] and [`pytest-forked`][3]
do not avoid this issue.

[1]: https://docs.python.org/3/tutorial/classes.html#instance-objects
[2]: https://github.com/pytest-dev/pytest-xdist
[3]: https://github.com/pytest-dev/pytest-forked
[4]: https://www.python.org/dev/peps/pep-0020/
johnyf added a commit to tulip-control/tulip-control that referenced this pull request Aug 12, 2021
for compatibility with Python 3.10.
The package `nose` is not compatible with Python 3.10,
because it imports abstract base classes from the
module `collections`,

[`nose == 1.3.7`](https://pypi.org/project/nose/1.3.7/#history)
imports in Python 3.10 without error, but `nosetests` fails,
due to imports from the module `collections` of classes that
have moved to the module `collections.abc`. The error raised contains:

```
  File ".../.virtualenvs/.../lib/python3.10/site-packages/nose/suite.py", line 106, in _set_tests
    if isinstance(tests, collections.Callable) and not is_suite:
AttributeError: module 'collections' has no attribute 'Callable'
```

Relevant information about CPython changes in `collections` (removal of
ABCs):
- python/cpython#23754
- https://bugs.python.org/issue37324
- https://docs.python.org/3.10/library/collections.abc.html#collections-abstract-base-classes
- Deprecation since Python 3.3, present until Python 3.9:
      https://docs.python.org/3.9/library/collections.html#module-collections
- Removed in Python 3.10:
      https://docs.python.org/3.10/whatsnew/3.10.html#removed

The script `run_tests.py` is rewritten, based on `pytest` functionality.

The file `test/transys_mathset_test.py` has been changed to mode 100644
in order to not need an equivalent `pytest` option for option `--exe`
of `nose`.

In the test script `tests/mathfunc_test.py`, the function
`_get_state_input_output_pair_test` has been renamed to
`_get_state_input_output_pair` to avoid its name being matched by the
pattern `python_functions = test_* *_test` defined in the
configuration file `tests/pytest.ini`.
johnyf added a commit to tulip-control/tulip-control that referenced this pull request Aug 20, 2021
for compatibility with Python 3.10.
The package `nose` is not compatible with Python 3.10,
because it imports abstract base classes from the
module `collections`,

[`nose == 1.3.7`](https://pypi.org/project/nose/1.3.7/#history)
imports in Python 3.10 without error, but `nosetests` fails,
due to imports from the module `collections` of classes that
have moved to the module `collections.abc`. The error raised contains:

```
  File ".../.virtualenvs/.../lib/python3.10/site-packages/nose/suite.py", line 106, in _set_tests
    if isinstance(tests, collections.Callable) and not is_suite:
AttributeError: module 'collections' has no attribute 'Callable'
```

Relevant information about CPython changes in `collections` (removal of
ABCs):
- python/cpython#23754
- https://bugs.python.org/issue37324
- https://docs.python.org/3.10/library/collections.abc.html#collections-abstract-base-classes
- Deprecation since Python 3.3, present until Python 3.9:
      https://docs.python.org/3.9/library/collections.html#module-collections
- Removed in Python 3.10:
      https://docs.python.org/3.10/whatsnew/3.10.html#removed

The script `run_tests.py` is rewritten, based on `pytest` functionality.

The file `test/transys_mathset_test.py` has been changed to mode 100644
in order to not need an equivalent `pytest` option for option `--exe`
of `nose`.

In the test script `tests/mathfunc_test.py`, the function
`_get_state_input_output_pair_test` has been renamed to
`_get_state_input_output_pair` to avoid its name being matched by the
pattern `python_functions = test_* *_test` defined in the
configuration file `tests/pytest.ini`.
johnyf added a commit to tulip-control/dd that referenced this pull request Jan 11, 2022
for compatibility with Python 3.10.

- REL: add `pytest >= 4.6.11` to the argument
  `tests_require` of the function `setuptools.setup`
  in the script `setup.py`.
  This lower bound has been selected to
  ensure compatibility with both Python 3 and
  Python 2.7. See below for details.

- REL: add `pytest >= 4.6.11` to file `requirements.txt`

- REL: remove `nose` from argument `tests_require` of
  the function `setuptools.setup` in script `setup.py`

- REL: remove `nose` from file `requirements.txt`

- CI: update file `.travis.yml`

- CI: remove the collection of
  coverage measurements on Travis CI,
  because these measurements do not collect
  Cython coverage with the build configuration
  currently used for CI testing.

- DEV: remove `coveralls` from the
  file `requirements.txt`, because the
  package `coveralls` was used only on Travis CI.

- TST: add a configuration file `tests/pytest.ini`
  and include it in `MANIFEST.in`

- GIT: ignore `.pytest_cache/` in `.gitignore`


## Motivation

The change from [`nose == 1.3.7`](
    https://pypi.org/project/nose/1.3.7/#history)
to [`pytest`](
    https://pypi.org/project/pytest)
is motivated by compatibility with Python 3.10.


## `nose` is incompatible with Python 3.10

The package `nose`, which was used to run
the tests of `dd`, is not compatible with Python 3.10
(for details, read the last section below).

Also, `nose` uses the `imp` module from
CPython's standard library.
The module `imp` [is deprecated](
    https://docs.python.org/3.10/library/imp.html),
so it may be removed in some future Python version.


## Summary of transition to `pytest`

In summary, using `pytest` with the
existing tests requires adding
a [configuration file `tests/pytest.ini`](
    https://docs.pytest.org/en/latest/reference/customize.html#configuration-file-formats)
to tell `pytest` which functions, classes, and
methods to collect tests from
(called "discovery" of tests).

The [parameter `--continue-on-collection-errors`](
    https://docs.pytest.org/en/latest/reference/reference.html#command-line-flags)
tells `pytest` to not stop in case any
test module fails to import,
and to continue with running the tests.
The ability to run the tests when some
`dd` C extension modules are not installed is necessary.

After transitioning the tests to `pytest`,
the tests have been confirmed
to run successfully:
- on Python 2.7 with `pytest == 4.6.11`, and
- on Python 3.9 with `pytest == 6.2.4`.


## Considering using `unittest`

For writing `dd` tests, the module [`unittest`](
    https://docs.python.org/3/library/unittest.html)
(part of CPython's standard library) suffices.

For *discovering* the tests, `unittest` seems to
require that tests be methods of subclasses of
the class `unittest.TestCase`.
This is not the case in the tests of `dd` tests.
Using `pytest` allows changing the test runner from `nosetests`
with minimal changes to the tests themselves.

Test discovery using `unittest` could possibly be
implemented by adding a file `tests/__init__.py`, and
defining in that file a function `load_tests`,
following the [documentation of `unittest`](
    https://docs.python.org/3/library/unittest.html#unittest.TestLoader.discover).
In any case, it is simpler to use `pytest`,
which requires only a configuration file.

If `unittest` encounters an `ImportError` during
collection of the tests (i.e., when `unittest` tries to
import test modules), then it stops.
There does not appear to be any way to tell `unittest`
to continue and run the rest of the test modules
(those that *could* be imported).


## Usage of `nose`

The dependence on `nose` was minimal.
Only one function was used from `nose`:
the function `nose.tools.assert_raises`.
The function `assert_raises` is dynamically defined
[in the module `nose.tools.trivial`](
    https://github.com/nose-devs/nose/blob/release_1.3.7/nose/tools/trivial.py#L32-L54)
by instantiating the class `unittest.TestCase`,
and setting `assert_raises` to equal the
[bound method `assertRaises`](
    https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaises)
of the instance of `TestCase`.

So the function `assert_raises` from `nose` is
a PEP8-compliant binding for the method
`unittest.TestCase.assertRaises`.

Reading the code of `unittest`:
- <https://github.com/python/cpython/blob/6fdc4d37f3fdbc1bd51f841be6e5e4708a3b8798/Lib/unittest/case.py#L156-L243>
- <https://github.com/python/cpython/blob/6fdc4d37f3fdbc1bd51f841be6e5e4708a3b8798/Lib/unittest/case.py#L704-L735>

it follows that the usage:

```python
with nose.tools.assert_raises(AssertionError):
    foo(1, 2)
```

is equivalent to the following code
(`AssertionError` here is used as an example):

```python
with unittest.TestCase().assertRaises(AssertionError):
    foo(1, 2)
```


## Replacing usage of `nose` with `pytest` in test code

The [context manager `pytest.raises`](
    https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises)
is a [drop-in replacement](
    https://en.wikipedia.org/wiki/Drop-in_replacement)
for the function `nose.tools.assert_raises`:

```python
with pytest.raises(AssertionError):
    foo(1, 2)
```

Also, the tests can still be run with `nosetests`
on Python versions where `nose` is still available.


## Replacing the test runner `nosetests` with `pytest`

- `pytest` correctly recognized the
  test files by default

- `pytest` does not recognize by default methods of
  classes that do not start with "Test" as test methods.

  The configuration file is necessary for
  changing this behavior of `pytest`
  (in particular, the command-line parameter `-k`
   did not seem to work for classes).

  Relevant documentation:
  - <https://docs.pytest.org/en/latest/explanation/goodpractices.html#conventions-for-python-test-discovery>
  - <https://docs.pytest.org/en/latest/example/pythoncollection.html#changing-naming-conventions>
  - <https://docs.pytest.org/en/latest/how-to/nose.html>
  - <https://docs.pytest.org/en/latest/reference/reference.html#confval-python_classes>
  - <https://docs.pytest.org/en/latest/reference/reference.html#confval-python_functions>
  - <https://docs.pytest.org/en/latest/reference/reference.html#confval-python_files>

- The call `pytest tests/foo_test.py` imports
  the package `dd` from `site-packages`
  (assuming that the module `foo_test.py` contains
   the statement `import dd`).
  So the default behavior of `pytest` is
  as intended.

  In contrast, `nosetests tests/foo_test.py` imports
  the package `dd` from the local directory `dd/`,
  even though `dd` *is* installed under `site-packages`.

  In any case, `pytest` is called from within
  the directory `tests/`, as was done for `nosetests`.

  Both:
  - `python -m pytest tests/foo_test.py` and
  - `PYTHONPATH=. pytest tests/foo_test.py`

  result in importing `dd` from the local directory `dd/`.

  Relevant documentation:
  - <https://docs.pytest.org/en/latest/explanation/pythonpath.html#invoking-pytest-versus-python-m-pytest>
  - <https://docs.pytest.org/en/latest/how-to/usage.html#invoke-python>

As remarked above, the parameter
[`--continue-on-collection-errors`](
    https://docs.pytest.org/en/latest/reference/reference.html#command-line-flags)
of `pytest` needs to be used for running the tests
when some of the C extension modules are not installed.
For example:

```
cd tests/
pytest -v --continue-on-collection-errors .
```

To activate [Python's development mode](
    https://docs.python.org/3/library/devmode.html):

```
cd tests/
python -X dev -m pytest -vvv --continue-on-collection-errors .
```


## Further remarks

Observations about `pytest`:
- detects assertions that failed, and marks
  their source lines
- avoids the deprecated `imp` module (of the
  standard library) that `nose` uses
  (and thus the associated `DeprecationWarning`)
   <https://docs.python.org/3.10/library/imp.html>
- running the tests of `dd` with `pytest` revealed
  several `DeprecationWarning`s that were previously
  not shown by `nose` (these warnings were about
   invalid escape sequences due to
   backslashes appearing in non-raw strings).


[`pytest == 6.2.4`](https://pypi.org/project/pytest/6.2.4) is not
compatible with Python 2.7.
[`pytest == 4.6.11`](https://pypi.org/project/pytest/4.6.11/)
is the latest version of `pytest` that is compatible with Python 2.7
(released on June 5, 2020).

`pytest` specifies `python_requires` [PEP 345](
    https://www.python.org/dev/peps/pep-0345/#requires-python),
[PEP 503](
    https://www.python.org/dev/peps/pep-0503/):
- <https://github.com/pytest-dev/pytest/blob/4.6.11/setup.cfg#L48>
- <https://github.com/pytest-dev/pytest/blob/5.0.0/setup.cfg#L43>

So including `pytest>=4.6.11` in the file
`requirements.txt` suffices to install,
on each Python version, the latest version of
`pytest` that is compatible with that Python version.

This simplifies testing on CI, and packaging.
In other words, conditional installations in
the file `.travis.yml` are not needed for `pytest`,
neither conditional definition of `tests_require`
in the script `setup.py`.

This approach leaves implicit the upper bound on
`pytest` in `tests_require`.
This upper bound is specified explicitly by
`pytest` itself, depending on the Python version of
the interpreter.

It appears that `pip == 9.0.0` and
`setuptools == 24.2.0` are required,
to correctly implement `python_requires`:
- <https://pip.pypa.io/en/stable/news/#v9-0-0>
- <https://setuptools.readthedocs.io/en/latest/history.html#v24-2-1>


## How replacing usage of `nose` with `unittest` would have looked like

A way to replace `nose` could have been to
add a module `tests/utils.py` containing:

```python
"""Common functionality for tests."""
import unittest

_test_case = unittest.TestCase()
assert_raises = _test_case.assertRaises
```

which is close to what `nose` does.
The function `assert_raises` could then
be imported from the module `utils` in
test modules, and used.

Using `pytest` avoids the need for
this workaround.


## Details about the incompatibility of `nose` with Python 3.10

[`nose == 1.3.7`](
    https://pypi.org/project/nose/1.3.7/#history)
imports in Python 3.10. Running `nosetests` fails,
due to imports from the module `collections` of
classes that have moved to the module `collections.abc`.

Relevant information about CPython changes
in `collections` (removal of ABCs):
- <python/cpython#23754>
- <https://bugs.python.org/issue37324>
- <https://docs.python.org/3.10/library/collections.abc.html#collections-abstract-base-classes>
- Deprecation since Python 3.3,
  present until Python 3.9:
      <https://docs.python.org/3.9/library/collections.html#module-collections>
- Removed in Python 3.10:
      <https://docs.python.org/3.10/whatsnew/3.10.html#removed>


## About skipping tests

The decorator `unittest.skipIf` is recognized
by `pytest`, and skipped tests are correctly
recorded and reported. In any case, note also
the `pytest` test-skipping facilities:
- <https://docs.pytest.org/en/latest/how-to/skipping.html>
- <https://docs.pytest.org/en/latest/how-to/unittest.html>
- <https://docs.pytest.org/en/latest/example/simple.html#control-skipping-of-tests-according-to-command-line-option>


## About passing `-X dev` to `python` in the `Makefile`

The argument `dev` is available for the `python` option
[`-X` only on Python 3.7 and higher](
    https://docs.python.org/3/library/devmode.html#devmode).

So the `Makefile` rules where `-X dev` appears are
not compatible with ealier Python versions supported by `dd`.
This is not an issue: the development environment is
intended to be Python 3.9 or higher, so there is no issue
with using `-X dev`.


## Avoiding interaction between tests via class attributes

Avoid class attributes in test classes.
Use [data attributes][1] instead.
Initialize the data attributes in setup methods
of the test classes, as is common practice.

This approach avoids interaction (via class
attributes) between test scripts that
import the same modules of common tests.

With `nose`, this kind of interaction
apparently did not occur, as observed by
test failures that were expected to happen.

However, `pytest` apparently runs tests in
a way that changes to imported modules
(e.g., class attributes)
persist between different test scripts.

This `pytest` behavior was observed by
the disappearance of test failures when
running with `pytest` (the test failures
were observable with `pytest` only when
telling `pytest` to run individual test
scripts, instead of collecting tests from
all test scripts.

The cause of the issue with `pytest` was
the modification of class attributes
(not [data attributes][1]) from the
importing module, of classes in the
imported module.

The modifications were done by setting the
class attribute `DD` that defines the BDD or
ZDD manager class. Both the scripts
`cudd_test.py` and `autoref_test.py` made
modifications. The end result was
`autoref` tests being run using the
class `dd.cudd.BDD`.

Using data attributes, instead of class
attributes, and subclassing, avoids this
kind of erroneous testing.
This approach is explicit [PEP 20][4].

[1]: https://docs.python.org/3/tutorial/classes.html#instance-objects
[2]: https://github.com/pytest-dev/pytest-xdist
[3]: https://github.com/pytest-dev/pytest-forked
[4]: https://www.python.org/dev/peps/pep-0020/
johnyf added a commit to tulip-control/dd that referenced this pull request Jan 11, 2022
for compatibility with Python 3.10.

- REL: add `pytest >= 4.6.11` to the argument
  `tests_require` of the function `setuptools.setup`
  in the script `setup.py`.
  This lower bound has been selected to
  ensure compatibility with both Python 3 and
  Python 2.7. See below for details.

- REL: add `pytest >= 4.6.11` to file `requirements.txt`

- REL: remove `nose` from argument `tests_require` of
  the function `setuptools.setup` in script `setup.py`

- REL: remove `nose` from file `requirements.txt`

- CI: update file `.travis.yml`

- CI: remove the collection of
  coverage measurements on Travis CI,
  because these measurements do not collect
  Cython coverage with the build configuration
  currently used for CI testing.

- DEV: remove `coveralls` from the
  file `requirements.txt`, because the
  package `coveralls` was used only on Travis CI.

- TST: add a configuration file `tests/pytest.ini`
  and include it in `MANIFEST.in`

- GIT: ignore `.pytest_cache/` in `.gitignore`


## Motivation

The change from [`nose == 1.3.7`](
    https://pypi.org/project/nose/1.3.7/#history)
to [`pytest`](
    https://pypi.org/project/pytest)
is motivated by compatibility with Python 3.10.


## `nose` is incompatible with Python 3.10

The package `nose`, which was used to run
the tests of `dd`, is not compatible with Python 3.10
(for details, read the last section below).

Also, `nose` uses the `imp` module from
CPython's standard library.
The module `imp` [is deprecated](
    https://docs.python.org/3.10/library/imp.html),
so it may be removed in some future Python version.


## Summary of transition to `pytest`

In summary, using `pytest` with the
existing tests requires adding
a [configuration file `tests/pytest.ini`](
    https://docs.pytest.org/en/latest/reference/customize.html#configuration-file-formats)
to tell `pytest` which functions, classes, and
methods to collect tests from
(called "discovery" of tests).

The [parameter `--continue-on-collection-errors`](
    https://docs.pytest.org/en/latest/reference/reference.html#command-line-flags)
tells `pytest` to not stop in case any
test module fails to import,
and to continue with running the tests.
The ability to run the tests when some
`dd` C extension modules are not installed is necessary.

After transitioning the tests to `pytest`,
the tests have been confirmed
to run successfully:
- on Python 2.7 with `pytest == 4.6.11`, and
- on Python 3.9 with `pytest == 6.2.4`.


## Considering using `unittest`

For writing `dd` tests, the module [`unittest`](
    https://docs.python.org/3/library/unittest.html)
(part of CPython's standard library) suffices.

For *discovering* the tests, `unittest` seems to
require that tests be methods of subclasses of
the class `unittest.TestCase`.
This is not the case in the tests of `dd` tests.
Using `pytest` allows changing the test runner from `nosetests`
with minimal changes to the tests themselves.

Test discovery using `unittest` could possibly be
implemented by adding a file `tests/__init__.py`, and
defining in that file a function `load_tests`,
following the [documentation of `unittest`](
    https://docs.python.org/3/library/unittest.html#unittest.TestLoader.discover).
In any case, it is simpler to use `pytest`,
which requires only a configuration file.

If `unittest` encounters an `ImportError` during
collection of the tests (i.e., when `unittest` tries to
import test modules), then it stops.
There does not appear to be any way to tell `unittest`
to continue and run the rest of the test modules
(those that *could* be imported).


## Usage of `nose`

The dependence on `nose` was minimal.
Only one function was used from `nose`:
the function `nose.tools.assert_raises`.
The function `assert_raises` is dynamically defined
[in the module `nose.tools.trivial`](
    https://github.com/nose-devs/nose/blob/release_1.3.7/nose/tools/trivial.py#L32-L54)
by instantiating the class `unittest.TestCase`,
and setting `assert_raises` to equal the
[bound method `assertRaises`](
    https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaises)
of the instance of `TestCase`.

So the function `assert_raises` from `nose` is
a PEP8-compliant binding for the method
`unittest.TestCase.assertRaises`.

Reading the code of `unittest`:
- <https://github.com/python/cpython/blob/6fdc4d37f3fdbc1bd51f841be6e5e4708a3b8798/Lib/unittest/case.py#L156-L243>
- <https://github.com/python/cpython/blob/6fdc4d37f3fdbc1bd51f841be6e5e4708a3b8798/Lib/unittest/case.py#L704-L735>

it follows that the usage:

```python
with nose.tools.assert_raises(AssertionError):
    foo(1, 2)
```

is equivalent to the following code
(`AssertionError` here is used as an example):

```python
with unittest.TestCase().assertRaises(AssertionError):
    foo(1, 2)
```


## Replacing usage of `nose` with `pytest` in test code

The [context manager `pytest.raises`](
    https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises)
is a [drop-in replacement](
    https://en.wikipedia.org/wiki/Drop-in_replacement)
for the function `nose.tools.assert_raises`:

```python
with pytest.raises(AssertionError):
    foo(1, 2)
```

Also, the tests can still be run with `nosetests`
on Python versions where `nose` is still available.


## Replacing the test runner `nosetests` with `pytest`

- `pytest` correctly recognized the
  test files by default

- `pytest` does not recognize by default methods of
  classes that do not start with "Test" as test methods.

  The configuration file is necessary for
  changing this behavior of `pytest`
  (in particular, the command-line parameter `-k`
   did not seem to work for classes).

  Relevant documentation:
  - <https://docs.pytest.org/en/latest/explanation/goodpractices.html#conventions-for-python-test-discovery>
  - <https://docs.pytest.org/en/latest/example/pythoncollection.html#changing-naming-conventions>
  - <https://docs.pytest.org/en/latest/how-to/nose.html>
  - <https://docs.pytest.org/en/latest/reference/reference.html#confval-python_classes>
  - <https://docs.pytest.org/en/latest/reference/reference.html#confval-python_functions>
  - <https://docs.pytest.org/en/latest/reference/reference.html#confval-python_files>

- The call `pytest tests/foo_test.py` imports
  the package `dd` from `site-packages`
  (assuming that the module `foo_test.py` contains
   the statement `import dd`).
  So the default behavior of `pytest` is
  as intended.

  In contrast, `nosetests tests/foo_test.py` imports
  the package `dd` from the local directory `dd/`,
  even though `dd` *is* installed under `site-packages`.

  In any case, `pytest` is called from within
  the directory `tests/`, as was done for `nosetests`.

  Both:
  - `python -m pytest tests/foo_test.py` and
  - `PYTHONPATH=. pytest tests/foo_test.py`

  result in importing `dd` from the local directory `dd/`.

  Relevant documentation:
  - <https://docs.pytest.org/en/latest/explanation/pythonpath.html#invoking-pytest-versus-python-m-pytest>
  - <https://docs.pytest.org/en/latest/how-to/usage.html#invoke-python>

As remarked above, the parameter
[`--continue-on-collection-errors`](
    https://docs.pytest.org/en/latest/reference/reference.html#command-line-flags)
of `pytest` needs to be used for running the tests
when some of the C extension modules are not installed.
For example:

```
cd tests/
pytest -v --continue-on-collection-errors .
```

To activate [Python's development mode](
    https://docs.python.org/3/library/devmode.html):

```
cd tests/
python -X dev -m pytest -vvv --continue-on-collection-errors .
```


## Further remarks

Observations about `pytest`:
- detects assertions that failed, and marks
  their source lines
- avoids the deprecated `imp` module (of the
  standard library) that `nose` uses
  (and thus the associated `DeprecationWarning`)
   <https://docs.python.org/3.10/library/imp.html>
- running the tests of `dd` with `pytest` revealed
  several `DeprecationWarning`s that were previously
  not shown by `nose` (these warnings were about
   invalid escape sequences due to
   backslashes appearing in non-raw strings).


[`pytest == 6.2.4`](https://pypi.org/project/pytest/6.2.4) is not
compatible with Python 2.7.
[`pytest == 4.6.11`](https://pypi.org/project/pytest/4.6.11/)
is the latest version of `pytest` that is compatible with Python 2.7
(released on June 5, 2020).

`pytest` specifies `python_requires` [PEP 345](
    https://www.python.org/dev/peps/pep-0345/#requires-python),
[PEP 503](
    https://www.python.org/dev/peps/pep-0503/):
- <https://github.com/pytest-dev/pytest/blob/4.6.11/setup.cfg#L48>
- <https://github.com/pytest-dev/pytest/blob/5.0.0/setup.cfg#L43>

So including `pytest>=4.6.11` in the file
`requirements.txt` suffices to install,
on each Python version, the latest version of
`pytest` that is compatible with that Python version.

This simplifies testing on CI, and packaging.
In other words, conditional installations in
the file `.travis.yml` are not needed for `pytest`,
neither conditional definition of `tests_require`
in the script `setup.py`.

This approach leaves implicit the upper bound on
`pytest` in `tests_require`.
This upper bound is specified explicitly by
`pytest` itself, depending on the Python version of
the interpreter.

It appears that `pip == 9.0.0` and
`setuptools == 24.2.0` are required,
to correctly implement `python_requires`:
- <https://pip.pypa.io/en/stable/news/#v9-0-0>
- <https://setuptools.readthedocs.io/en/latest/history.html#v24-2-1>


## How replacing usage of `nose` with `unittest` would have looked like

A way to replace `nose` could have been to
add a module `tests/utils.py` containing:

```python
"""Common functionality for tests."""
import unittest

_test_case = unittest.TestCase()
assert_raises = _test_case.assertRaises
```

which is close to what `nose` does.
The function `assert_raises` could then
be imported from the module `utils` in
test modules, and used.

Using `pytest` avoids the need for
this workaround.


## Details about the incompatibility of `nose` with Python 3.10

[`nose == 1.3.7`](
    https://pypi.org/project/nose/1.3.7/#history)
imports in Python 3.10. Running `nosetests` fails,
due to imports from the module `collections` of
classes that have moved to the module `collections.abc`.

Relevant information about CPython changes
in `collections` (removal of ABCs):
- <python/cpython#23754>
- <https://bugs.python.org/issue37324>
- <https://docs.python.org/3.10/library/collections.abc.html#collections-abstract-base-classes>
- Deprecation since Python 3.3,
  present until Python 3.9:
      <https://docs.python.org/3.9/library/collections.html#module-collections>
- Removed in Python 3.10:
      <https://docs.python.org/3.10/whatsnew/3.10.html#removed>


## About skipping tests

The decorator `unittest.skipIf` is recognized
by `pytest`, and skipped tests are correctly
recorded and reported. In any case, note also
the `pytest` test-skipping facilities:
- <https://docs.pytest.org/en/latest/how-to/skipping.html>
- <https://docs.pytest.org/en/latest/how-to/unittest.html>
- <https://docs.pytest.org/en/latest/example/simple.html#control-skipping-of-tests-according-to-command-line-option>


## About passing `-X dev` to `python` in the `Makefile`

The argument `dev` is available for the `python` option
[`-X` only on Python 3.7 and higher](
    https://docs.python.org/3/library/devmode.html#devmode).

So the `Makefile` rules where `-X dev` appears are
not compatible with ealier Python versions supported by `dd`.
This is not an issue: the development environment is
intended to be Python 3.9 or higher, so there is no issue
with using `-X dev`.


## Avoiding interaction between tests via class attributes

Avoid class attributes in test classes.
Use [data attributes][1] instead.
Initialize the data attributes in setup methods
of the test classes, as is common practice.

This approach avoids interaction (via class
attributes) between test scripts that
import the same modules of common tests.

With `nose`, this kind of interaction
apparently did not occur, as observed by
test failures that were expected to happen.

However, `pytest` apparently runs tests in
a way that changes to imported modules
(e.g., class attributes)
persist between different test scripts.

This `pytest` behavior was observed by
the disappearance of test failures when
running with `pytest` (the test failures
were observable with `pytest` only when
telling `pytest` to run individual test
scripts, instead of collecting tests from
all test scripts.

The cause of the issue with `pytest` was
the modification of class attributes
(not [data attributes][1]) from the
importing module, of classes in the
imported module.

The modifications were done by setting the
class attribute `DD` that defines the BDD or
ZDD manager class. Both the scripts
`cudd_test.py` and `autoref_test.py` made
modifications. The end result was
`autoref` tests being run using the
class `dd.cudd.BDD`.

Using data attributes, instead of class
attributes, and subclassing, avoids this
kind of erroneous testing.
This approach is explicit [PEP 20][4].

[1]: https://docs.python.org/3/tutorial/classes.html#instance-objects
[2]: https://github.com/pytest-dev/pytest-xdist
[3]: https://github.com/pytest-dev/pytest-forked
[4]: https://www.python.org/dev/peps/pep-0020/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants