Skip to content

Deprecate Python 3.6. #192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jan 28, 2022
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ repos:
rev: v2.31.0
hooks:
- id: pyupgrade
args: [--py36-plus]
args: [--py37-plus]
- repo: https://github.com/asottile/reorder_python_imports
rev: v2.6.0
hooks:
- id: reorder-python-imports
args: [--py37-plus, --add-import, 'from __future__ import annotations']
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.20.0
hooks:
Expand Down
5 changes: 2 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,8 @@ Installation

.. start-installation

pytask is available on `PyPI <https://pypi.org/project/pytask>`_ for Python >= 3.6.1 and
on `Anaconda.org <https://anaconda.org/conda-forge/pytask>`_ for Python >= 3.7. Install
the package with
pytask is available on `PyPI <https://pypi.org/project/pytask>`_ and on `Anaconda.org
<https://anaconda.org/conda-forge/pytask>`_. Install the package with

.. code-block:: console

Expand Down
1 change: 1 addition & 0 deletions docs/source/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ all releases are available on `PyPI <https://pypi.org/project/pytask>`_ and
------------------

- :gh:`191` adds a guide on how to profile pytask to the developer's guide.
- :gh:`192` deprecates Python 3.6.
- :gh:`193` adds more figures to the documentation.
- :gh:`194` updates the ``README.rst``.
- :gh:`196` references the two new cookiecutters for projects and plugins.
Expand Down
4 changes: 3 additions & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
# If extensions (or modules to document with autodoc) are in another directory, add
# these directories to sys.path here. If the directory is relative to the documentation
# root, use os.path.abspath to make it absolute, like shown here.
from __future__ import annotations

from importlib.metadata import version

import sphinx
Expand Down Expand Up @@ -137,7 +139,7 @@
}


def setup(app: "sphinx.application.Sphinx") -> None:
def setup(app: sphinx.application.Sphinx) -> None:
app.add_object_type(
"confval",
"confval",
Expand Down
3 changes: 1 addition & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ classifiers =
Operating System :: POSIX
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Expand All @@ -43,7 +42,7 @@ install_requires =
pluggy
pony>=0.7.13
rich
python_requires = >=3.6.1
python_requires = >=3.7
include_package_data = True
package_dir =
=src
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from setuptools import setup


Expand Down
2 changes: 2 additions & 0 deletions src/_pytask/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

try:
from ._version import version as __version__
except ImportError:
Expand Down
7 changes: 4 additions & 3 deletions src/_pytask/build.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Implement the build command."""
from __future__ import annotations

import sys
from typing import Any
from typing import Dict
from typing import TYPE_CHECKING

import click
Expand All @@ -28,7 +29,7 @@ def pytask_extend_command_line_interface(cli: click.Group) -> None:
cli.add_command(build)


def main(config_from_cli: Dict[str, Any]) -> Session:
def main(config_from_cli: dict[str, Any]) -> Session:
"""Run pytask.

This is the main command to run pytask which usually receives kwargs from the
Expand Down Expand Up @@ -117,7 +118,7 @@ def main(config_from_cli: Dict[str, Any]) -> Session:
type=click.Choice(["yes", "no"]),
help="Choose whether tracebacks should be displayed or not. [default: yes]",
)
def build(**config_from_cli: Any) -> "NoReturn":
def build(**config_from_cli: Any) -> NoReturn:
"""Collect and execute tasks and report the results.

This is the default command of pytask which searches given paths or the current
Expand Down
110 changes: 23 additions & 87 deletions src/_pytask/capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
<https://github.com/pytest-dev/pytest/blob/master/src/_pytest/debugging.py>`_.

"""
from __future__ import annotations

import contextlib
import functools
import io
Expand All @@ -31,15 +33,11 @@
from tempfile import TemporaryFile
from typing import Any
from typing import AnyStr
from typing import Dict
from typing import Generator
from typing import Generic
from typing import Iterator
from typing import Optional
from typing import TextIO
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union

