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

transform assert_almost_equal (a,b) into assert a == approx(b) #6

Merged
merged 12 commits into from
Nov 17, 2022
Merged
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
2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/nose2pytest.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions .idea/runConfigurations/RST____HTML.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

974 changes: 974 additions & 0 deletions .idea/workspace.xml

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ python:
- "3.4"
- "3.5"
- "3.6"
- "3.7"
- "3.8"
- "3.9"
- "3.10"
- "3.11"

install:
- pip install .
Expand Down
216 changes: 151 additions & 65 deletions README.html

Large diffs are not rendered by default.

84 changes: 73 additions & 11 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


Overview
------------
-------------

This package provides a Python script and py.test plugin to help convert Nose-based tests into py.test-based
tests. Specifically, the script transforms ``nose.tools.assert_*`` function calls into raw assert statements,
Expand Down Expand Up @@ -124,9 +124,7 @@ assert_is_none(a[, msg]) assert a is None[, msg]
assert_is_not_none(a[, msg]) assert a is not None[, msg]
-------------------------------------------- -----------------------------------------------------------------
assert_equal(a,b[, msg]) assert a == b[, msg]
assert_equals(a,b[, msg]) assert a == b[, msg]
assert_not_equal(a,b[, msg]) assert a != b[, msg]
assert_not_equals(a,b[, msg]) assert a != b[, msg]
assert_list_equal(a,b[, msg]) assert a == b[, msg]
assert_dict_equal(a,b[, msg]) assert a == b[, msg]
assert_set_equal(a,b[, msg]) assert a == b[, msg]
Expand All @@ -147,10 +145,12 @@ assert_count_equal(a,b[, msg]) assert collections.Counter(a) == co
assert_not_regex(a,b[, msg]) assert not re.search(b, a)[, msg]
assert_regex(a,b[, msg]) assert re.search(b, a)[, msg]
-------------------------------------------- -----------------------------------------------------------------
assert_almost_equal(a,b, delta[, msg]) assert abs(a - b) <= delta[, msg]
assert_almost_equals(a,b, delta[, msg]) assert abs(a - b) <= delta[, msg]
assert_not_almost_equal(a,b, delta[, msg]) assert abs(a - b) > delta[, msg]
assert_not_almost_equals(a,b, delta[, msg]) assert abs(a - b) > delta[, msg]
assert_almost_equal(a,b[, msg]) assert a == pytest.approx(b, abs=1e-7)[, msg]
assert_almost_equal(a,b, delta[, msg]) assert a == pytest.approx(b, abs=delta)[, msg]
assert_almost_equal(a, b, places[, msg]) assert a == pytest.approx(b, abs=1e-places)[, msg]
assert_not_almost_equal(a,b[, msg]) assert a != pytest.approx(b, abs=1e-7)[, msg]
assert_not_almost_equal(a,b, delta[, msg]) assert a != pytest.approx(b, abs=delta)[, msg]
assert_not_almost_equal(a,b, places[, msg]) assert a != pytest.approx(b, abs=1e-places)[, msg]
============================================ =================================================================

The script adds parentheses around ``a`` and/or ``b`` if operator precedence would change the interpretation of the
Expand Down Expand Up @@ -179,10 +179,6 @@ Not every ``assert_*`` function from ``nose.tools`` is converted by nose2pytest:

2. Some Nose functions could be transformed but the readability would be decreased:

- ``assert_almost_equal(a, b, places)`` -> ``assert round(abs(b-a), places) == 0``
- ``assert_almost_equal(a, b)`` -> ``assert round(abs(b-a), 7) == 0``
- ``assert_not_almost_equal(a, b, places)`` -> ``assert round(abs(b-a), places) != 0``
- ``assert_not_almost_equal(a, b)`` -> ``assert round(abs(b-a), 7) != 0``
- ``assert_dict_contains_subset(a,b)`` -> ``assert set(b.keys()) >= a.keys() and {k: b[k] for k in a if k in b} == a``

The nose2pytest distribution contains a module, ``assert_tools.py`` which defines these utility functions to
Expand Down Expand Up @@ -359,6 +355,72 @@ last paragraph of his `Extending 2to3 <http://python3porting.com/fixers.html>`_
``assert a == b in c)``.


Contributing
------------

