dask / dask-image

Distributed image processing
http://image.dask.org/en/latest/
BSD 3-Clause "New" or "Revised" License
210 stars 47 forks source link

Flake8 problems #307

Open GenevieveBuckley opened 1 year ago

GenevieveBuckley commented 1 year ago

There are two problems:

  1. I don't think pytest is running flake8 like it should be, and
  2. When you do force flake8 to run with pytest manually, there are failures. Something is wrong with the pytest flake8 hook (maybe a version mismatch between pytest, flake8, and pytest-flake8?)

Describe the issue: We have --flake8 as a pytest addopts extra:

[tool:pytest]
addopts = --flake8

...which I think is equivalent to the command line call

pytest --flake8

But when I run just pytest from the command line, it doesn't seem to pick up the extra addopts bit. All the tests pass with just pytest, but there are failures when I run pytest --flake8 explicitly.

Minimal Complete Verifiable Example:

  1. Install a development version of dask-image, including the test requirements. Also pip install pandas, as that is a test requirement for the new-ish find_objects function.
  2. Run pytest, and observe the tests passing.
  3. Run pytest --flake8 (which should be equivalent to the above, given we are using addopts "--flake8"), but this time observe some tests failing due to flake8.
Details (click to expand): ``` _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <_pytest.config.PytestPluginManager object at 0x1052391e0> hook_name = 'pytest_runtest_call' methods = [>, ...] kwargs = {'item': }, firstresult = False def _hookexec(self, hook_name, methods, kwargs, firstresult): # called from all hookcaller instances. # enable_tracing will set its own wrapping function at self._inner_hookexec > return self._inner_hookexec(hook_name, methods, kwargs, firstresult) ../../../mambaforge/envs/daskimg/lib/python3.10/site-packages/pluggy/_manager.py:80: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ hook_name = 'pytest_runtest_call' hook_impls = [>, ...] caller_kwargs = {'item': }, firstresult = False def _multicall(hook_name, hook_impls, caller_kwargs, firstresult): """Execute a call into multiple python functions/methods and return the result(s). ``caller_kwargs`` comes from _HookCaller.__call__(). """ __tracebackhide__ = True results = [] excinfo = None try: # run impl and wrapper setup functions in a loop teardowns = [] try: for hook_impl in reversed(hook_impls): try: args = [caller_kwargs[argname] for argname in hook_impl.argnames] except KeyError: for argname in hook_impl.argnames: if argname not in caller_kwargs: raise HookCallError( f"hook call must provide argument {argname!r}" ) if hook_impl.hookwrapper: try: gen = hook_impl.function(*args) next(gen) # first yield teardowns.append(gen) except StopIteration: _raise_wrapfail(gen, "did not yield") else: res = hook_impl.function(*args) if res is not None: results.append(res) if firstresult: # halt further impl calls break except BaseException: excinfo = sys.exc_info() finally: if firstresult: # first result hooks return a single value outcome = _Result(results[0] if results else None, excinfo) else: outcome = _Result(results, excinfo) # run all wrapper post-yield blocks for gen in reversed(teardowns): try: gen.send(outcome) _raise_wrapfail(gen, "has second yield") except StopIteration: pass > return outcome.get_result() ../../../mambaforge/envs/daskimg/lib/python3.10/site-packages/pluggy/_callers.py:60: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = def get_result(self): """Get the result(s) for this hook call. If the hook was marked as a ``firstresult`` only a single value will be returned otherwise a list of results. """ __tracebackhide__ = True if self._excinfo is None: return self._result else: ex = self._excinfo > raise ex[1].with_traceback(ex[2]) ../../../mambaforge/envs/daskimg/lib/python3.10/site-packages/pluggy/_result.py:60: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ hook_name = 'pytest_runtest_call' hook_impls = [>, ...] caller_kwargs = {'item': }, firstresult = False def _multicall(hook_name, hook_impls, caller_kwargs, firstresult): """Execute a call into multiple python functions/methods and return the result(s). ``caller_kwargs`` comes from _HookCaller.__call__(). """ __tracebackhide__ = True results = [] excinfo = None try: # run impl and wrapper setup functions in a loop teardowns = [] try: for hook_impl in reversed(hook_impls): try: args = [caller_kwargs[argname] for argname in hook_impl.argnames] except KeyError: for argname in hook_impl.argnames: if argname not in caller_kwargs: raise HookCallError( f"hook call must provide argument {argname!r}" ) if hook_impl.hookwrapper: try: gen = hook_impl.function(*args) next(gen) # first yield teardowns.append(gen) except StopIteration: _raise_wrapfail(gen, "did not yield") else: > res = hook_impl.function(*args) ../../../mambaforge/envs/daskimg/lib/python3.10/site-packages/pluggy/_callers.py:39: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ item = def pytest_runtest_call(item: Item) -> None: _update_current_test_var(item, "call") try: del sys.last_type del sys.last_value del sys.last_traceback except AttributeError: pass try: item.runtest() except Exception as e: # Store trace info to allow postmortem debugging sys.last_type = type(e) sys.last_value = e assert e.__traceback__ is not None # Skip *this* frame sys.last_traceback = e.__traceback__.tb_next > raise e ../../../mambaforge/envs/daskimg/lib/python3.10/site-packages/_pytest/runner.py:175: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ item = def pytest_runtest_call(item: Item) -> None: _update_current_test_var(item, "call") try: del sys.last_type del sys.last_value del sys.last_traceback except AttributeError: pass try: > item.runtest() ../../../mambaforge/envs/daskimg/lib/python3.10/site-packages/_pytest/runner.py:167: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = def runtest(self): with BytesIO() as bo, TextIOWrapper(bo, encoding='utf-8') as to, \ BytesIO() as be, TextIOWrapper(be, encoding='utf-8') as te, \ redirect_stdout(to), redirect_stderr(te): > found_errors = check_file( self.fspath, self.flake8ignore, self.maxlength, self.maxdoclength, self.maxcomplexity, self.showsource, self.statistics ) ../../../mambaforge/envs/daskimg/lib/python3.10/site-packages/pytest_flake8.py:136: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ path = local('/Users/genevieb/Documents/GitHub/dask-image/tests/test_dask_image/test_ndmorph/test_ndmorph.py') flake8ignore = [], maxlength = '', maxdoclenght = '', maxcomplexity = '' showsource = [], statistics = [] def check_file(path, flake8ignore, maxlength, maxdoclenght, maxcomplexity, showsource, statistics): """Run flake8 over a single file, and return the number of failures.""" args = [] if maxlength: args += ['--max-line-length', maxlength] if maxdoclenght: args += ['--max-doc-length', maxdoclenght] if maxcomplexity: args += ['--max-complexity', maxcomplexity] if showsource: args += ['--show-source'] if statistics: args += ['--statistics'] app = application.Application() > prelim_opts, remaining_args = app.parse_preliminary_options(args) E AttributeError: 'Application' object has no attribute 'parse_preliminary_options' ../../../mambaforge/envs/daskimg/lib/python3.10/site-packages/pytest_flake8.py:216: AttributeError FAILED dask_image/__init__.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/_version.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/dispatch/__init__.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/dispatch/_dispatch_ndfilters.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/dispatch/_dispatch_ndinterp.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/dispatch/_dispatch_ndmorph.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/dispatch/_dispatcher.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/dispatch/_utils.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/imread/__init__.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndfilters/__init__.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndfilters/_conv.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndfilters/_diff.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndfilters/_edge.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndfilters/_gaussian.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndfilters/_generic.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndfilters/_order.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndfilters/_smooth.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndfilters/_threshold.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndfilters/_utils.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndfourier/__init__.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndfourier/_utils.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndinterp/__init__.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndmeasure/__init__.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndmeasure/_utils/__init__.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndmeasure/_utils/_find_objects.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndmeasure/_utils/_label.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndmorph/__init__.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndmorph/_ops.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED dask_image/ndmorph/_utils.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED docs/conf.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED docs/release/generate_release_notes.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/__init__.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_imread/__init__.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_imread/test_core.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_imread/test_cupy_imread.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndfilters/__init__.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndfilters/test__conv.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndfilters/test__diff.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndfilters/test__edge.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndfilters/test__gaussian.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndfilters/test__generic.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndfilters/test__order.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndfilters/test__smooth.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndfilters/test__threshold.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndfilters/test__utils.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndfilters/test_cupy_ndfilters.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndfilters/test_cupy_threshold.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndfourier/test__utils.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndfourier/test_core.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndinterp/test_affine_transformation.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndinterp/test_spline_filter.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndmeasure/__init__.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndmeasure/test__utils.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndmeasure/test_core.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndmeasure/test_find_objects.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndmorph/__init__.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndmorph/test__utils.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndmorph/test_cupy_ndmorph.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... FAILED tests/test_dask_image/test_ndmorph/test_ndmorph.py::flake-8::FLAKE8 - AttributeError: 'Application' object has no attribute 'parse_preliminary_op... ```

