Skip to content

Commit

Permalink
Merge pull request #30 from cclauss/patch-2
Browse files Browse the repository at this point in the history
Update README.rst to recommend pipx run or install
  • Loading branch information
nicoddemus authored Apr 29, 2024
2 parents 4de8785 + 335a39f commit 2964c29
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 62 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ jobs:
matrix:
python-version: ["3.6", "3.7.7", "3.8", "3.9", "3.10", "3.11"]
os: [ubuntu-20.04, macos-latest, windows-latest]
exclude: # Apple Silicon ARM64 does not support Python < v3.8
- python-version: "3.6"
os: macos-latest
- python-version: "3.7.7"
os: macos-latest
include: # So run those legacy versions on Intel CPUs
- python-version: "3.6"
os: macos-13
- python-version: "3.7.7"
os: macos-13

steps:
- uses: actions/checkout@v4
Expand Down
108 changes: 46 additions & 62 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Overview

This package provides a Python script and pytest plugin to help convert Nose-based tests into pytest-based
tests. Specifically, the script transforms ``nose.tools.assert_*`` function calls into raw assert statements,
while preserving format of original arguments as much as possible. For example, the script:
while preserving the format of original arguments as much as possible. For example, the script:

.. code-block:: python
Expand All @@ -31,35 +31,36 @@ transformed because there is no raw assert statement equivalent, or the equivale
maintain. They are provided as functions in the pytest namespace via pytest's plugin system.


Installation
-------------

From a command shell run ::

pip install nose2pytest

This puts an executable file in ``<python-root>/Scripts`` with *python-root* being the root folder of the
Python installation from which ``pip`` was run.


Running
------------

From a command shell, ::
For a one-time conversion use the shell command ::

nose2pytest path/to/dir/with/python_files
pipx run --python 3.11 nose2pytest path/to/dir/with/python_files
This will find all ``.py`` files in the folder tree starting at ``path/to/dir/with/python_files`` and
overwrite the original (assuming most users will be running this on a version-controlled code base, this is
almost always what would be most convenient). Type ``nose2pytest -h`` for other options, such as ``-v``.


Installation
-------------

For doing multiple conversions use the shell command ::

pipx install --python 3.11 nose2pytest

For each conversion use the shell command ::

nose2pytest path/to/dir/with/python_files


Motivation
------------

I have used Nose for years and it is a great tool. However, to get good test failure diagnostics with Nose you
ought to use the ``assert_*()`` functions from ``nose.tools``. Although they provide very good diagnostics, they
are not as convenient to use as raw assertions, since you have to decide before hand what type of assertion you
are not as convenient to use as raw assertions, since you have to decide beforehand what type of assertion you
are going to write: an identity comparison to None, a truth check, a falseness check, an identity comparison to another
object, etc. Just being able to write a raw assertion, and still get good diagnostics on failure as done by
pytest, is really nice. This is a main reason for using pytest for me. Another reason is the design of fixtures
Expand All @@ -72,7 +73,7 @@ manually, to get the same test coverage and results. A few gotchas:
- test classes that have ``__init__`` will be ignored, those will have to be moved (usually, into class's
``setup_class()``)
- the ``setup.cfg`` may have to be edited since test discovery rules are slightly more strict with pytest
- the order of tests may be different, but in general that should not matter
- the order of tests may be different, but in general, that should not matter
- all test modules are imported up-front, so some test modules may need adjustment such as moving some
code from the top of the test module into its ``setup_module()``

Expand All @@ -83,7 +84,7 @@ uses ``nose.tools.assert_*`` functions, yet with pytest you can use raw assertio
these two approaches should a developer use? If you modify existing tests, should new assertions use raw assert?
Should the remaining test method, test class, or test module be updated? A test module can contain hundreds of
calls to ``nose.tools.assert_*`` functions, is a developer to manually go through each one to convert it? Painful and
error prone, in general not feasible to do manually.
error-prone, in general not feasible to do manually.

This is why I developed nose2pytest: I wanted to migrate my pypubsub project's test suite from Nose to pytest,
but also have only pytest as a dependency, and have one obvious way to write assertions in the test suite.
Expand All @@ -92,14 +93,9 @@ but also have only pytest as a dependency, and have one obvious way to write ass
Requirements
-------------

I expect nose2pytest script to run with Python >= 3.4, to correctly convert Python test suite >= 2.7, on any
OS supported by a version of python that has lib2to3 compatible with Python 3.4's lib2to3. I expect it to
succeed even with quite old versions of Nose (even prior to 1.0 which came out ca. 2010), and with the new
Nose2 test driver.