Patches and extensions are welcome. Please fork, branch, 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
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`.
#. 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)`
is not the same as `assert var1 and var2 is None`; parentheses must be added i.e. the target expression
must be `assert (var1 and var2) is None`. Whether this is necessary depends on the transformation. The
`wrap_parens_*` functions provide examples of how and when to do this.
#. Spacing: white space and newlines in code must be preserved as much as possible, and removed
when unnecessary. For example, `assert_equal(a, b)` convers to `assert a == b`; the latter already has a
a space before the b, but so does the original; the `lib2to3.pytree` captures such 'non-code' information
so that generating Python code from a Node yields the same as the input if no transformations were applied.
This is done via the `Node.prefix` property.

When the pattern is correctly defined in step 1, adding a test in tests/test_script.py for a string that
contains Python code that matches it will cause the `FixAssertBase.transform(node, results)` to be called,
with `node` being the Node for which the children match the defined pattern. The `results` is map of object
names defined in the pattern, to the Node subtree representing the sub-expression matched. For example,
a pattern for `assert_none(a)` (where `a` could be any sub-expression such as `1+2` or `sqrt(5)` or
`var1+var2`) will cause `results` to contain the sub-expression that `a` represents. The objective of
`transform()` is then to put the extracted results at the correct location into a new Node tree that
represents the target (transformed) expression.

Nodes form a tree, each Node has a `children` property, containing 0 or more Node and/or Leaf. For example,
if `node` represents `assert a/2 == b`, then the tree might be something like this::

node (Node)
assert (Leaf)
node (node)
node (node)
a (Leaf)
/ (Leaf)
2 (Leaf)
== (Leaf)
b (Leaf)

Sometimes you may be able to guess what the tree is for a given expression, however most often it is best to use
a debugger to run a test that attempts to transform your expression of interest (there are several examples of
how to do this in tests/test_script.py), break at the beginning of the `FixAssertBase.transform()` method, and
explore the `node.children` tree to find the subexpressions that you need to extract. In the above example,
the `assert` leaf node is child at index 0 of `node.children`, whereas child 1 is another Node; the `a` leaf
is child 0 of child 0 of child 1 of `node.children`, i.e. it is `node.children[0].children[0].children[1]`.
Therefore the "path" from `node` to reach 'a' is (0, 0, 1).

The main challenge for this step of nose2test extension is then to find the paths to reach the desired
"placeholder" objects in the target expression. For example if `assert_almost_equal(a, b, delta=value)`
must be converted to `assert a == pytest.approx(b, delta=value)`, then the nodes of interest are a, b, and
delta, and their paths are 0, (2, 2, 1, 0) and (2, 2, 1, 2, 2) respectively (when a path contains only
1 item, there is no need to use a tuple).


Acknowledgements
----------------

Expand Down
44 changes: 3 additions & 41 deletions nose2pytest/assert_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@
"""

import unittest
import pytest


__all__ = [
'assert_almost_equal',
'assert_not_almost_equal',
'assert_dict_contains_subset',

'assert_raises_regex',
Expand All @@ -25,40 +24,6 @@
]


def assert_almost_equal(a, b, places=7, msg=None):
"""
Fail if the two objects are unequal as determined by their
difference rounded to the given number of decimal places
and comparing to zero.

Note that decimal places (from zero) are usually not the same
as significant digits (measured from the most signficant digit).

See the builtin round() function for places parameter.
"""
if msg is None:
assert round(abs(b - a), places) == 0
else:
assert round(abs(b - a), places) == 0, msg


def assert_not_almost_equal(a, b, places=7, msg=None):
"""
Fail if the two objects are equal as determined by their
difference rounded to the given number of decimal places
and comparing to zero.

Note that decimal places (from zero) are usually not the same
as significant digits (measured from the most signficant digit).

See the builtin round() function for places parameter.
"""
if msg is None:
assert round(abs(b - a), places) != 0
else:
assert round(abs(b - a), places) != 0, msg


def assert_dict_contains_subset(subset, dictionary, msg=None):
"""
Checks whether dictionary is a superset of subset. If not, the assertion message will have useful details,
Expand Down Expand Up @@ -96,13 +61,10 @@ def do_nothing(self):

# Use similar trick as Nose to bring in bound methods from unittest.TestCase as free functions:

def pytest_namespace() -> {str: callable}:
namespace = {}
def pytest_configure():
for name, obj in globals().items():
if name.startswith('assert_'):
namespace[name] = obj

return namespace
setattr(pytest, name, obj)


# licensing
Expand Down
Loading