import click
from _pytask.config import hookimpl
Expand Down Expand Up @@ -92,9 +90,9 @@ def pytask_extend_command_line_interface(cli: click.Group) -> None:

@hookimpl
def pytask_parse_config(
config: Dict[str, Any],
config_from_cli: Dict[str, Any],
config_from_file: Dict[str, Any],
config: dict[str, Any],
config_from_cli: dict[str, Any],
config_from_file: dict[str, Any],
) -> None:
"""Parse configuration.

Expand Down Expand Up @@ -122,10 +120,8 @@ def pytask_parse_config(


@hookimpl
def pytask_post_parse(config: Dict[str, Any]) -> None:
def pytask_post_parse(config: dict[str, Any]) -> None:
"""Initialize the CaptureManager."""
if config["capture"] == "fd":
_py36_windowsconsoleio_workaround(sys.stdout)
_colorama_workaround()

pluginmanager = config["pm"]
Expand All @@ -136,7 +132,7 @@ def pytask_post_parse(config: Dict[str, Any]) -> None:
capman.suspend()


def _capture_callback(x: "Optional[_CaptureMethod]") -> "Optional[_CaptureMethod]":
def _capture_callback(x: _CaptureMethod | None) -> _CaptureMethod | None:
"""Validate the passed options for capturing output."""
if x in [None, "None", "none"]:
x = None
Expand All @@ -148,8 +144,8 @@ def _capture_callback(x: "Optional[_CaptureMethod]") -> "Optional[_CaptureMethod


def _show_capture_callback(
x: "Optional[_CaptureCallback]",
) -> "Optional[_CaptureCallback]":
x: _CaptureCallback | None,
) -> _CaptureCallback | None:
"""Validate the passed options for showing captured output."""
if x in [None, "None", "none"]:
x = None
Expand Down Expand Up @@ -181,66 +177,6 @@ def _colorama_workaround() -> None:
pass


def _py36_windowsconsoleio_workaround(stream: TextIO) -> None:
"""Workaround for Windows Unicode console handling on Python>=3.6.

Python 3.6 implemented Unicode console handling for Windows. This works by
reading/writing to the raw console handle using ``{Read,Write}ConsoleW``.

The problem is that we are going to ``dup2`` over the stdio file descriptors when
doing ``FDCapture`` and this will ``CloseHandle`` the handles used by Python to
write to the console. Though there is still some weirdness and the console handle
seems to only be closed randomly and not on the first call to ``CloseHandle``, or
maybe it gets reopened with the same handle value when we suspend capturing.

The workaround in this case will reopen stdio with a different fd which also means a
different handle by replicating the logic in
"Py_lifecycle.c:initstdio/create_stdio".

Parameters
---------
stream
In practice ``sys.stdout`` or ``sys.stderr``, but given here as parameter for
unit testing purposes.

See https://github.com/pytest-dev/py/issues/103.