Note however that I have run the script only with Python 3.4, to convert Python 3.4 test suites based on
Nose 1.3.7 on Windows 7 Pro 64. If you have successfully used nose2pytest with other combinations, please
kindly let me know (via github).
I expect nose2pytest script to run with supported versions of CPython <= v3.11, on any OS supported by a version of
Python that has lib2to3 compatible with fissix. I expect it to succeed even with quite old versions of Nose (even
prior to 1.0 which came out ca. 2010) and with the new Nose2 test driver.

The pytest package namespace will be extended with ``assert_`` functions that are not converted by the script
only if, err, you have pytest installed!
Expand Down Expand Up @@ -207,7 +203,7 @@ Limitations

- The script does not convert ``nose.tools.assert_`` import statements as there are too many possibilities.
Should ``from nose.tools import ...`` be changed to ``from pytest import ...``, and the implemented
conversions removed? Should an ``import pytest`` statement be added, and if so, where? If it is added after
conversions be removed? Should an ``import pytest`` statement be added, and if so, where? If it is added after
the line that had the ``nose.tools`` import, is the previous line really needed? Indeed the ``assert_``
functions added in the ``pytest`` namespace could be accessed via ``pytest.assert_``, in which case the
script should prepend ``pytest.`` and remove the ``from nose.tools import ...`` entirely. Too many options,
Expand All @@ -229,30 +225,17 @@ Limitations
import bogo.assert_true
bogo.assert_true(...) # should this one be converted?
The possiblities are endless so supporting this would require such a large amount of time that I
The possibilities are endless so supporting this would require such a large amount of time that I
do not have. As with other limitations in this section

- Nose functions that can be used as context managers can obviously not be converted to raw assertions.
However, there is currently no way of preventing nose2pytest from converting Nose functions used this way.
You will have to manually fix.

- The lib2to3 package that nose2pytest relies on assumes python 2.7 syntax as input. The only issue that
this has caused so far on code base of 20k lines of python 3.4 *test* code (i.e. the source code does not
matter, as none of the test code, such as import statements, is actually run) are keywords like ``exec``
and ``print``, which in Python 2.x were statements, whereas they are functions in Python 3.x. This means
that in Python 3.x, a method can be named ``exec()`` or ``print()``, whereas this would lead to a syntax
error in Python 2.7. Some libraries that do not support 2.x take advantage of this (like PyQt5). Any
occurrence of these two keywords as methods in your test code will cause the script to fail converting
anything.

