├── .github ├── dependabot.yml └── workflows │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.rst ├── LICENSE.txt ├── MANIFEST ├── MANIFEST.in ├── README.rst ├── RELEASING.rst ├── nose2pytest ├── __init__.py ├── assert_tools.py └── script.py ├── setup.cfg ├── setup.py ├── tests └── test_script.py ├── tools ├── find_pattern.py └── fixer_ex_pattern.txt └── tox.ini /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Keep GitHub Actions up to date with GitHub's Dependabot... 2 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot 3 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem 4 | version: 2 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | groups: 9 | github-actions: 10 | patterns: 11 | - "*" # Group all Actions updates into a single larger pull request 12 | schedule: 13 | interval: weekly 14 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | 10 | deploy: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Build and Check Package 17 | uses: hynek/build-and-inspect-python-package@v2.12 18 | 19 | - name: Download Package 20 | uses: actions/download-artifact@v4 21 | with: 22 | name: Packages 23 | path: dist 24 | 25 | - name: Publish package to PyPI 26 | uses: pypa/gh-action-pypi-publish@master 27 | with: 28 | password: ${{ secrets.pypi_token }} 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | env: 6 | FORCE_COLOR: 1 7 | 8 | jobs: 9 | 10 | package: 11 | runs-on: "ubuntu-latest" 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Build and Check Package 17 | uses: hynek/build-and-inspect-python-package@v2.12 18 | 19 | build: 20 | needs: "package" 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | python-version: ["3.8", "3.9", "3.10", "3.11"] 26 | os: [ubuntu-latest, macos-latest, windows-latest] 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - name: Download Package 32 | uses: actions/download-artifact@v4 33 | with: 34 | name: Packages 35 | path: dist 36 | 37 | - name: Set up Python ${{ matrix.python-version }} 38 | uses: actions/setup-python@v5 39 | with: 40 | python-version: ${{ matrix.python-version }} 41 | 42 | - name: Install dependencies 43 | run: | 44 | python -m pip install --upgrade pip 45 | python -m pip install --upgrade tox 46 | 47 | - name: Test 48 | shell: bash 49 | run: | 50 | tox -e py --installpkg `find dist/*.tar.gz` 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | .idea/ 64 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ============ 3 | 4 | See `Releases `__ for the changelog of each version. 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | =========================================== 2 | License terms for nose2pytest distribution 3 | =========================================== 4 | 5 | Copyright (c) 2016, Oliver Schoenborn 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of pytest_from_nose nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | LICENSE.txt 3 | README.rst 4 | assert_tools.py 5 | nose2pytest 6 | setup.cfg 7 | setup.py 8 | tests\test_script.py 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | include LICENSE.txt 3 | include README.rst 4 | recursive-include tests *.py 5 | exclude find_pattern.py 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://badge.fury.io/py/nose2pytest.svg 2 | :target: https://badge.fury.io/py/nose2pytest 3 | .. image:: https://github.com/pytest-dev/nose2pytest/workflows/Test/badge.svg 4 | :target: https://github.com/pytest-dev/nose2pytest/actions 5 | 6 | 7 | .. contents:: 8 | 9 | 10 | Overview 11 | ------------- 12 | 13 | This package provides a Python script and pytest plugin to help convert Nose-based tests into pytest-based 14 | tests. Specifically, the script transforms ``nose.tools.assert_*`` function calls into raw assert statements, 15 | while preserving the format of original arguments as much as possible. For example, the script: 16 | 17 | .. code-block:: python 18 | 19 | assert_true(a, msg) 20 | assert_greater(a, b, msg) 21 | 22 | gets converted to: 23 | 24 | .. code-block:: python 25 | 26 | assert a, msg 27 | assert a > b, msg 28 | 29 | A small subset of ``nose.tools.assert_*`` function calls are not 30 | transformed because there is no raw assert statement equivalent, or the equivalent would be hard to 31 | maintain. They are provided as functions in the pytest namespace via pytest's plugin system. 32 | 33 | 34 | Running 35 | ------------ 36 | 37 | For a one-time conversion use the shell command :: 38 | 39 | pipx run --python 3.11 nose2pytest path/to/dir/with/python_files 40 | 41 | This will find all ``.py`` files in the folder tree starting at ``path/to/dir/with/python_files`` and 42 | overwrite the original (assuming most users will be running this on a version-controlled code base, this is 43 | almost always what would be most convenient). Type ``nose2pytest -h`` for other options, such as ``-v``. 44 | 45 | 46 | Installation 47 | ------------- 48 | 49 | For doing multiple conversions use the shell command :: 50 | 51 | pipx install --python 3.11 nose2pytest 52 | 53 | For each conversion use the shell command :: 54 | 55 | nose2pytest path/to/dir/with/python_files 56 | 57 | 58 | Motivation 59 | ------------ 60 | 61 | I have used Nose for years and it is a great tool. However, to get good test failure diagnostics with Nose you 62 | ought to use the ``assert_*()`` functions from ``nose.tools``. Although they provide very good diagnostics, they 63 | are not as convenient to use as raw assertions, since you have to decide beforehand what type of assertion you 64 | are going to write: an identity comparison to None, a truth check, a falseness check, an identity comparison to another 65 | object, etc. Just being able to write a raw assertion, and still get good diagnostics on failure as done by 66 | pytest, is really nice. This is a main reason for using pytest for me. Another reason is the design of fixtures 67 | in pytest. 68 | 69 | Switching an existing test suite from Nose to pytest is feasible even without nose2pytest, as it requires 70 | relatively little work: *relatively* as in, you will probably only need a few modifications, all achievable 71 | manually, to get the same test coverage and results. A few gotchas: 72 | 73 | - test classes that have ``__init__`` will be ignored, those will have to be moved (usually, into class's 74 | ``setup_class()``) 75 | - the ``setup.cfg`` may have to be edited since test discovery rules are slightly more strict with pytest 76 | - the order of tests may be different, but in general, that should not matter 77 | - all test modules are imported up-front, so some test modules may need adjustment such as moving some 78 | code from the top of the test module into its ``setup_module()`` 79 | 80 | Once the above has been done to an existing code base, you don't really have to do anything else. However, your test 81 | suite now has an additional third-party test dependency (Nose), just because of those ``assert_*`` functions used all 82 | over the place. Moreover, there is no longer one obvious way to do things in your test suite: existing test code 83 | uses ``nose.tools.assert_*`` functions, yet with pytest you can use raw assertions. If you add tests, which of 84 | these two approaches should a developer use? If you modify existing tests, should new assertions use raw assert? 85 | Should the remaining test method, test class, or test module be updated? A test module can contain hundreds of 86 | calls to ``nose.tools.assert_*`` functions, is a developer to manually go through each one to convert it? Painful and 87 | error-prone, in general not feasible to do manually. 88 | 89 | This is why I developed nose2pytest: I wanted to migrate my pypubsub project's test suite from Nose to pytest, 90 | but also have only pytest as a dependency, and have one obvious way to write assertions in the test suite. 91 | 92 | 93 | Requirements 94 | ------------- 95 | 96 | I expect nose2pytest script to run with supported versions of CPython <= v3.11, on any OS supported by a version of 97 | Python that has lib2to3 compatible with fissix. I expect it to succeed even with quite old versions of Nose (even 98 | prior to 1.0 which came out ca. 2010) and with the new Nose2 test driver. 99 | 100 | The pytest package namespace will be extended with ``assert_`` functions that are not converted by the script 101 | only if, err, you have pytest installed! 102 | 103 | 104 | Status 105 | ------------------------------ 106 | 107 | The package has been used on over 5000 ``assert_*()`` function calls, among which the pypubsub test suite. 108 | I consider it stable, but I have only used it on my code, and code by a few other developers. Feedback on 109 | results of conversions would be most appreciated (such as version information and number of assert statements 110 | converted). 111 | 112 | The following conversions have been implemented: 113 | 114 | ============================================ ================================================================= 115 | Function Statement 116 | ============================================ ================================================================= 117 | assert_true(a[, msg]) assert a[, msg] 118 | assert_false(a[, msg]) assert not a[, msg] 119 | assert_is_none(a[, msg]) assert a is None[, msg] 120 | assert_is_not_none(a[, msg]) assert a is not None[, msg] 121 | -------------------------------------------- ----------------------------------------------------------------- 122 | assert_equal(a,b[, msg]) assert a == b[, msg] 123 | assert_not_equal(a,b[, msg]) assert a != b[, msg] 124 | assert_list_equal(a,b[, msg]) assert a == b[, msg] 125 | assert_dict_equal(a,b[, msg]) assert a == b[, msg] 126 | assert_set_equal(a,b[, msg]) assert a == b[, msg] 127 | assert_sequence_equal(a,b[, msg]) assert a == b[, msg] 128 | assert_tuple_equal(a,b[, msg]) assert a == b[, msg] 129 | assert_multi_line_equal(a,b[, msg]) assert a == b[, msg] 130 | assert_greater(a,b[, msg]) assert a > b[, msg] 131 | assert_greater_equal(a,b[, msg]) assert a >= b[, msg] 132 | assert_less(a,b[, msg]) assert a < b[, msg] 133 | assert_less_equal(a,b[, msg]) assert a <= b[, msg] 134 | assert_in(a,b[, msg]) assert a in b[, msg] 135 | assert_not_in(a,b[, msg]) assert a not in b[, msg] 136 | assert_is(a,b[, msg]) assert a is b[, msg] 137 | assert_is_not(a,b[, msg]) assert a is not b[, msg] 138 | -------------------------------------------- ----------------------------------------------------------------- 139 | assert_is_instance(a,b[, msg]) assert isinstance(a, b)[, msg] 140 | assert_count_equal(a,b[, msg]) assert collections.Counter(a) == collections.Counter(b)[, msg] 141 | assert_not_regex(a,b[, msg]) assert not re.search(b, a)[, msg] 142 | assert_regex(a,b[, msg]) assert re.search(b, a)[, msg] 143 | -------------------------------------------- ----------------------------------------------------------------- 144 | assert_almost_equal(a,b[, msg]) assert a == pytest.approx(b, abs=1e-7)[, msg] 145 | assert_almost_equal(a,b, delta[, msg]) assert a == pytest.approx(b, abs=delta)[, msg] 146 | assert_almost_equal(a, b, places[, msg]) assert a == pytest.approx(b, abs=1e-places)[, msg] 147 | assert_not_almost_equal(a,b[, msg]) assert a != pytest.approx(b, abs=1e-7)[, msg] 148 | assert_not_almost_equal(a,b, delta[, msg]) assert a != pytest.approx(b, abs=delta)[, msg] 149 | assert_not_almost_equal(a,b, places[, msg]) assert a != pytest.approx(b, abs=1e-places)[, msg] 150 | ============================================ ================================================================= 151 | 152 | The script adds parentheses around ``a`` and/or ``b`` if operator precedence would change the interpretation of the 153 | expression or involves newline. For example: 154 | 155 | .. code-block:: python 156 | 157 | assert_true(some-long-expression-a in 158 | some-long-expression-b, msg) 159 | assert_equal(a == b, b == c), msg 160 | 161 | gets converted to: 162 | 163 | .. code-block:: python 164 | 165 | assert (some-long-expression-a in 166 | some-long-expression-b), msg 167 | assert (a == b) == (b == c), msg 168 | 169 | Not every ``assert_*`` function from ``nose.tools`` is converted by nose2pytest: 170 | 171 | 1. Some Nose functions can be handled via a global search-replace, so a fixer was not a necessity: 172 | 173 | - ``assert_raises``: replace with ``pytest.raises`` 174 | - ``assert_warns``: replace with ``pytest.warns`` 175 | 176 | 2. Some Nose functions could be transformed but the readability would be decreased: 177 | 178 | - ``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`` 179 | 180 | The nose2pytest distribution contains a module, ``assert_tools.py`` which defines these utility functions to 181 | contain the equivalent raw assert statement. Copy the module into your test folder or into the pytest package 182 | and change your test code's ``from nose.tools import ...`` statements accordingly. pytest introspection will 183 | provide error information on assertion failure. 184 | 185 | 3. Some Nose functions don't have a one-line assert statement equivalent, they have to remain utility functions: 186 | 187 | - ``assert_raises_regex`` 188 | - ``assert_raises_regexp`` # deprecated by Nose 189 | - ``assert_regexp_matches`` # deprecated by Nose 190 | - ``assert_warns_regex`` 191 | 192 | These functions are available in ``assert_tools.py`` of nose2pytest distribution, and are imported as 193 | is from ``unittest.TestCase`` (but renamed as per Nose). Copy the module into your test folder or into 194 | the pytest package and change your test code's ``from nose.tools import ...`` statements accordingly. 195 | 196 | 4. Some Nose functions simply weren't on my radar; for example I just noticed for the first time that there 197 | is a ``nose.tools.ok_()`` function which is the same as ``assert_equal``. Feel free to contribute via email 198 | or pull requests. 199 | 200 | 201 | Limitations 202 | ------------ 203 | 204 | - The script does not convert ``nose.tools.assert_`` import statements as there are too many possibilities. 205 | Should ``from nose.tools import ...`` be changed to ``from pytest import ...``, and the implemented 206 | conversions be removed? Should an ``import pytest`` statement be added, and if so, where? If it is added after 207 | the line that had the ``nose.tools`` import, is the previous line really needed? Indeed the ``assert_`` 208 | functions added in the ``pytest`` namespace could be accessed via ``pytest.assert_``, in which case the 209 | script should prepend ``pytest.`` and remove the ``from nose.tools import ...`` entirely. Too many options, 210 | and you can fairly easily handle this via a global regexp search/replace. 211 | 212 | - Similarly, statements of the form ``nose.tools.assert_`` are not converted: this would require some form 213 | of semantic analysis of each call to a function, because any of the following are possible: 214 | 215 | .. code-block:: python 216 | 217 | import nose.tools as nt 218 | 219 | nt.assert_true(...) 220 | 221 | nt2 = nt 222 | nt2.assert_true(...) 223 | nt2.assert_true(...) 224 | 225 | import bogo.assert_true 226 | bogo.assert_true(...) # should this one be converted? 227 | 228 | The possibilities are endless so supporting this would require such a large amount of time that I 229 | do not have. As with other limitations in this section 230 | 231 | - Nose functions that can be used as context managers can obviously not be converted to raw assertions. 232 | However, there is currently no way of preventing nose2pytest from converting Nose functions used this way. 233 | You will have to manually fix. 234 | 235 | - ``@raises``: this decorator can be replaced via the regular expression ``@raises\((.*)\)`` to 236 | ``@pytest.mark.xfail(raises=$1)``, 237 | but I prefer instead to convert such decorated test functions to use ``pytest.raises`` in the test function body. 238 | Indeed, it is easy to forget the decorator and add code after the line that raises, but this code will never 239 | be run and you won't know. Using the ``pytest.raises(...)`` is better than ``xfail(raise=...)``. 240 | 241 | - Nose2pytest does not have a means of determining if an assertion function is inside a lambda expression, so 242 | the valid ``lambda: assert_func(a, b)`` gets converted to the invalid ``lambda: assert a operator b``. 243 | These should be rare, are easy to spot (your IDE will flag the syntax error, or you will get an exception 244 | on import), and are easy to fix by changing from a lambda expression to a local function. 245 | 246 | I have no doubt that more limitations will arise as nose2pytest gets used on more code bases. Contributions to 247 | address these and existing limitations are most welcome. 248 | 249 | 250 | Other tools 251 | ------------ 252 | 253 | If your test suite is unittest- or unittest2-based, or your Nose tests also use some unittest/2 functionatlity 254 | (such as ``setUp(self)`` method in test classes), then you might find the following useful: 255 | 256 | - https://github.com/pytest-dev/unittest2pytest 257 | - https://github.com/dropbox/unittest2pytest 258 | 259 | I have used neither, so I can't make recommendations. However, if your Nose-based test suite uses both Nose/2 and 260 | unittest/2 functionality (such as ``unittest.case.TestCase`` and/or ``setUp(self)/tearDown(self)`` methods), you 261 | should be able to run both a unittest2pytest converter, then the nose2pytest converter. 262 | 263 | 264 | Solution Notes 265 | --------------- 266 | 267 | I don't think this script would have been possible without lib2to3/fissix, certainly not with the same 268 | functionality since lib2to3/fissix, due to their purpose, preserves newlines, spaces and comments. The 269 | documentation for lib2to3/fissix is very minimal, so I was lucky to 270 | find http://python3porting.com/fixers.html. 271 | 272 | Other than figuring out lib2to3/fissix package so I could harness its capabilities, some aspects of code 273 | transformations still turned out to be tricky, as warned by Regobro in the last paragraph of his 274 | `Extending 2to3 `_ page. 275 | 276 | - Multi-line arguments: Python accepts multi-line expressions when they are surrounded by parentheses, brackets 277 | or braces, but not otherwise. For example, converting: 278 | 279 | .. code-block:: python 280 | 281 | assert_func(long_a + 282 | long_b, msg) 283 | 284 | to: 285 | 286 | .. code-block:: python 287 | 288 | assert long_a + 289 | long_b, msg 290 | 291 | yields invalid Python code. However, converting to the following yields valid Python code: 292 | 293 | .. code-block:: python 294 | 295 | assert (long_a + 296 | long_b), msg 297 | 298 | So nose2pytest checks each argument expression (such as ``long_a +\n long_b``) to see if it has 299 | newlines that would cause an invalid syntax, and if so, wraps them in parentheses. However, it is also important 300 | for the readability of raw assertions that parentheses only be present if necessary. In other words: 301 | 302 | .. code-block:: python 303 | 304 | assert_func((long_a + 305 | long_b), msg) 306 | assert_func(z + (long_a + 307 | long_b), msg) 308 | 309 | should convert to: 310 | 311 | .. code-block:: python 312 | 313 | assert (long_a + 314 | long_b), msg 315 | assert z + (long_a + 316 | long_b), msg) 317 | 318 | rather than: 319 | 320 | .. code-block:: python 321 | 322 | assert ((long_a + 323 | long_b)), msg 324 | assert (z + (long_a + 325 | long_b)), msg) 326 | 327 | So nose2pytest only tries to limit the addition of external parentheses to code that really needs it. 328 | 329 | - Operator precedence: Python assigns precedence to each operator; operators that are on the same level 330 | of precedence (like the comparison operators ==, >=, !=, etc) are executed in sequence. This poses a problem 331 | for two-argument assertion functions. Example: translating ``assert_equal(a != b, a <= c)`` to 332 | ``assert a != b == a <= c`` is incorrect, it must be converted to ``assert (a != b) == (a <= c)``. However, 333 | wrapping every argument in parentheses all the time does not produce easy-to-read assertions: 334 | ``assert_equal(a, b < c)`` should convert to ``assert a == (b < c)``, not ``assert (a) == (b < c)``. 335 | 336 | So nose2pytest adds parentheses around its arguments if the operator used between the args has lower precedence 337 | than any operator found in the arg. So ``assert_equal(a, b + c)`` converts to assert ``a == b + c`` whereas 338 | ``assert_equal(a, b in c)`` converts to ``assert a == (b in c)`` but ``assert_in(a == b, c)`` converts to 339 | ``assert a == b in c)``. 340 | 341 | 342 | Contributing 343 | ------------ 344 | 345 | Patches and extensions are welcome. Please fork, branch, and then submit PR. Nose2pytest uses `lib2to3.pytree`, 346 | in particular the Leaf and Node classes. There are a few particularly challenging aspects to transforming 347 | nose test expressions to equivalent pytest expressions: 348 | 349 | #. Finding expressions that match a pattern: If the code you want to transform does not already match one 350 | of the uses cases in script.py, you will have to determine the lib2to3/fissix pattern expression 351 | that describes it (this is similar to regular expressions, but for AST representation of code, 352 | instead of text strings). Various expression patterns already exist near the top of 353 | nose2pytest/script.py. This is largely trial and error as there is (as of this writing) no good 354 | documentation. 355 | #. Inserting the sub-expressions extracted by lib2to3/fissix in step 1 into the target "expression template". 356 | For example to convert `assert_none(a)` to `assert a is None`, the `a` sub-expression extracted via the 357 | lib2to3/fissix pattern must be inserted into the correct "placeholder" node of the target expression. If 358 | step 1 was necessary, then step 2 like involves creating a new class that derives from `FixAssertBase`. 359 | #. Parentheses and priority of operators: sometimes, it is necessary to add parentheses around an extracted 360 | subexpression to protect it against higher-priority operators. For example, in `assert_none(a)` the `a` 361 | could be an arbitrary Python expression, such as `var1 and var2`. The meaning of `assert_none(var1 and var2)` 362 | is not the same as `assert var1 and var2 is None`; parentheses must be added i.e. the target expression 363 | must be `assert (var1 and var2) is None`. Whether this is necessary depends on the transformation. The 364 | `wrap_parens_*` functions provide examples of how and when to do this. 365 | #. Spacing: white space and newlines in code must be preserved as much as possible, and removed 366 | when unnecessary. For example, `assert_equal(a, b)` convers to `assert a == b`; the latter already has a 367 | a space before the b, but so does the original; the `lib2to3.pytree` captures such 'non-code' information 368 | so that generating Python code from a Node yields the same as the input if no transformations were applied. 369 | This is done via the `Node.prefix` property. 370 | 371 | When the pattern is correctly defined in step 1, adding a test in tests/test_script.py for a string that 372 | contains Python code that matches it will cause the `FixAssertBase.transform(node, results)` to be called, 373 | with `node` being the Node for which the children match the defined pattern. The `results` is map of object 374 | names defined in the pattern, to the Node subtree representing the sub-expression matched. For example, 375 | a pattern for `assert_none(a)` (where `a` could be any sub-expression such as `1+2` or `sqrt(5)` or 376 | `var1+var2`) will cause `results` to contain the sub-expression that `a` represents. The objective of 377 | `transform()` is then to put the extracted results at the correct location into a new Node tree that 378 | represents the target (transformed) expression. 379 | 380 | Nodes form a tree, each Node has a `children` property, containing 0 or more Node and/or Leaf. For example, 381 | if `node` represents `assert a/2 == b`, then the tree might be something like this:: 382 | 383 | node (Node) 384 | assert (Leaf) 385 | node (node) 386 | node (node) 387 | a (Leaf) 388 | / (Leaf) 389 | 2 (Leaf) 390 | == (Leaf) 391 | b (Leaf) 392 | 393 | Sometimes you may be able to guess what the tree is for a given expression, however most often it is best to use 394 | a debugger to run a test that attempts to transform your expression of interest (there are several examples of 395 | how to do this in tests/test_script.py), break at the beginning of the `FixAssertBase.transform()` method, and 396 | explore the `node.children` tree to find the subexpressions that you need to extract. In the above example, 397 | the `assert` leaf node is child at index 0 of `node.children`, whereas child 1 is another Node; the `a` leaf 398 | is child 0 of child 0 of child 1 of `node.children`, i.e. it is `node.children[0].children[0].children[1]`. 399 | Therefore the "path" from `node` to reach 'a' is (0, 0, 1). 400 | 401 | The main challenge for this step of nose2test extension is then to find the paths to reach the desired 402 | "placeholder" objects in the target expression. For example if `assert_almost_equal(a, b, delta=value)` 403 | must be converted to `assert a == pytest.approx(b, delta=value)`, then the nodes of interest are a, b, and 404 | delta, and their paths are 0, (2, 2, 1, 0) and (2, 2, 1, 2, 2) respectively (when a path contains only 405 | 1 item, there is no need to use a tuple). 406 | 407 | 408 | Releasing 409 | --------- 410 | 411 | See `RELEASING.rst `__. 412 | 413 | Maintenance 414 | ----------- 415 | 416 | - Clone or fork the git repo, create a branch 417 | - Install `pytest` and `nose` on your system: `python -m pip install pytest nose` 418 | - In the root folder, run `pytest` 419 | - Once all tests pass, install tox on your system: on Ubuntu, `python -m pip install tox` 420 | - Run tox: `tox` 421 | - Add a python version if the latest Python is not in `tox.ini` 422 | 423 | .. note:: 424 | 425 | Notes for Ubuntu: 426 | 427 | My experience today installing python 3.5 to 3.11 on Ubuntu 18 was surprisingly not smooth. I had to use these commands: 428 | 429 | * sudo apt install python3.5 (ok) 430 | * sudo apt install python3.x-distutils for x=9,10,11 431 | * had to use `python -m pip` intead of just `pip` otherwise wrong version would get found 432 | * used `sudo update-alternatives --install /usr/bin/python python /usr/bin/python3.x 1` for all x 433 | * used `sudo update-alternatives --config python` to choose which python active 434 | * had to install setuptools from git repo otherwise weird pip error (used https://stackoverflow.com/a/69573368/869951) 435 | * note however that once the correct tox installed, 436 | 437 | 438 | Acknowledgments 439 | --------------- 440 | 441 | Thanks to (AFAICT) Lennart Regebro for having written http://python3porting.com/fixers.html#find-pattern, and 442 | to those who answered 443 | `my question on SO `_ 444 | and `my question on pytest-dev `_. 445 | -------------------------------------------------------------------------------- /RELEASING.rst: -------------------------------------------------------------------------------- 1 | Here are the steps on how to make a new release. 2 | 3 | 1. Create a ``release-VERSION`` branch from ``upstream/master``. 4 | 2. Install ``bumpversion`` and execute: 5 | 6 | :: 7 | 8 | bumpversion minor --new-version 1.0.11 9 | 10 | Changing ``minor`` and ``--new-version`` above accordingly. 11 | 12 | 3. Push a branch with the changes. 13 | 4. Once all builds pass and at least another maintainer approves, push a tag to ``upstream`` in the format ``v1.0.11`. 14 | This will deploy to PyPI. 15 | 5. Merge the PR (do not squash, to keep the tag). 16 | 6. Create a new release on GitHub, posting the contents of the current CHANGELOG. 17 | 7. Open a new PR clearing the ``CHANGELOG.md`` file. 18 | -------------------------------------------------------------------------------- /nose2pytest/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.0.8" 2 | -------------------------------------------------------------------------------- /nose2pytest/assert_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016 Oliver Schoenborn. BSD 3-Clause license (see __license__ at bottom of this file for details). 3 | 4 | This module is part of the nose2pytest distribution. 5 | 6 | This module's assert_ functions provide drop-in replacements for nose.tools.assert_ functions (many of which are 7 | pep-8-ized extractions from Python's unittest.case.TestCase methods). As such, it can be imported in a test 8 | suite run by pytest, to replace the nose imports with functions that rely on pytest's assertion 9 | introspection for error reporting. When combined with running nose2pytest.py on your test suite, this 10 | module may be sufficient to decrease your test suite's third-party dependencies by 1. 11 | """ 12 | 13 | import pytest 14 | import unittest 15 | import pytest 16 | 17 | 18 | __all__ = [ 19 | 'assert_dict_contains_subset', 20 | 21 | 'assert_raises_regex', 22 | 'assert_raises_regexp', 23 | 'assert_regexp_matches', 24 | 'assert_warns_regex', 25 | ] 26 | 27 | 28 | def assert_dict_contains_subset(subset, dictionary, msg=None): 29 | """ 30 | Checks whether dictionary is a superset of subset. If not, the assertion message will have useful details, 31 | unless msg is given, then msg is output. 32 | """ 33 | dictionary = dictionary 34 | missing_keys = sorted(list(set(subset.keys()) - set(dictionary.keys()))) 35 | mismatch_vals = {k: (subset[k], dictionary[k]) for k in subset if k in dictionary and subset[k] != dictionary[k]} 36 | if msg is None: 37 | assert missing_keys == [], 'Missing keys = {}'.format(missing_keys) 38 | assert mismatch_vals == {}, 'Mismatched values (s, d) = {}'.format(mismatch_vals) 39 | else: 40 | assert missing_keys == [], msg 41 | assert mismatch_vals == {}, msg 42 | 43 | 44 | # make other unittest.TestCase methods available as-is as functions; trick taken from Nose 45 | 46 | class _Dummy(unittest.TestCase): 47 | def do_nothing(self): 48 | pass 49 | 50 | _t = _Dummy('do_nothing') 51 | 52 | assert_raises_regex=_t.assertRaisesRegex, 53 | assert_raises_regexp=_t.assertRaisesRegex, 54 | assert_regexp_matches=_t.assertRegex, 55 | assert_warns_regex=_t.assertWarnsRegex, 56 | 57 | del _Dummy 58 | del _t 59 | 60 | 61 | # pytest integration: add all assert_ function to the pytest package namespace 62 | 63 | # Use similar trick as Nose to bring in bound methods from unittest.TestCase as free functions: 64 | 65 | 66 | def _supported_nose_name(name): 67 | return name.startswith('assert_') or name in ('ok_', 'eq_') 68 | 69 | 70 | def pytest_configure(): 71 | for name, obj in globals().items(): 72 | if _supported_nose_name(name): 73 | setattr(pytest, name, obj) 74 | 75 | 76 | # licensing 77 | 78 | __license__ = """ 79 | Copyright (c) 2016, Oliver Schoenborn 80 | All rights reserved. 81 | 82 | Redistribution and use in source and binary forms, with or without 83 | modification, are permitted provided that the following conditions are met: 84 | 85 | * Redistributions of source code must retain the above copyright notice, this 86 | list of conditions and the following disclaimer. 87 | 88 | * Redistributions in binary form must reproduce the above copyright notice, 89 | this list of conditions and the following disclaimer in the documentation 90 | and/or other materials provided with the distribution. 91 | 92 | * Neither the name of nose2pytest nor the names of its 93 | contributors may be used to endorse or promote products derived from 94 | this software without specific prior written permission. 95 | 96 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 97 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 98 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 99 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 100 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 101 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 102 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 103 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 104 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 105 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 106 | """ 107 | -------------------------------------------------------------------------------- /nose2pytest/script.py: -------------------------------------------------------------------------------- 1 | #! python 2 | """ 3 | Copyright 2016 Oliver Schoenborn. BSD 3-Clause license (see __license__ at bottom of this file for details). 4 | 5 | This script transforms nose.tools.assert_* function calls into raw assert statements, while preserving format 6 | of original arguments as much as possible. A small subset of nose.tools.assert_* function calls is not 7 | transformed because there is no raw assert statement equivalent. However, if you don't use those functions 8 | in your code, you will be able to remove nose as a test dependency of your library/app. 9 | 10 | Requires Python 3.4. 11 | 12 | This script relies heavily on fissix, using it to find patterns of code to transform and convert transformed 13 | code nodes back into Python source code. The following article was very useful: 14 | http://python3porting.com/fixers.html#find-pattern. 15 | """ 16 | 17 | import sys 18 | import argparse 19 | import logging 20 | from pathlib import Path 21 | 22 | from fissix import refactor, fixer_base, pygram, pytree, pgen2 23 | from fissix.pytree import Node as PyNode, Leaf as PyLeaf 24 | from fissix.pgen2 import token 25 | from fissix.fixer_util import parenthesize 26 | 27 | __version__ = "1.0.12" 28 | 29 | log = logging.getLogger('nose2pytest') 30 | 31 | 32 | def override_required(func: callable): 33 | """Decorator used to document that the decorated function must be overridden in derived class.""" 34 | return func 35 | 36 | 37 | def override_optional(func: callable): 38 | """Decorator used to document that the decorated function can be overridden in derived class, but need not be.""" 39 | return func 40 | 41 | 42 | def override(BaseClass): 43 | """Decorator used to document that the decorated function overrides the function of same name in BaseClass.""" 44 | 45 | def decorator(func): 46 | return func 47 | 48 | return decorator 49 | 50 | 51 | # Transformations: 52 | 53 | grammar = pygram.python_grammar 54 | driver = pgen2.driver.Driver(grammar, convert=pytree.convert, logger=log) 55 | 56 | PATTERN_ONE_ARG_OR_KWARG = """ 57 | power< 'func' trailer< '(' 58 | not(arglist) obj1=any 59 | ')' > > 60 | """ 61 | 62 | PATTERN_ONE_ARG = """ 63 | power< 'func' trailer< '(' 64 | not(arglist | argument >""" 66 | 67 | PATTERN_ONE_KWARG = """ 68 | power< 'func' trailer< '(' 69 | obj1=argument< any '=' any > 70 | ')' > >""" 71 | 72 | PATTERN_TWO_ARGS_OR_KWARGS = """ 73 | power< 'func' trailer< '(' 74 | arglist< obj1=any ',' obj2=any > 75 | ')' > >""" 76 | 77 | PATTERN_1_OR_2_ARGS = """ 78 | power< '{}' trailer< '(' 79 | ( not(arglist | argument ) 81 | ')' > > 82 | """ 83 | 84 | PATTERN_2_OR_3_ARGS = """ 85 | power< '{}' trailer< '(' 86 | ( arglist< lhs=any ',' rhs=any [','] > 87 | | arglist< lhs=any ',' rhs=any ',' msg=any > ) 88 | ')' > > 89 | """ 90 | 91 | PATTERN_ALMOST_ARGS = """ 92 | power< '{}' trailer< '(' 93 | ( arglist< aaa=any ',' bbb=any [','] > 94 | | arglist< aaa=any ',' bbb=any ',' arg3=any [','] > 95 | | arglist< aaa=any ',' bbb=any ',' arg3=any ',' arg4=any > ) 96 | ')' > > 97 | """ 98 | 99 | # for the following node types, contains_newline() will return False even if newlines are between ()[]{} 100 | NEWLINE_OK_TOKENS = (token.LPAR, token.LSQB, token.LBRACE) 101 | 102 | # these operators require parens around function arg if binop is ==, !=, ... 103 | COMPARISON_TOKENS = (token.EQEQUAL, token.NOTEQUAL, token.LESS, token.LESSEQUAL, token.GREATER, token.GREATEREQUAL) 104 | 105 | if sys.version_info.major < 3: 106 | raise RuntimeError('nose2pytest must be run using Python 3.x') 107 | 108 | py_grammar_symbols = pygram.python_grammar.symbol2number 109 | 110 | GRAM_SYM = py_grammar_symbols['comparison'] 111 | COMP_OP = py_grammar_symbols['comp_op'] 112 | MEMBERSHIP_SYMBOLS = ( 113 | (GRAM_SYM, 1, 'in'), 114 | (GRAM_SYM, COMP_OP, 'not in') 115 | ) 116 | IDENTITY_SYMBOLS = ( 117 | (GRAM_SYM, 1, 'is'), 118 | (GRAM_SYM, COMP_OP, 'is not') 119 | ) 120 | BOOLEAN_OPS = ( 121 | (py_grammar_symbols['not_test'], 1, 'not'), 122 | (py_grammar_symbols['and_test'], 1, 'and'), 123 | (py_grammar_symbols['or_test'], 1, 'or') 124 | ) 125 | GENERATOR_TYPE = py_grammar_symbols['argument'] 126 | 127 | # these operators require parens around function arg if binop is + or - 128 | ADD_SUB_GROUP_TOKENS = ( 129 | token.PLUS, token.MINUS, 130 | token.RIGHTSHIFT, token.LEFTSHIFT, 131 | token.VBAR, token.AMPER, token.CIRCUMFLEX, 132 | ) 133 | 134 | 135 | def contains_newline(node: PyNode) -> bool: 136 | """ 137 | Returns True if any of the children of node have a prefix containing \n, or any of their children recursively. 138 | Returns False if no non-bracketed children are found that have such prefix. Example: node of 'a\n in b' would 139 | return True, whereas '(a\n b)' would return False. 140 | """ 141 | for child in node.children: 142 | if child.type in NEWLINE_OK_TOKENS: 143 | return False 144 | if '\n' in child.prefix: 145 | return True 146 | if isinstance(child, PyNode) and contains_newline(child): 147 | return True 148 | 149 | return False 150 | 151 | 152 | def wrap_parens(arg_node: PyNode, checker_fn: callable) -> PyNode or PyLeaf: 153 | """ 154 | If a node that represents an argument to assert_ function should be grouped, return a new node that adds 155 | parentheses around arg_node. Otherwise, return arg_node. 156 | :param arg_node: the arg_node to parenthesize 157 | :return: the arg_node for the parenthesized expression, or the arg_node itself 158 | """ 159 | if isinstance(arg_node, PyNode) and checker_fn(arg_node): 160 | # log.info('adding parens: "{}" ({}), "{}" ({})'.format(first_child, first_child.type, sibling, sibling.type)) 161 | # sometimes arg_node has parent, need to remove it before giving to parenthesize() then re-insert: 162 | parent = arg_node.parent 163 | if parent is not None: 164 | pos_parent = arg_node.remove() 165 | new_node = parenthesize(arg_node) 166 | parent.insert_child(pos_parent, new_node) 167 | else: 168 | new_node = parenthesize(arg_node) 169 | 170 | new_node.prefix = arg_node.prefix 171 | arg_node.prefix = '' 172 | return new_node 173 | 174 | return arg_node 175 | 176 | 177 | def is_if_else_op(node: PyNode) -> bool: 178 | return (len(node.children) == 5 and 179 | node.children[1] == PyLeaf(token.NAME, 'if') and 180 | node.children[3] == PyLeaf(token.NAME, 'else') 181 | ) 182 | 183 | 184 | def has_weak_op_for_comparison(node: PyNode) -> bool: 185 | """Test if node contains operators that are weaking than comparison operators""" 186 | 187 | if is_if_else_op(node): 188 | return True 189 | 190 | for child in node.children: 191 | if child.type in NEWLINE_OK_TOKENS: 192 | return False 193 | 194 | # comparisons and boolean combination: 195 | binop_type = child.type 196 | if binop_type in COMPARISON_TOKENS: 197 | return True 198 | 199 | # membership and identity tests: 200 | binop_name = str(child).strip() 201 | symbol = (node.type, binop_type, binop_name) 202 | if symbol in BOOLEAN_OPS or symbol in MEMBERSHIP_SYMBOLS or symbol in IDENTITY_SYMBOLS: 203 | return True 204 | 205 | # continue into children that are nodes: 206 | if isinstance(child, PyNode) and has_weak_op_for_comparison(child): 207 | return True 208 | 209 | return False 210 | 211 | 212 | def wrap_parens_for_comparison(arg_node: PyNode or PyLeaf) -> PyNode or PyLeaf: 213 | """ 214 | Assuming arg_node represents an argument to an assert_ function that uses comparison operators, then if 215 | arg_node has any operators that have equal or weaker precedence than those operators (including 216 | membership and identity test operators), return a new node that adds parentheses around arg_node. 217 | Otherwise, return arg_node. 218 | 219 | :param arg_node: the arg_node to parenthesize 220 | :return: the arg_node for the parenthesized expression, or the arg_node itself 221 | """ 222 | return wrap_parens(arg_node, has_weak_op_for_comparison) 223 | 224 | 225 | def has_weak_op_for_addsub(node: PyNode, check_comparison: bool = True) -> bool: 226 | if check_comparison and has_weak_op_for_comparison(node): 227 | return True 228 | 229 | for child in node.children: 230 | if child.type in NEWLINE_OK_TOKENS: 231 | return False 232 | 233 | if child.type in ADD_SUB_GROUP_TOKENS: 234 | return True 235 | 236 | # continue into children that are nodes: 237 | if isinstance(child, PyNode) and has_weak_op_for_addsub(child, check_comparison=False): 238 | return True 239 | 240 | return False 241 | 242 | 243 | def wrap_parens_for_addsub(arg_node: PyNode or PyLeaf) -> PyNode or PyLeaf: 244 | """ 245 | Assuming arg_node represents an argument to an assert_ function that uses + or - operators, then if 246 | arg_node has any operators that have equal or weaker precedence than those operators, return a new node 247 | that adds parentheses around arg_node. Otherwise, return arg_node. 248 | 249 | :param arg_node: the arg_node to parenthesize 250 | :return: the arg_node for the parenthesized expression, or the arg_node itself 251 | """ 252 | return wrap_parens(arg_node, has_weak_op_for_addsub) 253 | 254 | 255 | def get_prev_sibling(node: PyNode) -> PyNode: 256 | if node is None: 257 | return None # could not find 258 | if node.prev_sibling is not None: 259 | return node.prev_sibling 260 | return get_prev_sibling(node.parent) 261 | 262 | 263 | def adjust_prefix_first_arg(node: PyNode or PyLeaf, orig_prefix: str): 264 | if get_prev_sibling(node).type != token.NAME: 265 | node.prefix = '' 266 | else: 267 | node.prefix = orig_prefix or " " 268 | 269 | 270 | class FixAssertBase(fixer_base.BaseFix): 271 | # BM_compatible = True 272 | 273 | # Each derived class should define a dictionary where the key is the name of the nose function to convert, 274 | # and the value is a pair where the first item is the assertion statement expression, and the second item 275 | # is data that will be available in _transform_dest() override as self._arg_paths. 276 | conversions = None 277 | 278 | DEFAULT_ARG_PATHS = None 279 | 280 | @classmethod 281 | def create_all(cls, *args, **kwargs) -> [fixer_base.BaseFix]: 282 | """ 283 | Create an instance for each key in cls.conversions, assumed to be defined by derived class. 284 | The *args and **kwargs are those of BaseFix. 285 | :return: list of instances created 286 | """ 287 | fixers = [] 288 | for nose_func in cls.conversions: 289 | fixers.append(cls(nose_func, *args, **kwargs)) 290 | return fixers 291 | 292 | def __init__(self, nose_func_name: str, *args, **kwargs): 293 | if self.DEFAULT_ARG_PATHS is None: 294 | test_expr, conv_data = self.conversions[nose_func_name] 295 | self._arg_paths = conv_data 296 | else: 297 | test_expr = self.conversions[nose_func_name] 298 | self._arg_paths = self.DEFAULT_ARG_PATHS 299 | 300 | self.nose_func_name = nose_func_name 301 | 302 | self.PATTERN = self.PATTERN.format(nose_func_name) 303 | log.info('%s will convert %s as "assert %s"', self.__class__.__name__, nose_func_name, test_expr) 304 | super().__init__(*args, **kwargs) 305 | 306 | self.dest_tree = driver.parse_string('assert ' + test_expr + '\n') 307 | # remove the \n we added 308 | del self.dest_tree.children[0].children[1] 309 | 310 | @override(fixer_base.BaseFix) 311 | def transform(self, node: PyNode, results: {str: PyNode}) -> PyNode: 312 | assert results 313 | dest_tree = self.dest_tree.clone() 314 | assert_arg_test_node = self._get_node(dest_tree, (0, 0, 1)) 315 | assert_args = assert_arg_test_node.parent 316 | 317 | if self._transform_dest(assert_arg_test_node, results): 318 | assert_arg_test_node = self._get_node(dest_tree, (0, 0, 1)) 319 | if contains_newline(assert_arg_test_node): 320 | prefixes = assert_arg_test_node.prefix.split('\n', 1) 321 | assert_arg_test_node.prefix = '\n' + prefixes[1] if len(prefixes) > 1 else '' 322 | # NOTE: parenthesize(node) needs an unparent node, so give it a clone: 323 | new_node = parenthesize(assert_arg_test_node.clone()) 324 | new_node.prefix = prefixes[0] or ' ' 325 | assert_arg_test_node.replace(new_node) 326 | 327 | self.__handle_opt_msg(assert_args, results) 328 | 329 | dest_tree.prefix = node.prefix 330 | return dest_tree 331 | 332 | else: 333 | return node 334 | 335 | @override_required 336 | def _transform_dest(self, assert_arg_test_node: PyNode, results: {str: PyNode}) -> bool: 337 | """ 338 | Transform the given node to use the results. 339 | :param assert_arg_test_node: the destination node representing the assertion test argument 340 | :param results: the results of pattern matching 341 | """ 342 | pass 343 | 344 | def _get_node(self, from_node, indices_path: None or int or [int]) -> PyLeaf or PyNode: 345 | """ 346 | Get a node relative to another node. 347 | :param from_node: the node from which to start 348 | :param indices_path: the path through children 349 | :return: node found (could be leaf); if indices_path is None, this is from_node itself; if it is a 350 | number, return from_node[indices_path]; else returns according to sequence of children indices 351 | 352 | Example: if indices_path is (1, 2, 3), will return from_node.children[1].children[2].children[3]. 353 | """ 354 | if indices_path is None: 355 | return from_node 356 | 357 | try: 358 | node = from_node 359 | for index in indices_path: 360 | node = node.children[index] 361 | return node 362 | 363 | except TypeError: 364 | return from_node.children[indices_path] 365 | 366 | def __handle_opt_msg(self, assertion_args_node: PyNode, results: {str: PyNode}): 367 | """ 368 | Append a message argument to assertion args node, if one appears in results. 369 | :param assertion_args_node: the node representing all the arguments of assertion function 370 | :param results: results from pattern matching 371 | """ 372 | if 'msg' in results: 373 | msg = results["msg"] 374 | if len(msg.children) > 1: 375 | # the message text might have been passed by name, extract the text: 376 | children = msg.children 377 | if children[0] == PyLeaf(token.NAME, 'msg') and children[1] == PyLeaf(token.EQUAL, '='): 378 | msg = children[2] 379 | 380 | msg = msg.clone() 381 | msg.prefix = ' ' 382 | siblings = assertion_args_node.children 383 | siblings.append(PyLeaf(token.COMMA, ',')) 384 | siblings.append(msg) 385 | 386 | 387 | class FixAssert1Arg(FixAssertBase): 388 | """ 389 | Fixer class for any 1-argument assertion function (assert_func(a)). It supports optional 2nd arg for the 390 | assertion message, ie assert_func(a, msg) -> assert a binop something, msg. 391 | """ 392 | 393 | PATTERN = PATTERN_1_OR_2_ARGS 394 | 395 | # the path to arg node is different for every conversion 396 | # Example: assert_false(a) becomes "assert not a", so the PyNode for assertion expression is 'not a', and 397 | # the 'a' is its children[1] so self._arg_paths needs to be 1. 398 | conversions = dict( 399 | assert_true=('a', None), 400 | ok_=('a', None), 401 | assert_false=('not a', 1), 402 | assert_is_none=('a is None', 0), 403 | assert_is_not_none=('a is not None', 0), 404 | ) 405 | 406 | @override(FixAssertBase) 407 | def _transform_dest(self, assert_arg_test_node: PyNode, results: {str: PyNode}) -> bool: 408 | test = results["test"] 409 | test = test.clone() 410 | if test.type == GENERATOR_TYPE: 411 | test = parenthesize(test) 412 | test.prefix = " " 413 | 414 | # the destination node for 'a' is in conv_data: 415 | dest_node = self._get_node(assert_arg_test_node, self._arg_paths) 416 | dest_node.replace(test) 417 | 418 | return True 419 | 420 | 421 | class FixAssert2Args(FixAssertBase): 422 | """ 423 | Fixer class for any 2-argument assertion function (assert_func(a, b)). It supports optional third arg 424 | as the assertion message, ie assert_func(a, b, msg) -> assert a binop b, msg. 425 | """ 426 | 427 | PATTERN = PATTERN_2_OR_3_ARGS 428 | 429 | NEED_ARGS_PARENS = False 430 | 431 | # The args node paths are different for every conversion so the second item of each pair is paths infom 432 | # per base class. Here the paths info is itself a pair, one for arg a and the other for arg b. 433 | # 434 | # Example: assert_is_instance(a, b) converts to "assert isinstance(a, b)" so the conversion data is 435 | # the pair of node paths (1, 1, 0) and (1, 1, 1) since from the PyNode for the assertion expression 436 | # "isinstance(a, b)", 'a' is that node's children[1].children[1].children[0], whereas 'b' is 437 | # that node's children[1].children[1].children[1]. 438 | conversions = dict( 439 | assert_is_instance=('isinstance(a, b)', ((1, 1, 0), (1, 1, 2))), 440 | assert_count_equal=('collections.Counter(a) == collections.Counter(b)', ((0, 2, 1), (2, 2, 1))), 441 | assert_not_regex=('not re.search(b, a)', ((1, 2, 1, 2), (1, 2, 1, 0))), 442 | assert_regex=('re.search(b, a)', ((2, 1, 2), (2, 1, 0))), 443 | ) 444 | 445 | @override(FixAssertBase) 446 | def _transform_dest(self, assert_arg_test_node: PyNode, results: {str: PyNode}) -> bool: 447 | lhs = results["lhs"].clone() 448 | 449 | rhs = results["rhs"] 450 | rhs = rhs.clone() 451 | 452 | dest1 = self._get_node(assert_arg_test_node, self._arg_paths[0]) 453 | dest2 = self._get_node(assert_arg_test_node, self._arg_paths[1]) 454 | 455 | new_lhs = wrap_parens_for_comparison(lhs) if self.NEED_ARGS_PARENS else lhs 456 | dest1.replace(new_lhs) 457 | adjust_prefix_first_arg(new_lhs, results["lhs"].prefix) 458 | 459 | new_rhs = wrap_parens_for_comparison(rhs) if self.NEED_ARGS_PARENS else rhs 460 | dest2.replace(new_rhs) 461 | if get_prev_sibling(new_rhs).type in NEWLINE_OK_TOKENS: 462 | new_rhs.prefix = '' 463 | 464 | return True 465 | 466 | 467 | class FixAssertBinOp(FixAssert2Args): 468 | """ 469 | Fixer class for any 2-argument assertion function (assert_func(a, b)) that is of the form "a binop b". 470 | """ 471 | 472 | NEED_ARGS_PARENS = True 473 | 474 | # The args node paths are the same for every binary comparison assertion: the first element is for 475 | # arg a, the second for arg b 476 | # 477 | # Example 1: assert_equal(a, b) will convert to "assert a == b" so the PyNode for assertion expression 478 | # is 'a == b' and a is that node's children[0], whereas b is that node's children[2], so the self._arg_paths 479 | # will be simply (0, 2). 480 | DEFAULT_ARG_PATHS = (0, 2) 481 | 482 | conversions = dict( 483 | assert_equal='a == b', 484 | eq_='a == b', 485 | assert_equals='a == b', 486 | assert_not_equal='a != b', 487 | 488 | assert_list_equal='a == b', 489 | assert_dict_equal='a == b', 490 | assert_set_equal='a == b', 491 | assert_sequence_equal='a == b', 492 | assert_tuple_equal='a == b', 493 | assert_multi_line_equal='a == b', 494 | 495 | assert_greater='a > b', 496 | assert_greater_equal='a >= b', 497 | assert_less='a < b', 498 | assert_less_equal='a <= b', 499 | 500 | assert_in='a in b', 501 | assert_not_in='a not in b', 502 | 503 | assert_is='a is b', 504 | assert_is_not='a is not b', 505 | ) 506 | 507 | 508 | class FixAssertAlmostEq(FixAssertBase): 509 | """ 510 | Fixer class for any 3-argument assertion function (assert_func(a, b, c)). It supports optional fourth arg 511 | as the assertion message, ie assert_func(a, b, c, msg) -> assert a op b op c, msg. 512 | """ 513 | 514 | PATTERN = PATTERN_ALMOST_ARGS 515 | 516 | # The args node paths are the same for every assert function: the first tuple is for 517 | # arg a, the second for arg b, the third for arg c (delta). 518 | DEFAULT_ARG_PATHS = (0, (2, 2, 1, 0), (2, 2, 1, 2, 2)) 519 | 520 | conversions = dict( 521 | assert_almost_equal='a == pytest.approx(b, abs=delta)', 522 | assert_not_almost_equal='a != pytest.approx(b, abs=delta)', 523 | ) 524 | 525 | @override(FixAssertBase) 526 | def _transform_dest(self, assert_arg_test_node: PyNode, results: {str: PyNode}) -> bool: 527 | aaa = results["aaa"].clone() 528 | bbb = results["bbb"].clone() 529 | 530 | # first arg 531 | dest1 = self._get_node(assert_arg_test_node, self._arg_paths[0]) 532 | new_aaa = wrap_parens_for_addsub(aaa) 533 | dest1.replace(new_aaa) 534 | adjust_prefix_first_arg(new_aaa, results["aaa"].prefix) 535 | 536 | # second arg 537 | dest2 = self._get_node(assert_arg_test_node, self._arg_paths[1]) 538 | new_bbb = wrap_parens_for_addsub(bbb) 539 | if get_prev_sibling(dest2).type in NEWLINE_OK_TOKENS: 540 | new_bbb.prefix = '' 541 | dest2.replace(new_bbb) 542 | 543 | # third arg (optional) 544 | dest3 = self._get_node(assert_arg_test_node, self._arg_paths[2]) 545 | 546 | if "arg3" not in results: 547 | # then only 2 args so `places` defaults to '7', delta to None and 'msg' to "": 548 | self._use_places_default(dest3) 549 | return True 550 | 551 | # NOTE: arg3 could be places or delta, or even msg 552 | arg3 = results["arg3"].clone() 553 | if "arg4" not in results: 554 | if arg3.children[0] == PyLeaf(token.NAME, 'msg'): 555 | self._fix_results_err_msg_arg(results, arg3) 556 | self._use_places_default(dest3) 557 | return True 558 | 559 | return self._process_if_arg_is_places_or_delta(arg3, dest3) 560 | 561 | # we have 4 args: msg could be last, or it could be third: 562 | # first try assuming 3rd arg is places/delta: 563 | if self._process_if_arg_is_places_or_delta(arg3, dest3): 564 | self._fix_results_err_msg_arg(results, results["arg4"].clone()) 565 | return True 566 | 567 | # arg3 was not places/delta, try msg: 568 | if arg3.children[0] == PyLeaf(token.NAME, 'msg'): 569 | self._fix_results_err_msg_arg(results, arg3) 570 | delta_or_places = results["arg4"].clone() 571 | return self._process_if_arg_is_places_or_delta(delta_or_places, dest3) 572 | 573 | else: 574 | # if arg4 name is not msg, no match: 575 | return False 576 | 577 | def _use_places_default(self, abs_dest: PyNode): 578 | places_node = PyLeaf(token.NUMBER, '7', prefix="1e-") 579 | abs_dest.replace(places_node) 580 | 581 | def _fix_results_err_msg_arg(self, results: {str: PyNode}, err_msg_node: PyNode): 582 | # caller will look for 'msg' not 'arg3' so fix this: 583 | err_msg_node.children[2].prefix = "" 584 | results['msg'] = err_msg_node # the caller will look for this 585 | 586 | def _process_if_arg_is_places_or_delta(self, arg3: PyNode, dest3: PyNode) -> bool: 587 | if arg3.children[0] == PyLeaf(token.NAME, 'delta'): 588 | arg3_val = arg3.children[2] 589 | arg3_val.prefix = "" 590 | wrapped_delta_val = wrap_parens_for_comparison(arg3_val) 591 | dest3.replace(wrapped_delta_val) 592 | 593 | elif arg3.children[0] == PyLeaf(token.NAME, 'places'): 594 | arg3_val = arg3.children[2] 595 | arg3_val.prefix = "1e-" 596 | wrapped_places_val = wrap_parens_for_comparison(arg3_val) 597 | dest3.replace(wrapped_places_val) 598 | 599 | else: 600 | return False 601 | 602 | return True 603 | 604 | 605 | # ------------ Main portion of script ------------------------------- 606 | 607 | class NoseConversionRefactoringTool(refactor.MultiprocessRefactoringTool): 608 | def __init__(self, verbose: bool = False): 609 | flags = dict(print_function=True) 610 | super().__init__([], flags) 611 | level = logging.DEBUG if verbose else logging.INFO 612 | logging.basicConfig(format='%(name)s: %(message)s', level=level) 613 | logger = logging.getLogger('fissix.main') 614 | 615 | def get_fixers(self): 616 | pre_fixers = [] 617 | post_fixers = [] 618 | 619 | pre_fixers.extend(FixAssert1Arg.create_all(self.options, self.fixer_log)) 620 | pre_fixers.extend(FixAssert2Args.create_all(self.options, self.fixer_log)) 621 | pre_fixers.extend(FixAssertBinOp.create_all(self.options, self.fixer_log)) 622 | pre_fixers.extend(FixAssertAlmostEq.create_all(self.options, self.fixer_log)) 623 | 624 | return pre_fixers, post_fixers 625 | 626 | 627 | def setup(): 628 | # from nose import tools as nosetools 629 | # import inspect 630 | # for key in dir(nosetools): 631 | # if key.startswith('assert_'): 632 | # argspec = inspect.getargspec(getattr(nosetools, key)) 633 | # print(key, argspec) 634 | 635 | parser = argparse.ArgumentParser(description='Convert nose assertions to regular assertions for use by pytest') 636 | parser.add_argument('dir_name', type=str, 637 | help='folder name from which to start; all .py files under it will be converted') 638 | parser.add_argument('-w', dest='write', action='store_false', 639 | help='disable overwriting of original files') 640 | parser.add_argument('-v', dest='verbose', action='store_true', 641 | help='verbose output (list files changed, etc)') 642 | parser.add_argument('--version', action='version', 643 | version='%(prog)s {0}'.format(__version__)) 644 | 645 | return parser.parse_args() 646 | 647 | 648 | def main(): 649 | args = setup() 650 | if not Path(args.dir_name).exists(): 651 | print('ERROR: Path "%s" does not exist' % args.dir_name, file=sys.stderr) 652 | sys.exit(1) 653 | 654 | refac = NoseConversionRefactoringTool(args.verbose) 655 | refac.refactor_dir(args.dir_name, write=args.write) 656 | 657 | 658 | if __name__ == '__main__': 659 | main() 660 | 661 | __license__ = """ 662 | Copyright (c) 2016, Oliver Schoenborn 663 | All rights reserved. 664 | 665 | Redistribution and use in source and binary forms, with or without 666 | modification, are permitted provided that the following conditions are met: 667 | 668 | * Redistributions of source code must retain the above copyright notice, this 669 | list of conditions and the following disclaimer. 670 | 671 | * Redistributions in binary form must reproduce the above copyright notice, 672 | this list of conditions and the following disclaimer in the documentation 673 | and/or other materials provided with the distribution. 674 | 675 | * Neither the name of nose2pytest nor the names of its 676 | contributors may be used to endorse or promote products derived from 677 | this software without specific prior written permission. 678 | 679 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 680 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 681 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 682 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 683 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 684 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 685 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 686 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 687 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 688 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 689 | """ 690 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | files = setup.py nose2pytest/script.py 3 | current_version = 1.0.12 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name='nose2pytest', 6 | version='1.0.12', 7 | packages=['nose2pytest'], 8 | long_description=open('README.rst', encoding="UTF-8").read(), 9 | long_description_content_type="text/x-rst", 10 | entry_points={ 11 | 'console_scripts': [ 12 | 'nose2pytest = nose2pytest.script:main', 13 | ], 14 | 'pytest11': ['pytest_nose_assert_tools = nose2pytest.assert_tools'], 15 | }, 16 | url='https://github.com/schollii/nose2pytest', 17 | license='BSD-3', 18 | author='Oliver Schoenborn', 19 | author_email='oliver.schoenborn@gmail.com', 20 | description='Convert nose.tools.assert_ calls found in your Nose test modules into raw asserts for pytest', 21 | keywords='nose to pytest conversion', 22 | install_requires=[ 23 | 'fissix', 24 | ], 25 | python_requires='>=3.8,<3.12', 26 | classifiers=[ 27 | 'Development Status :: 5 - Production/Stable', 28 | 29 | # Indicate who your project is intended for 30 | 'Intended Audience :: Developers', 31 | 'Topic :: Software Development :: Testing', 32 | 33 | # Pick your license as you wish (should match "license" above) 34 | 'License :: OSI Approved :: BSD License', 35 | 36 | # Specify the Python versions you support here. 37 | 'Programming Language :: Python :: 3.8', 38 | 'Programming Language :: Python :: 3.9', 39 | 'Programming Language :: Python :: 3.10', 40 | 'Programming Language :: Python :: 3.11', 41 | ], 42 | ) 43 | -------------------------------------------------------------------------------- /tests/test_script.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from logging import StreamHandler 4 | from textwrap import dedent 5 | 6 | import pytest 7 | 8 | from nose2pytest.script import NoseConversionRefactoringTool 9 | from nose2pytest.assert_tools import _supported_nose_name 10 | 11 | log = logging.getLogger('nose2pytest') 12 | 13 | nosetools = {} 14 | pytesttools = {} 15 | 16 | refac = NoseConversionRefactoringTool() 17 | 18 | 19 | @pytest.fixture(scope="session", autouse=True) 20 | def setup_log(): 21 | redirect = StreamHandler(stream=sys.stdout) 22 | redirect.setLevel(logging.DEBUG) 23 | log.addHandler(redirect) 24 | log.setLevel(logging.DEBUG) 25 | 26 | import nose.tools 27 | for name, val in vars(nose.tools).items(): 28 | if _supported_nose_name(name): 29 | nosetools[name] = val 30 | 31 | import re, collections 32 | pytesttools['re'] = re 33 | pytesttools['collections'] = collections 34 | pytesttools['pytest'] = pytest 35 | 36 | 37 | def check_transformation(input, expect): 38 | result = refac.refactor_string(dedent(input + '\n'), 'script') 39 | assert dedent(expect + '\n') == str(result) 40 | 41 | 42 | def check_passes(refac, statement_in, expect_out): 43 | result = refac.refactor_string(statement_in + '\n', 'script') 44 | statement_out = str(result) 45 | exec(statement_in, nosetools) 46 | exec(statement_out, pytesttools) 47 | assert statement_out == expect_out + '\n' 48 | 49 | 50 | def check_fails(refac, statement_in, expect_out): 51 | result = refac.refactor_string(statement_in + '\n', 'script') 52 | statement_out = str(result) 53 | pytest.raises(AssertionError, exec, statement_in, nosetools) 54 | pytest.raises(AssertionError, exec, statement_out, pytesttools) 55 | assert statement_out == expect_out + '\n' 56 | 57 | 58 | class Test1Arg: 59 | 60 | def test_params(self): 61 | test_script = """ 62 | log.print("hi") 63 | 64 | assert_true(a) 65 | ok_(a) 66 | assert_true(a, 'text') 67 | assert_true(a, msg='text') 68 | """ 69 | check_transformation(test_script, """ 70 | log.print("hi") 71 | 72 | assert a 73 | assert a 74 | assert a, 'text' 75 | assert a, 'text' 76 | """) 77 | 78 | def test_parens(self): 79 | check_transformation('assert_true(a + \nb)', 'assert (a + \nb)') 80 | 81 | def test_generator(self): 82 | check_transformation('assert_true(x for x in range(1))', 'assert (x for x in range(1))') 83 | 84 | def test_same_results(self): 85 | check_passes(refac, 'assert_true(True)', 'assert True') 86 | check_fails(refac, 'assert_true(False)', 'assert False') 87 | 88 | check_passes(refac, 'ok_(True)', 'assert True') 89 | check_fails(refac, 'ok_(False)', 'assert False') 90 | 91 | check_passes(refac, 'assert_false(False)', 'assert not False') 92 | check_fails(refac, 'assert_false(True)', 'assert not True') 93 | 94 | check_passes(refac, 'assert_is_none(None)', 'assert None is None') 95 | check_fails(refac, 'assert_is_none("")', 'assert "" is None') 96 | 97 | check_passes(refac, 'assert_is_not_none("")', 'assert "" is not None') 98 | check_fails(refac, 'assert_is_not_none(None)', 'assert None is not None') 99 | 100 | 101 | class Test2Args: 102 | 103 | def test_params(self): 104 | test_script = """ 105 | assert_in(a, b) 106 | assert_in(a, b, 'text') 107 | assert_in(a, b, msg='text') 108 | """ 109 | check_transformation(test_script, """ 110 | assert a in b 111 | assert a in b, 'text' 112 | assert a in b, 'text' 113 | """) 114 | 115 | def test_dont_add_parens(self): 116 | check_transformation('assert_in(a, c)', 117 | 'assert a in c') 118 | check_transformation('assert_in(a.b, c)', 119 | 'assert a.b in c') 120 | check_transformation('assert_in(a.b(), c)', 121 | 'assert a.b() in c') 122 | check_transformation('assert_in(a(), d)', 123 | 'assert a() in d') 124 | check_transformation('assert_in(a[1], d)', 125 | 'assert a[1] in d') 126 | check_transformation('assert_in((a+b), d)', 127 | 'assert (a+b) in d') 128 | check_transformation('assert_in((a+b), d)', 129 | 'assert (a+b) in d') 130 | check_transformation('assert_in(-a, +b)', 131 | 'assert -a in +b') 132 | 133 | def test_add_parens(self): 134 | check_transformation('assert_in(a == b, d)', 135 | 'assert (a == b) in d') 136 | check_transformation('assert_in(a != b, d)', 137 | 'assert (a != b) in d') 138 | check_transformation('assert_in(b <= c, d)', 139 | 'assert (b <= c) in d') 140 | check_transformation('assert_in(c >= d, d)', 141 | 'assert (c >= d) in d') 142 | check_transformation('assert_in(d < e, d)', 143 | 'assert (d < e) in d') 144 | check_transformation('assert_in(d > e, d)', 145 | 'assert (d > e) in d') 146 | check_transformation('eq_(a in b, c)', 147 | 'assert (a in b) == c') 148 | check_transformation('assert_equal(a in b, c)', 149 | 'assert (a in b) == c') 150 | check_transformation('assert_equal(a not in b, c)', 151 | 'assert (a not in b) == c') 152 | check_transformation('assert_equal(a is b, c)', 153 | 'assert (a is b) == c') 154 | check_transformation('assert_equal(a is not b, c)', 155 | 'assert (a is not b) == c') 156 | check_transformation('assert_equal(not a, c)', 157 | 'assert (not a) == c') 158 | check_transformation('assert_equal(a and b, c or d)', 159 | 'assert (a and b) == (c or d)') 160 | check_transformation('assert_in(a.b + c, d)', 161 | 'assert a.b + c in d') 162 | check_transformation('assert_in(a() + b, d)', 163 | 'assert a() + b in d') 164 | check_transformation('assert_in(a + b, c + d)', 165 | 'assert a + b in c + d') 166 | check_transformation('assert_in(a + b, c + d, "text")', 167 | 'assert a + b in c + d, "text"') 168 | check_transformation('assert_equal(a + b if c + d < 0 else e + f if g+h < 0 else i + j, -100)', 169 | 'assert (a + b if c + d < 0 else e + f if g+h < 0 else i + j) == -100') 170 | 171 | def test_newline_all(self): 172 | test_script = """ 173 | assert_in(long_a, 174 | long_b) 175 | """ 176 | check_transformation(test_script, """ 177 | assert (long_a in 178 | long_b) 179 | """) 180 | 181 | test_script = """ 182 | assert_in( 183 | long_a, long_b) 184 | """ 185 | check_transformation(test_script, """ 186 | assert ( 187 | long_a in long_b) 188 | """) 189 | 190 | test_script = """ 191 | assert_in(long_a, 192 | long_b + something) 193 | """ 194 | check_transformation(test_script, """ 195 | assert (long_a in 196 | long_b + something) 197 | """) 198 | 199 | test_script = """ 200 | assert_in(long_a, 201 | long_b > something) 202 | """ 203 | check_transformation(test_script, """ 204 | assert (long_a in 205 | (long_b > something)) 206 | """) 207 | 208 | test_script = """ 209 | assert_in(a, long_b + 210 | something) 211 | """ 212 | check_transformation(test_script, """ 213 | assert (a in long_b + 214 | something) 215 | """) 216 | 217 | def test_same_results(self): 218 | check_passes(refac, 'assert_equal(123, 123)', 'assert 123 == 123') 219 | check_fails(refac, 'assert_equal(123, 456)', 'assert 123 == 456') 220 | check_passes(refac, 'eq_(123, 123)', 'assert 123 == 123') 221 | check_fails(refac, 'eq_(123, 456)', 'assert 123 == 456') 222 | 223 | check_passes(refac, 'assert_not_equal(123, 456)', 'assert 123 != 456') 224 | check_fails(refac, 'assert_not_equal(123, 123)', 'assert 123 != 123') 225 | 226 | check_passes(refac, 'assert_list_equal([123, 456], [123, 456])', 'assert [123, 456] == [123, 456]') 227 | check_fails(refac, 'assert_list_equal([123, 123], [123, 456])', 'assert [123, 123] == [123, 456]') 228 | 229 | check_passes(refac, 'assert_tuple_equal((123, 456), (123, 456))', 'assert (123, 456) == (123, 456)') 230 | check_fails(refac, 'assert_tuple_equal((123, 123), (123, 456))', 'assert (123, 123) == (123, 456)') 231 | 232 | check_passes(refac, 'assert_set_equal({123, 456}, {123, 456})', 'assert {123, 456} == {123, 456}') 233 | check_fails(refac, 'assert_set_equal({123, 123}, {123, 456})', 'assert {123, 123} == {123, 456}') 234 | 235 | check_passes(refac, 'assert_dict_equal(dict(a=123, b=456), dict(a=123, b=456))', 236 | 'assert dict(a=123, b=456) == dict(a=123, b=456)') 237 | check_fails(refac, 'assert_dict_equal(dict(a=123, b=456), dict(a=123, b=123))', 238 | 'assert dict(a=123, b=456) == dict(a=123, b=123)') 239 | check_fails(refac, 'assert_dict_equal(dict(a=123, b=456), dict(a=123, c=456))', 240 | 'assert dict(a=123, b=456) == dict(a=123, c=456)') 241 | 242 | check_passes(refac, 'assert_multi_line_equal("""1\n2\n""", """1\n2\n""")', 243 | 'assert """1\n2\n""" == """1\n2\n"""') 244 | check_fails(refac, 'assert_multi_line_equal("""1\n2\n""", """1\n3\n""")', 'assert """1\n2\n""" == """1\n3\n"""') 245 | 246 | check_passes(refac, 'assert_greater(123, 1)', 'assert 123 > 1') 247 | check_fails(refac, 'assert_greater(123, 123)', 'assert 123 > 123') 248 | check_fails(refac, 'assert_greater(123, 456)', 'assert 123 > 456') 249 | 250 | check_passes(refac, 'assert_greater_equal(123, 1)', 'assert 123 >= 1') 251 | check_passes(refac, 'assert_greater_equal(123, 123)', 'assert 123 >= 123') 252 | check_fails(refac, 'assert_greater_equal(123, 456)', 'assert 123 >= 456') 253 | 254 | check_passes(refac, 'assert_less(123, 456)', 'assert 123 < 456') 255 | check_fails(refac, 'assert_less(123, 123)', 'assert 123 < 123') 256 | check_fails(refac, 'assert_less(123, 1)', 'assert 123 < 1') 257 | 258 | check_passes(refac, 'assert_less_equal(123, 456)', 'assert 123 <= 456') 259 | check_passes(refac, 'assert_less_equal(123, 123)', 'assert 123 <= 123') 260 | check_fails(refac, 'assert_less_equal(123, 1)', 'assert 123 <= 1') 261 | 262 | check_passes(refac, 'assert_in(123, [123, 456])', 'assert 123 in [123, 456]') 263 | check_fails(refac, 'assert_in(123, [789, 456])', 'assert 123 in [789, 456]') 264 | 265 | check_passes(refac, 'assert_not_in(123, [789, 456])', 'assert 123 not in [789, 456]') 266 | check_fails(refac, 'assert_not_in(123, [123, 456])', 'assert 123 not in [123, 456]') 267 | 268 | check_passes(refac, 'assert_is(123, 123)', 'assert 123 is 123') 269 | check_fails(refac, 'assert_is(123, 1)', 'assert 123 is 1') 270 | 271 | check_passes(refac, 'assert_is_not(123, 1)', 'assert 123 is not 1') 272 | check_fails(refac, 'assert_is_not(123, 123)', 'assert 123 is not 123') 273 | 274 | check_passes(refac, 'assert_is_instance(123, int)', 'assert isinstance(123, int)') 275 | check_fails(refac, 'assert_is_instance(123, float)', 'assert isinstance(123, float)') 276 | 277 | check_passes(refac, 'assert_count_equal([456, 789, 456], [456, 456, 789])', 278 | 'assert collections.Counter([456, 789, 456]) == collections.Counter([456, 456, 789])') 279 | check_fails(refac, 'assert_count_equal([789, 456], [456])', 280 | 'assert collections.Counter([789, 456]) == collections.Counter([456])') 281 | 282 | check_passes(refac, 'assert_regex("125634", "12.*34")', 'assert re.search("12.*34","125634")') 283 | check_fails(refac, 'assert_regex("125678", "12.*34")', 'assert re.search("12.*34","125678")') 284 | 285 | check_passes(refac, 'assert_not_regex("125678", "12.*34")', 'assert not re.search("12.*34","125678")') 286 | check_fails(refac, 'assert_not_regex("125634", "12.*34")', 'assert not re.search("12.*34","125634")') 287 | 288 | 289 | class Test3Args: 290 | 291 | def test_no_add_parens(self): 292 | check_transformation('assert_almost_equal(a * b, ~c, delta=d**e)', 293 | 'assert a * b == pytest.approx(~c, abs=d**e)') 294 | 295 | def test_add_parens(self): 296 | check_transformation('assert_almost_equal(a + b, c, delta=d>e)', 297 | 'assert (a + b) == pytest.approx(c, abs=(d>e))') 298 | check_transformation('assert_almost_equal(a | b, c ^ d, delta=0.1)', 299 | 'assert (a | b) == pytest.approx((c ^ d), abs=0.1)') 300 | check_transformation('assert_almost_equal(a & b, c << d, delta=0.1)', 301 | 'assert (a & b) == pytest.approx((c << d), abs=0.1)') 302 | check_transformation('assert_almost_equal(a or b, c >> d, delta=0.1)', 303 | 'assert (a or b) == pytest.approx((c >> d), abs=0.1)') 304 | 305 | def test_almost_equal_with_delta(self): 306 | check_transformation('assert_almost_equal(123.456, 124, delta=0.6, msg="reason")', 307 | 'assert 123.456 == pytest.approx(124, abs=0.6), "reason"') 308 | check_transformation('assert_almost_equal(123.456, 124, msg="reason", delta=0.6)', 309 | 'assert 123.456 == pytest.approx(124, abs=0.6), "reason"') 310 | 311 | check_passes(refac, 'assert_almost_equal(123.456, 123.5, delta=0.1)', 312 | 'assert 123.456 == pytest.approx(123.5, abs=0.1)') 313 | check_passes(refac, 'assert_almost_equal(123.456, 123.5, delta=0.2, msg="text")', 314 | 'assert 123.456 == pytest.approx(123.5, abs=0.2), "text"') 315 | check_passes(refac, 'assert_almost_equal(123.456, 123.5, msg="text", delta=0.3)', 316 | 'assert 123.456 == pytest.approx(123.5, abs=0.3), "text"') 317 | check_fails(refac, 'assert_almost_equal(123.456, 124, delta=0.1)', 318 | 'assert 123.456 == pytest.approx(124, abs=0.1)') 319 | 320 | def test_not_almost_equal(self): 321 | check_transformation('assert_not_almost_equal(123.456, 124, msg="reason")', 322 | 'assert 123.456 != pytest.approx(124, abs=1e-7), "reason"') 323 | check_transformation('assert_not_almost_equal(123.456, 124, delta=0.6, msg="reason")', 324 | 'assert 123.456 != pytest.approx(124, abs=0.6), "reason"') 325 | check_transformation('assert_not_almost_equal(123.456, 124, msg="reason", delta=0.6)', 326 | 'assert 123.456 != pytest.approx(124, abs=0.6), "reason"') 327 | check_transformation('assert_not_almost_equal(123.456, 124, places=4, msg="reason")', 328 | 'assert 123.456 != pytest.approx(124, abs=1e-4), "reason"') 329 | check_transformation('assert_not_almost_equal(123.456, 124, msg="reason", places=4)', 330 | 'assert 123.456 != pytest.approx(124, abs=1e-4), "reason"') 331 | 332 | check_passes(refac, 333 | 'assert_not_almost_equal(123.456, 123.5, delta=0.01)', 334 | 'assert 123.456 != pytest.approx(123.5, abs=0.01)') 335 | check_passes(refac, 336 | 'assert_not_almost_equal(123.456, 123.5, delta=0.02, msg="text")', 337 | 'assert 123.456 != pytest.approx(123.5, abs=0.02), "text"') 338 | check_passes(refac, 339 | 'assert_not_almost_equal(123.456, 123.5, msg="text", delta=0.03)', 340 | 'assert 123.456 != pytest.approx(123.5, abs=0.03), "text"') 341 | check_fails(refac, 342 | 'assert_not_almost_equal(123.456, 124, delta=0.6)', 343 | 'assert 123.456 != pytest.approx(124, abs=0.6)') 344 | 345 | def test_almost_equal_with_places(self): 346 | check_transformation('assert_almost_equal(123.456, 124)', 347 | 'assert 123.456 == pytest.approx(124, abs=1e-7)') 348 | check_transformation('assert_almost_equal(123.456, 124, msg="reason")', 349 | 'assert 123.456 == pytest.approx(124, abs=1e-7), "reason"') 350 | 351 | check_transformation('assert_almost_equal(123.456, 124, places=1)', 352 | 'assert 123.456 == pytest.approx(124, abs=1e-1)') 353 | check_transformation('assert_almost_equal(123.456, 124, places=6)', 354 | 'assert 123.456 == pytest.approx(124, abs=1e-6)') 355 | check_transformation('assert_almost_equal(123.456, 124, places=6, msg="reason")', 356 | 'assert 123.456 == pytest.approx(124, abs=1e-6), "reason"') 357 | check_transformation('assert_almost_equal(123.456, 124, msg="reason", places=6)', 358 | 'assert 123.456 == pytest.approx(124, abs=1e-6), "reason"') 359 | 360 | check_passes(refac, 361 | 'assert_almost_equal(123.456, 123.450, places=1)', 362 | 'assert 123.456 == pytest.approx(123.450, abs=1e-1)') 363 | 364 | 365 | class TestAssertTools: 366 | 367 | def test_dict_keys_subset(self): 368 | dict1 = dict(a=1, b=2, c=3) 369 | 370 | # check keys are subset: 371 | dict2 = dict1.copy() 372 | pytest.assert_dict_contains_subset(dict1, dict2) 373 | 374 | dict2['d'] = 4 375 | pytest.assert_dict_contains_subset(dict1, dict2) 376 | 377 | del dict2['a'] 378 | pytest.raises(AssertionError, pytest.assert_dict_contains_subset, dict1, dict2) 379 | # assert_dict_contains_subset(dict1, dict2) 380 | 381 | def test_dict_values_subset(self): 382 | dict1 = dict(a=1, b=2, c=3) 383 | 384 | # check keys are subset: 385 | dict2 = dict1.copy() 386 | dict2['d'] = 4 387 | dict2['a'] = 4 388 | pytest.raises(AssertionError, pytest.assert_dict_contains_subset, dict1, dict2) 389 | # assert_dict_contains_subset(dict1, dict2) 390 | -------------------------------------------------------------------------------- /tools/find_pattern.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Script that makes determining PATTERN for a new fix much easier. 4 | 5 | Figuring out exactly what PATTERN I want for a given fixer class is 6 | getting tedious. This script will step through each possible subtree 7 | for a given string, allowing you to select which one you want. It will 8 | then try to figure out an appropriate pattern to match that tree. This 9 | pattern will require some editing (it will be overly restrictive) but 10 | should provide a solid base to work with and handle the tricky parts. 11 | 12 | Usage: 13 | 14 | python find_pattern.py "g.throw(E, V, T)" 15 | 16 | This will step through each subtree in the parse. To reject a 17 | candidate subtree, hit enter; to accept a candidate, hit "y" and 18 | enter. The pattern will be spit out to stdout. 19 | 20 | For example, the above will yield a succession of possible snippets, 21 | skipping all leaf-only trees. I accept 22 | 23 | 'g.throw(E, V, T)' 24 | 25 | This causes find_pattern to spit out 26 | 27 | power< 'g' trailer< '.' 'throw' > 28 | trailer< '(' arglist< 'E' ',' 'V' ',' 'T' > ')' > > 29 | 30 | 31 | Some minor tweaks later, I'm left with 32 | 33 | power< any trailer< '.' 'throw' > 34 | trailer< '(' args=arglist< exc=any ',' val=any [',' tb=any] > ')' > > 35 | 36 | which is exactly what I was after. 37 | 38 | Larger snippets can be placed in a file (as opposed to a command-line 39 | arg) and processed with the -f option. 40 | """ 41 | 42 | __author__ = "Collin Winter " 43 | 44 | # Python imports 45 | import optparse 46 | import sys 47 | from io import StringIO 48 | 49 | # Local imports 50 | from fissix import pytree 51 | from fissix.pgen2 import driver 52 | from fissix.pygram import python_symbols, python_grammar 53 | 54 | driver = driver.Driver(python_grammar, convert=pytree.convert) 55 | 56 | def main(args): 57 | parser = optparse.OptionParser(usage="find_pattern.py [options] [string]") 58 | parser.add_option("-f", "--file", action="store", 59 | help="Read a code snippet from the specified file") 60 | 61 | # Parse command line arguments 62 | options, args = parser.parse_args(args) 63 | if options.file: 64 | tree = driver.parse_file(options.file) 65 | elif len(args) > 1: 66 | tree = driver.parse_stream(StringIO(args[1] + "\n")) 67 | else: 68 | print >>sys.stderr, "You must specify an input file or an input string" 69 | return 1 70 | 71 | examine_tree(tree) 72 | return 0 73 | 74 | def examine_tree(tree): 75 | for node in tree.post_order(): 76 | if isinstance(node, pytree.Leaf): 77 | continue 78 | print(repr(str(node))) 79 | verdict = input('enter to ignore, y to keep') 80 | if verdict.strip(): 81 | print(find_pattern(node)) 82 | return 83 | 84 | def find_pattern(node): 85 | if isinstance(node, pytree.Leaf): 86 | return repr(node.value) 87 | 88 | return find_symbol(node.type) + \ 89 | "< " + " ".join(find_pattern(n) for n in node.children) + " >" 90 | 91 | def find_symbol(sym): 92 | for n, v in python_symbols.__dict__.items(): 93 | if v == sym: 94 | return n 95 | 96 | if __name__ == "__main__": 97 | sys.exit(main(sys.argv)) 98 | -------------------------------------------------------------------------------- /tools/fixer_ex_pattern.txt: -------------------------------------------------------------------------------- 1 | assert_is_none(msg=ccc) 2 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{38, 39, 310, 311} 3 | 4 | [testenv] 5 | deps = 6 | pytest 7 | nose 8 | 9 | commands = pytest 10 | --------------------------------------------------------------------------------