Anything else we need to know?:

I've noticed the same problem in https://github.com/dask/dask-image/pull/306

I might have done something silly by adding a separate pytest.ini file in https://github.com/dask/dask-image/pull/151, which might override or conflict with the pytest section in setup.cfg. So this might have masked the problem for a while, but since I see the same problems with the new pyrpoject.toml configuration (https://github.com/dask/dask-image/pull/306) that can't explain everything.

Environment:

conda list (click to expand): ``` # Name Version Build Channel appnope 0.1.3 pyhd8ed1ab_0 conda-forge asttokens 2.2.1 pyhd8ed1ab_0 conda-forge attrs 22.2.0 pypi_0 pypi backcall 0.2.0 pyh9f0ad1d_0 conda-forge backports 1.0 pyhd8ed1ab_3 conda-forge backports.functools_lru_cache 1.6.4 pyhd8ed1ab_0 conda-forge bzip2 1.0.8 h3422bc3_4 conda-forge ca-certificates 2022.12.7 h4653dfc_0 conda-forge click 8.1.3 pypi_0 pypi cloudpickle 2.2.1 pypi_0 pypi dask 2023.3.2 pypi_0 pypi dask-image 2022.9.0+30.gb08d494 dev_0 decorator 5.1.1 pyhd8ed1ab_0 conda-forge exceptiongroup 1.1.1 pypi_0 pypi executing 1.2.0 pyhd8ed1ab_0 conda-forge flake8 6.0.0 pypi_0 pypi fsspec 2023.3.0 pypi_0 pypi imageio 2.27.0 pypi_0 pypi importlib-metadata 6.1.0 pypi_0 pypi iniconfig 2.0.0 pypi_0 pypi ipython 8.11.0 pyhd1c38e8_0 conda-forge jedi 0.18.2 pyhd8ed1ab_0 conda-forge libffi 3.4.2 h3422bc3_5 conda-forge libsqlite 3.40.0 h76d750c_0 conda-forge libzlib 1.2.13 h03a7124_4 conda-forge locket 1.0.0 pypi_0 pypi matplotlib-inline 0.1.6 pyhd8ed1ab_0 conda-forge mccabe 0.7.0 pypi_0 pypi ncurses 6.3 h07bb92c_1 conda-forge numpy 1.24.2 pypi_0 pypi openssl 3.1.0 h03a7124_0 conda-forge packaging 23.0 pypi_0 pypi pandas 1.5.3 pypi_0 pypi parso 0.8.3 pyhd8ed1ab_0 conda-forge partd 1.3.0 pypi_0 pypi pexpect 4.8.0 pyh1a96a4e_2 conda-forge pickleshare 0.7.5 py_1003 conda-forge pillow 9.4.0 pypi_0 pypi pims 0.6.1 pypi_0 pypi pip 23.0.1 pyhd8ed1ab_0 conda-forge pluggy 1.0.0 pypi_0 pypi prompt-toolkit 3.0.38 pyha770c72_0 conda-forge prompt_toolkit 3.0.38 hd8ed1ab_0 conda-forge ptyprocess 0.7.0 pyhd3deb0d_0 conda-forge pure_eval 0.2.2 pyhd8ed1ab_0 conda-forge pycodestyle 2.10.0 pypi_0 pypi pyflakes 3.0.1 pypi_0 pypi pygments 2.14.0 pyhd8ed1ab_0 conda-forge pytest 7.2.2 pypi_0 pypi pytest-flake8 1.1.1 pypi_0 pypi pytest-timeout 2.1.0 pypi_0 pypi python 3.10.10 h3ba56d0_0_cpython conda-forge python-dateutil 2.8.2 pypi_0 pypi pytz 2023.3 pypi_0 pypi pyyaml 6.0 pypi_0 pypi readline 8.2 h92ec313_1 conda-forge scipy 1.10.1 pypi_0 pypi setuptools 67.6.1 pyhd8ed1ab_0 conda-forge six 1.16.0 pyh6c4a22f_0 conda-forge slicerator 1.1.0 pypi_0 pypi stack_data 0.6.2 pyhd8ed1ab_0 conda-forge tifffile 2023.3.21 pypi_0 pypi tk 8.6.12 he1e0b03_0 conda-forge tomli 2.0.1 pypi_0 pypi toolz 0.12.0 pypi_0 pypi traitlets 5.9.0 pyhd8ed1ab_0 conda-forge tzdata 2023c h71feb2d_0 conda-forge wcwidth 0.2.6 pyhd8ed1ab_0 conda-forge wheel 0.40.0 pyhd8ed1ab_0 conda-forge xz 5.2.6 h57fd34a_0 conda-forge zipp 3.15.0 pypi_0 pypi ```
GenevieveBuckley commented 1 year ago