The work around is, luckily, simple: do a global search-replace of ``\.exec\(`` for ``.exec__(`` in your
test folder, run nose2pytest, then reverse the search-replace (do a global search-replace of ``\.exec__\(``
for ``.exec(``).


- ``@raises``: this decorator can be replaced via the regular expression ``@raises\((.*)\)`` to
``@pytest.mark.xfail(raises=$1)``,
but I prefer instead to convert such decorated test functions to use ``pytest.raises`` in the test function body.
Indeed, it is easy to forget the decorator, and add code after the line that raises, but this code will never
Indeed, it is easy to forget the decorator and add code after the line that raises, but this code will never
be run and you won't know. Using the ``pytest.raises(...)`` is better than ``xfail(raise=...)``.

- Nose2pytest does not have a means of determining if an assertion function is inside a lambda expression, so
Expand Down Expand Up @@ -281,16 +264,17 @@ should be able to run both a unittest2pytest converter, then the nose2pytest con
Solution Notes
---------------

I don't think this script would have been possible without lib2to3, certainly not with the same functionality since
lib2to3, due to its purpose, preserves newlines, spaces and comments. The documentation for lib2to3 is very
minimal, so I was lucky to find http://python3porting.com/fixers.html.
I don't think this script would have been possible without lib2to3/fissix, certainly not with the same
functionality since lib2to3/fissix, due to their purpose, preserves newlines, spaces and comments. The
documentation for lib2to3/fissix is very minimal, so I was lucky to
find http://python3porting.com/fixers.html.

Other than figuring out lib2to3 package so I could harness its
capabilities, some aspects of code transformations still turned out to be tricky, as warned by Regobro in the
last paragraph of his `Extending 2to3 <http://python3porting.com/fixers.html>`_ page.
Other than figuring out lib2to3/fissix package so I could harness its capabilities, some aspects of code
transformations still turned out to be tricky, as warned by Regobro in the last paragraph of his
`Extending 2to3 <http://python3porting.com/fixers.html>`_ page.

- Multi-line arguments: Python accepts multi-line expressions when they are surrounded by parentheses, brackets
or braces, but not otherwise. For example converting:
or braces, but not otherwise. For example, converting:

.. code-block:: python
Expand All @@ -313,7 +297,7 @@ last paragraph of his `Extending 2to3 <http://python3porting.com/fixers.html>`_
So nose2pytest checks each argument expression (such as ``long_a +\n long_b``) to see if it has
newlines that would cause an invalid syntax, and if so, wraps them in parentheses. However, it is also important
for readability of raw assertions that parentheses only be present if necessary. In other words:
for the readability of raw assertions that parentheses only be present if necessary. In other words:

.. code-block:: python
Expand Down Expand Up @@ -342,10 +326,10 @@ last paragraph of his `Extending 2to3 <http://python3porting.com/fixers.html>`_
So nose2pytest only tries to limit the addition of external parentheses to code that really needs it.

- Operator precedence: Python assigns a precedence to each operator; operators that are on the same level
- Operator precedence: Python assigns precedence to each operator; operators that are on the same level
of precedence (like the comparison operators ==, >=, !=, etc) are executed in sequence. This poses a problem
for two-argument assertion functions. Example: translating ``assert_equal(a != b, a <= c)`` to
``assert a != b == a <= c`` is incorrect, it must be converted to ``assert (a != b) == (a <= c)``. However
``assert a != b == a <= c`` is incorrect, it must be converted to ``assert (a != b) == (a <= c)``. However,
wrapping every argument in parentheses all the time does not produce easy-to-read assertions:
``assert_equal(a, b < c)`` should convert to ``assert a == (b < c)``, not ``assert (a) == (b < c)``.

Expand All @@ -358,20 +342,20 @@ last paragraph of his `Extending 2to3 <http://python3porting.com/fixers.html>`_
Contributing
------------

Patches and extensions are welcome. Please fork, branch, then submit PR. Nose2pytest uses `lib2to3.pytree`,
Patches and extensions are welcome. Please fork, branch, and then submit PR. Nose2pytest uses `lib2to3.pytree`,
in particular the Leaf and Node classes. There are a few particularly challenging aspects to transforming
nose test expressions to equivalent pytest expressions:

#. Finding expressions that match a pattern: If the code you want to transform does not already match one
of the uses cases in script.py, you will have to determine the lib2to3 pattern expression
of the uses cases in script.py, you will have to determine the lib2to3/fissix pattern expression
that describes it (this is similar to regular expressions, but for AST representation of code,
instead of text strings). Various expression patterns already exist near the top of
nose2pytest/script.py. This is largely trial and error as there is (as of this writing) no good
documentation.
#. Inserting the sub-expressions extracted by lib2to3 in step 1 into the target "expression template". For
example to convert `assert_none(a)` to `assert a is None`, the `a` sub-expression extracted via the lib2to3
pattern must be inserted into the correct "placeholder" node of the target expression. If step 1 was
necessary, then step 2 like involves creating a new class that derives from `FixAssertBase`.
#. Inserting the sub-expressions extracted by lib2to3/fissix in step 1 into the target "expression template".
For example to convert `assert_none(a)` to `assert a is None`, the `a` sub-expression extracted via the
lib2to3/fissix pattern must be inserted into the correct "placeholder" node of the target expression. If
step 1 was necessary, then step 2 like involves creating a new class that derives from `FixAssertBase`.
#. Parentheses and priority of operators: sometimes, it is necessary to add parentheses around an extracted
subexpression to protect it against higher-priority operators. For example, in `assert_none(a)` the `a`
could be an arbitrary Python expression, such as `var1 and var2`. The meaning of `assert_none(var1 and var2)`
Expand Down Expand Up @@ -431,10 +415,10 @@ Maintenance

- Clone or fork the git repo, create a branch
- Install `pytest` and `nose` on your system: `python -m pip install pytest nose`
- In root folder, run `pytest`
- In the root folder, run `pytest`
- Once all tests pass, install tox on your system: on Ubuntu, `python -m pip install tox`
- Run tox: `tox`
- Add a python version if latest python is not in `tox.ini`
- Add a python version if the latest Python is not in `tox.ini`

Notes for Ubuntu:
- My experience today installing python 3.5 to 3.11 on Ubuntu 18 was surprisingly not smooth. I had to use these
Expand All @@ -448,8 +432,8 @@ Notes for Ubuntu:
- note however that once the correct tox installed,


Acknowledgements
----------------
Acknowledgments
---------------

Thanks to (AFAICT) Lennart Regebro for having written http://python3porting.com/fixers.html#find-pattern, and
to those who answered
Expand Down

0 comments on commit 2964c29

Please sign in to comment.