"""
if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"):
return

# Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
if not hasattr(stream, "buffer"):
return

buffered = hasattr(stream.buffer, "raw")
# ``getattr`` hack since ``buffer`` might not have an attribute ``raw``.
raw_stdout = getattr(stream.buffer, "raw", stream.buffer)

# ``getattr`` hack since ``_WindowsConsoleIO`` is not defined in stubs.
windowsconsoleio = getattr(io, "_WindowsConsoleIO", None)
if windowsconsoleio is not None and not isinstance(raw_stdout, windowsconsoleio):
return

def _reopen_stdio(f: TextIO, mode: str) -> TextIO:
if not buffered and mode[0] == "w":
buffering = 0
else:
buffering = -1

return io.TextIOWrapper(
open(os.dup(f.fileno()), mode, buffering),
f.encoding,
f.errors,
f.newlines,
bool(f.line_buffering),
)

sys.stdin = _reopen_stdio(sys.stdin, "rb")
sys.stdout = _reopen_stdio(sys.stdout, "wb")
sys.stderr = _reopen_stdio(sys.stderr, "wb")


# IO Helpers.


Expand Down Expand Up @@ -292,7 +228,7 @@ def read(self, *_args: Any) -> None: # noqa: U101
readlines = read
__next__ = read

def __iter__(self) -> "DontReadFromInput":
def __iter__(self) -> DontReadFromInput:
return self

def fileno(self) -> int:
Expand All @@ -305,7 +241,7 @@ def close(self) -> None:
pass

@property
def buffer(self) -> "DontReadFromInput":
def buffer(self) -> DontReadFromInput:
return self


Expand Down Expand Up @@ -368,7 +304,7 @@ def __repr__(self) -> str:
self.tmpfile,
)

def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
assert (
self._state in states
), "cannot {} in state {!r}: expected one of {}".format(
Expand Down Expand Up @@ -463,7 +399,7 @@ def __init__(self, targetfd: int) -> None:
# Further complications are the need to support suspend() and the
# possibility of FD reuse (e.g. the tmpfile getting the very same target
# FD). The following approach is robust, I believe.
self.targetfd_invalid: Optional[int] = os.open(os.devnull, os.O_RDWR)
self.targetfd_invalid: int | None = os.open(os.devnull, os.O_RDWR)
os.dup2(self.targetfd_invalid, targetfd)
else:
self.targetfd_invalid = None
Expand Down Expand Up @@ -496,7 +432,7 @@ def __repr__(self) -> str:
self.tmpfile,
)

def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
assert (
self._state in states
), "cannot {} in state {!r}: expected one of {}".format(
Expand Down Expand Up @@ -614,8 +550,8 @@ def __getitem__(self, item: int) -> AnyStr:
return tuple(self)[item]

def _replace(
self, *, out: Optional[AnyStr] = None, err: Optional[AnyStr] = None
) -> "CaptureResult[AnyStr]":
self, *, out: AnyStr | None = None, err: AnyStr | None = None
) -> CaptureResult[AnyStr]:
return CaptureResult(
out=self.out if out is None else out, err=self.err if err is None else err
)
Expand Down Expand Up @@ -657,9 +593,9 @@ class MultiCapture(Generic[AnyStr]):

def __init__(
self,
in_: Optional[Union[FDCapture, SysCapture]],
out: Optional[Union[FDCapture, SysCapture]],
err: Optional[Union[FDCapture, SysCapture]],
in_: FDCapture | SysCapture | None,
out: FDCapture | SysCapture | None,
err: FDCapture | SysCapture | None,
) -> None:
self.in_ = in_
self.out = out
Expand All @@ -686,7 +622,7 @@ def start_capturing(self) -> None:
if self.err:
self.err.start()

def pop_outerr_to_orig(self) -> Tuple[AnyStr, AnyStr]:
def pop_outerr_to_orig(self) -> tuple[AnyStr, AnyStr]:
"""Pop current snapshot out/err capture and flush to orig streams."""
out, err = self.readouterr()
if out:
Expand Down Expand Up @@ -743,7 +679,7 @@ def readouterr(self) -> CaptureResult[AnyStr]:
return CaptureResult(out, err) # type: ignore


def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]:
"""Set up the MultiCapture class with the passed method.

For each valid method, the function instantiates the :class:`MultiCapture` class
Expand Down Expand Up @@ -779,9 +715,9 @@ class CaptureManager:

"""

def __init__(self, method: "_CaptureMethod") -> None:
def __init__(self, method: _CaptureMethod) -> None:
self._method = method
self._capturing: Optional[MultiCapture[str]] = None
self._capturing: MultiCapture[str] | None = None

def __repr__(self) -> str:
return ("<CaptureManager _method={!r} _capturing={!r}>").format(
Expand Down
Loading