Suggestion from https://github.com/dask/dask-image/pull/306#discussion_r1152641096

Maybe FlakeHell is a better option? This blogpost seems encouraging: https://dev.to/bowmanjd/using-flake8-and-pyproject-toml-with-flakehell-1cn1

m-albert commented 1 year ago

Oh yeah, flake8 shows quite some problems when invoked explicitely!

Not sure about setup.py, but since you're working on migrating to pyproject.toml: It seems that indicating the addopts in an entry [tool.pytest.ini_options] instead of [tool.pytest] does the trick. It works for me locally. Found here

m-albert commented 1 year ago

Regarding the flake8 related failures themselves, it seems they're coming from the pytest plugin pytest-flake8: https://stackoverflow.com/questions/61743089/python-testing-with-flake8-and-pytest-results-in-attributeerror-application-o.

Actually, the pytest-flake8 project seems to be unmaintained: https://github.com/pypi/support/issues/2587.

m-albert commented 1 year ago

Maybe FlakeHell is a better option? This blogpost seems encouraging: https://dev.to/bowmanjd/using-flake8-and-pyproject-toml-with-flakehell-1cn1

Looks like the project was archived and this fork is active https://flakeheaven.readthedocs.io/en/latest/

I'm very unexperienced here 😁. What would be the advantage of using flakeheaven instead of flake8 directly? Is it that flakeheaven can be configured in pyproject.toml? Flake8 could be configured in its own config file.

Also, what would it mean to decouple the linting from testing, that additional steps would need to be added to the github actions configuration? Locally for contributors I've seen that we're already suggesting to explicitely call flake8 in CONTRIBUTING.rst.