diff --git a/.pylintrc b/.pylintrc index c502ae9..44ea991 100644 --- a/.pylintrc +++ b/.pylintrc @@ -137,7 +137,6 @@ enable=old-style-class, old-ne-operator, old-octal-literal, import-star-module-level, - c-extension-no-member, lowercase-l-suffix, deprecated-module, invalid-encoded-data, diff --git a/.travis.yml b/.travis.yml index f7524ad..d76f8ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -99,13 +99,13 @@ jobs: - pip install --upgrade pydocstyle script: - pydocstyle exec_helpers - - <<: *code_style_check - name: "Black formatting" - install: - - *upgrade_python_toolset - - pip install --upgrade black - script: - - black --check exec_helpers +# - <<: *code_style_check +# name: "Black formatting" +# install: +# - *upgrade_python_toolset +# - pip install --upgrade black +# script: +# - black --check exec_helpers - stage: test <<: *python35 diff --git a/MANIFEST.in b/MANIFEST.in index 69fb558..8c19e94 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,9 +2,10 @@ include *.rst LICENSE requirements.txt global-exclude *.c exclude Makefile prune tools -exclude .travis.yml appveyor.yml +exclude .travis.yml appveyor.yml azure-pipelines.yml exclude tox.ini pytest.ini .coveragerc prune test prune .github +prune .azure_pipelines prune docs exclude CODEOWNERS CODE_OF_CONDUCT.md _config.yml diff --git a/README.rst b/README.rst index 2aee40c..10946bb 100644 --- a/README.rst +++ b/README.rst @@ -149,6 +149,8 @@ This methods are almost the same for `SSHCleint` and `Subprocess`, except specif error_info=None, # type: typing.Optional[str] expected=None, # type: typing.Optional[typing.Iterable[int]] raise_on_err=True, # type: bool + # Keyword only: + exception_class=CalledProcessError, # typing.Type[CalledProcessError] **kwargs ) @@ -160,6 +162,9 @@ This methods are almost the same for `SSHCleint` and `Subprocess`, except specif timeout=1 * 60 * 60, # type: type: typing.Union[int, float, None] error_info=None, # type: typing.Optional[str] raise_on_err=True, # type: bool + # Keyword only: + expected=None, # typing.Optional[typing.Iterable[typing.Union[int, ExitCodes]]] + exception_class=CalledProcessError, # typing.Type[CalledProcessError] ) If no STDOUT or STDERR required, it is possible to disable this FIFO pipes via `**kwargs` with flags `open_stdout=False` and `open_stderr=False`. @@ -208,7 +213,7 @@ SSHClient specific ------------------ SSHClient commands support get_pty flag, which enables PTY open on remote side. -PTY width and height can be set via kwargs, dimensions in pixels are always 0x0. +PTY width and height can be set via keyword arguments, dimensions in pixels are always 0x0. Possible to call commands in parallel on multiple hosts if it's not produce huge output: @@ -219,7 +224,9 @@ Possible to call commands in parallel on multiple hosts if it's not produce huge command, # type: str timeout=1 * 60 * 60, # type: type: typing.Union[int, float, None] expected=None, # type: typing.Optional[typing.Iterable[int]] - raise_on_err=True # type: bool + raise_on_err=True, # type: bool + # Keyword only: + exception_class=ParallelCallProcessError # typing.Type[ParallelCallProcessError] ) results # type: typing.Dict[typing.Tuple[str, int], exec_result.ExecResult] @@ -237,7 +244,10 @@ For execute through SSH host can be used `execute_through_host` method: target_port=22, # type: int timeout=1 * 60 * 60, # type: type: typing.Union[int, float, None] verbose=False, # type: bool + # Keyword only: get_pty=False, # type: bool + width=80, # type: int + height=24 # type: int ) Where hostname is a target hostname, auth is an alternate credentials for target host. @@ -312,7 +322,7 @@ Additional (non-standard) helpers: Subprocess specific ------------------- -Kwargs set properties: +Keyword arguments: - cwd - working directory. - env - environment variables dict. diff --git a/doc/source/SSHClient.rst b/doc/source/SSHClient.rst index c04a8fc..886f7f6 100644 --- a/doc/source/SSHClient.rst +++ b/doc/source/SSHClient.rst @@ -121,7 +121,7 @@ API: SSHClient and SSHAuth. .. Note:: Enter and exit ssh context manager is produced as well. .. versionadded:: 1.2.1 - .. py:method:: execute_async(command, stdin=None, open_stdout=True, open_stderr=True, verbose=False, log_mask_re=None, **kwargs) + .. py:method:: execute_async(command, stdin=None, open_stdout=True, open_stderr=True, verbose=False, log_mask_re=None, *, get_pty=False, width=80, height=24, **kwargs) Execute command in async mode and return channel with IO objects. @@ -138,12 +138,19 @@ API: SSHClient and SSHAuth. :param log_mask_re: regex lookup rule to mask command for logger. all MATCHED groups will be replaced by '<*masked*>' :type log_mask_re: typing.Optional[str] + :param get_pty: Get PTY for connection + :type get_pty: bool + :param width: PTY width + :type width: int + :param height: PTY height + :type height: int :rtype: SshExecuteAsyncResult .. versionchanged:: 1.2.0 open_stdout and open_stderr flags .. versionchanged:: 1.2.0 stdin data .. versionchanged:: 1.2.0 get_pty moved to `**kwargs` .. versionchanged:: 2.1.0 Use typed NamedTuple as result + .. versionchanged:: 3.2.0 Expose pty options as optional keyword-only arguments .. py:method:: execute(command, verbose=False, timeout=1*60*60, **kwargs) @@ -160,7 +167,7 @@ API: SSHClient and SSHAuth. .. versionchanged:: 1.2.0 default timeout 1 hour - .. py:method:: check_call(command, verbose=False, timeout=1*60*60, error_info=None, expected=None, raise_on_err=True, **kwargs) + .. py:method:: check_call(command, verbose=False, timeout=1*60*60, error_info=None, expected=None, raise_on_err=True, *, exception_class=CalledProcessError, **kwargs) Execute command and check for return code. @@ -176,13 +183,16 @@ API: SSHClient and SSHAuth. :type expected: ``typing.Optional[typing.Iterable[int]]`` :param raise_on_err: Raise exception on unexpected return code :type raise_on_err: ``bool`` + :param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory. + :type exception_class: typing.Type[CalledProcessError] :rtype: ExecResult :raises ExecHelperTimeoutError: Timeout exceeded :raises CalledProcessError: Unexpected exit code .. versionchanged:: 1.2.0 default timeout 1 hour + .. versionchanged:: 3.2.0 Exception class can be substituted - .. py:method:: check_stderr(command, verbose=False, timeout=1*60*60, error_info=None, raise_on_err=True, **kwargs) + .. py:method:: check_stderr(command, verbose=False, timeout=1*60*60, error_info=None, raise_on_err=True, *, expected=None, exception_class=CalledProcessError, **kwargs) Execute command expecting return code 0 and empty STDERR. @@ -196,14 +206,18 @@ API: SSHClient and SSHAuth. :type error_info: ``typing.Optional[str]`` :param raise_on_err: Raise exception on unexpected return code :type raise_on_err: ``bool`` + :param expected: expected return codes (0 by default) + :type expected: typing.Optional[typing.Iterable[typing.Union[int, ExitCodes]]] + :param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory. + :type exception_class: typing.Type[CalledProcessError] :rtype: ExecResult :raises ExecHelperTimeoutError: Timeout exceeded :raises CalledProcessError: Unexpected exit code or stderr presents - .. note:: expected return codes can be overridden via kwargs. .. versionchanged:: 1.2.0 default timeout 1 hour + .. versionchanged:: 3.2.0 Exception class can be substituted - .. py:method:: execute_through_host(hostname, command, auth=None, target_port=22, verbose=False, timeout=1*60*60, get_pty=False, **kwargs) + .. py:method:: execute_through_host(hostname, command, auth=None, target_port=22, verbose=False, timeout=1*60*60, *, get_pty=False, width=80, height=24, **kwargs) Execute command on remote host through currently connected host. @@ -221,12 +235,18 @@ API: SSHClient and SSHAuth. :type timeout: ``typing.Union[int, float, None]`` :param get_pty: open PTY on target machine :type get_pty: ``bool`` + :param width: PTY width + :type width: int + :param height: PTY height + :type height: int :rtype: ExecResult :raises ExecHelperTimeoutError: Timeout exceeded .. versionchanged:: 1.2.0 default timeout 1 hour + .. versionchanged:: 3.2.0 Expose pty options as optional keyword-only arguments + .. versionchanged:: 3.2.0 Exception class can be substituted - .. py:classmethod:: execute_together(remotes, command, timeout=1*60*60, expected=None, raise_on_err=True, **kwargs) + .. py:classmethod:: execute_together(remotes, command, timeout=1*60*60, expected=None, raise_on_err=True, *, exception_class=ParallelCallProcessError, **kwargs) Execute command on multiple remotes in async mode. @@ -240,12 +260,15 @@ API: SSHClient and SSHAuth. :type expected: ``typing.Optional[typing.Iterable[]]`` :param raise_on_err: Raise exception on unexpected return code :type raise_on_err: ``bool`` + :param exception_class: Exception to raise on error. Mandatory subclass of ParallelCallProcessError + :type exception_class: typing.Type[ParallelCallProcessError] :return: dictionary {(hostname, port): result} :rtype: typing.Dict[typing.Tuple[str, int], ExecResult] :raises ParallelCallProcessError: Unexpected any code at lest on one target :raises ParallelCallExceptions: At lest one exception raised during execution (including timeout) .. versionchanged:: 1.2.0 default timeout 1 hour + .. versionchanged:: 3.2.0 Exception class can be substituted .. py:method:: open(path, mode='r') diff --git a/doc/source/Subprocess.rst b/doc/source/Subprocess.rst index c28506f..d10d135 100644 --- a/doc/source/Subprocess.rst +++ b/doc/source/Subprocess.rst @@ -8,15 +8,18 @@ API: Subprocess .. py:class:: Subprocess() - .. py:method:: __init__(logger, log_mask_re=None) + .. py:method:: __init__(logger, log_mask_re=None, *, logger=logging.getLogger("exec_helpers.subprocess_runner")) ExecHelper global API. :param log_mask_re: regex lookup rule to mask command for logger. all MATCHED groups will be replaced by '<*masked*>' :type log_mask_re: typing.Optional[str] + :param logger: logger instance to use + :type logger: logging.Logger .. versionchanged:: 1.2.0 log_mask_re regex rule for masking cmd .. versionchanged:: 3.1.0 Not singleton anymore. Only lock is shared between all instances. + .. versionchanged:: 3.2.0 Logger can be enforced. .. py:attribute:: log_mask_re @@ -40,7 +43,7 @@ API: Subprocess .. versionchanged:: 1.1.0 release lock on exit - .. py:method:: execute_async(command, stdin=None, open_stdout=True, open_stderr=True, verbose=False, log_mask_re=None, **kwargs) + .. py:method:: execute_async(command, stdin=None, open_stdout=True, open_stderr=True, verbose=False, log_mask_re=None, *, cwd=None, env=None, **kwargs) Execute command in async mode and return Popen with IO objects. @@ -57,11 +60,16 @@ API: Subprocess :param log_mask_re: regex lookup rule to mask command for logger. all MATCHED groups will be replaced by '<*masked*>' :type log_mask_re: ``typing.Optional[str]`` + :param cwd: Sets the current directory before the child is executed. + :type cwd: typing.Optional[typing.Union[str, bytes]] + :param env: Defines the environment variables for the new process. + :type env: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]] :rtype: SubprocessExecuteAsyncResult :raises OSError: impossible to process STDIN .. versionadded:: 1.2.0 .. versionchanged:: 2.1.0 Use typed NamedTuple as result + .. versionchanged:: 3.2.0 Expose cwd and env as optional keyword-only arguments .. py:method:: execute(command, verbose=False, timeout=1*60*60, **kwargs) @@ -82,7 +90,7 @@ API: Subprocess .. versionchanged:: 1.2.0 default timeout 1 hour .. versionchanged:: 1.2.0 stdin data - .. py:method:: check_call(command, verbose=False, timeout=1*60*60, error_info=None, expected=None, raise_on_err=True, **kwargs) + .. py:method:: check_call(command, verbose=False, timeout=1*60*60, error_info=None, expected=None, raise_on_err=True, *, exception_class=CalledProcessError, **kwargs) Execute command and check for return code. @@ -98,14 +106,17 @@ API: Subprocess :type expected: ``typing.Optional[typing.Iterable[int]]`` :param raise_on_err: Raise exception on unexpected return code :type raise_on_err: ``bool`` + :param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory. + :type exception_class: typing.Type[CalledProcessError] :rtype: ExecResult :raises ExecHelperTimeoutError: Timeout exceeded :raises CalledProcessError: Unexpected exit code .. versionchanged:: 1.1.0 make method .. versionchanged:: 1.2.0 default timeout 1 hour + .. versionchanged:: 3.2.0 Exception class can be substituted - .. py:method:: check_stderr(command, verbose=False, timeout=1*60*60, error_info=None, raise_on_err=True, **kwargs) + .. py:method:: check_stderr(command, verbose=False, timeout=1*60*60, error_info=None, raise_on_err=True, *, expected=None, exception_class=CalledProcessError, **kwargs) Execute command expecting return code 0 and empty STDERR. @@ -119,14 +130,17 @@ API: Subprocess :type error_info: ``typing.Optional[str]`` :param raise_on_err: Raise exception on unexpected return code :type raise_on_err: ``bool`` + :param expected: expected return codes (0 by default) + :type expected: typing.Optional[typing.Iterable[typing.Union[int, ExitCodes]]] + :param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory. + :type exception_class: typing.Type[CalledProcessError] :rtype: ExecResult :raises ExecHelperTimeoutError: Timeout exceeded :raises CalledProcessError: Unexpected exit code or stderr presents - .. note:: expected return codes can be overridden via kwargs. - .. versionchanged:: 1.1.0 make method .. versionchanged:: 1.2.0 default timeout 1 hour + .. versionchanged:: 3.2.0 Exception class can be substituted .. py:class:: SubprocessExecuteAsyncResult diff --git a/doc/source/exceptions.rst b/doc/source/exceptions.rst index 3568efd..dffe30a 100644 --- a/doc/source/exceptions.rst +++ b/doc/source/exceptions.rst @@ -100,16 +100,14 @@ API: exceptions ``str`` stdout string or brief string -.. py:exception:: ParallelCallExceptions(ExecCalledProcessError) +.. py:exception:: ParallelCallProcessError(ExecCalledProcessError) - Exception raised during parallel call as result of exceptions. + Exception during parallel execution. - .. py:method:: __init__(command, exceptions, errors, results, expected=None, ) + .. py:method:: __init__(command, errors, results, expected=None, ) :param command: command :type command: ``str`` - :param exceptions: Exception on connections - :type exceptions: ``typing.Dict[typing.Tuple[str, int], Exception]`` :param errors: results with errors :type errors: typing.Dict[typing.Tuple[str, int], ExecResult] :param results: all results @@ -124,11 +122,6 @@ API: exceptions ``str`` command - .. py:attribute:: exceptions - - ``typing.Dict[typing.Tuple[str, int], Exception]`` - Exception on connections - .. py:attribute:: errors results with errors @@ -147,14 +140,16 @@ API: exceptions :rtype: typing.List[typing.Union[int, ExitCodes]] -.. py:exception:: ParallelCallProcessError(ExecCalledProcessError) +.. py:exception:: ParallelCallExceptions(ParallelCallProcessError) - Exception during parallel execution. + Exception raised during parallel call as result of exceptions. - .. py:method:: __init__(command, errors, results, expected=None, ) + .. py:method:: __init__(command, exceptions, errors, results, expected=None, ) :param command: command :type command: ``str`` + :param exceptions: Exception on connections + :type exceptions: ``typing.Dict[typing.Tuple[str, int], Exception]`` :param errors: results with errors :type errors: typing.Dict[typing.Tuple[str, int], ExecResult] :param results: all results @@ -169,6 +164,11 @@ API: exceptions ``str`` command + .. py:attribute:: exceptions + + ``typing.Dict[typing.Tuple[str, int], Exception]`` + Exception on connections + .. py:attribute:: errors results with errors diff --git a/exec_helpers/__init__.py b/exec_helpers/__init__.py index 29af91b..5e81ad8 100644 --- a/exec_helpers/__init__.py +++ b/exec_helpers/__init__.py @@ -58,6 +58,7 @@ except pkg_resources.DistributionNotFound: # package is not installed, try to get from SCM try: + # noinspection PyPackageRequirements,PyUnresolvedReferences import setuptools_scm # type: ignore __version__ = setuptools_scm.get_version() diff --git a/exec_helpers/_ssh_client_base.py b/exec_helpers/_ssh_client_base.py index cd3b16c..a39f390 100644 --- a/exec_helpers/_ssh_client_base.py +++ b/exec_helpers/_ssh_client_base.py @@ -539,7 +539,7 @@ def keepalive(self, enforce: bool = True) -> "typing.ContextManager": """ return self.__get_keepalive(ssh=self, enforce=enforce) - def execute_async( + def execute_async( # pylint: disable=arguments-differ self, command: str, stdin: typing.Union[bytes, str, bytearray, None] = None, @@ -547,6 +547,10 @@ def execute_async( open_stderr: bool = True, verbose: bool = False, log_mask_re: typing.Optional[str] = None, + *, + get_pty: bool = False, + width: int = 80, + height: int = 24, **kwargs: typing.Any ) -> SshExecuteAsyncResult: """Execute command in async mode and return channel with IO objects. @@ -564,6 +568,12 @@ def execute_async( :param log_mask_re: regex lookup rule to mask command for logger. all MATCHED groups will be replaced by '<*masked*>' :type log_mask_re: typing.Optional[str] + :param get_pty: Get PTY for connection + :type get_pty: bool + :param width: PTY width + :type width: int + :param height: PTY height + :type height: int :param kwargs: additional parameters for call. :type kwargs: typing.Any :return: Tuple with control interface and file-like objects for STDIN/STDERR/STDOUT @@ -581,6 +591,7 @@ def execute_async( .. versionchanged:: 1.2.0 stdin data .. versionchanged:: 1.2.0 get_pty moved to `**kwargs` .. versionchanged:: 2.1.0 Use typed NamedTuple as result + .. versionchanged:: 3.2.0 Expose pty options as optional keyword-only arguments """ cmd_for_log = self._mask_command(cmd=command, log_mask_re=log_mask_re) @@ -590,15 +601,9 @@ def execute_async( chan = self._ssh.get_transport().open_session() - if kwargs.get("get_pty", False): + if get_pty: # Open PTY - chan.get_pty( - term="vt100", - width=kwargs.get("width", 80), - height=kwargs.get("height", 24), - width_pixels=0, - height_pixels=0, - ) + chan.get_pty(term="vt100", width=width, height=height, width_pixels=0, height_pixels=0) _stdin = chan.makefile("wb") # type: paramiko.ChannelFile stdout = chan.makefile("rb") if open_stdout else None @@ -634,6 +639,8 @@ def _exec_command( # type: ignore timeout: typing.Union[int, float, None], verbose: bool = False, log_mask_re: typing.Optional[str] = None, + *, + stdin: typing.Union[bytes, str, bytearray, None] = None, **kwargs: typing.Any ) -> exec_result.ExecResult: """Get exit status from channel with timeout. @@ -649,6 +656,8 @@ def _exec_command( # type: ignore :param log_mask_re: regex lookup rule to mask command for logger. all MATCHED groups will be replaced by '<*masked*>' :type log_mask_re: typing.Optional[str] + :param stdin: pass STDIN text to the process + :type stdin: typing.Union[bytes, str, bytearray, None] :param kwargs: additional parameters for call. :type kwargs: typing.Any :return: Execution result @@ -681,7 +690,7 @@ def poll_pipes() -> None: cmd_for_log = self._mask_command(cmd=command, log_mask_re=log_mask_re) # Store command with hidden data - result = exec_result.ExecResult(cmd=cmd_for_log, stdin=kwargs.get("stdin")) + result = exec_result.ExecResult(cmd=cmd_for_log, stdin=stdin) # pylint: disable=assignment-from-no-return # noinspection PyNoneFunctionAssignment @@ -711,7 +720,10 @@ def execute_through_host( target_port: int = 22, verbose: bool = False, timeout: typing.Union[int, float, None] = constants.DEFAULT_TIMEOUT, + *, get_pty: bool = False, + width: int = 80, + height: int = 24, **kwargs: typing.Any ) -> exec_result.ExecResult: """Execute command on remote host through currently connected host. @@ -730,6 +742,10 @@ def execute_through_host( :type timeout: typing.Union[int, float, None] :param get_pty: open PTY on target machine :type get_pty: bool + :param width: PTY width + :type width: int + :param height: PTY height + :type height: int :param kwargs: additional parameters for call. :type kwargs: typing.Any :return: Execution result @@ -738,6 +754,7 @@ def execute_through_host( .. versionchanged:: 1.2.0 default timeout 1 hour .. versionchanged:: 1.2.0 log_mask_re regex rule for masking cmd + .. versionchanged:: 3.2.0 Expose pty options as optional keyword-only arguments """ cmd_for_log = self._mask_command(cmd=command, log_mask_re=kwargs.get("log_mask_re", None)) self.logger.log( # type: ignore @@ -759,13 +776,7 @@ def execute_through_host( channel = transport.open_session() if get_pty: # Open PTY - channel.get_pty( - term="vt100", - width=kwargs.get("width", 80), - height=kwargs.get("height", 24), - width_pixels=0, - height_pixels=0, - ) + channel.get_pty(term="vt100", width=width, height=height, width_pixels=0, height_pixels=0) # Make proxy objects for read _stdin = channel.makefile("wb") # type: paramiko.ChannelFile @@ -808,6 +819,8 @@ def execute_together( timeout: typing.Union[int, float, None] = constants.DEFAULT_TIMEOUT, expected: typing.Optional[typing.Iterable[int]] = None, raise_on_err: bool = True, + *, + exception_class: "typing.Type[exceptions.ParallelCallProcessError]" = exceptions.ParallelCallProcessError, **kwargs: typing.Any ) -> typing.Dict[typing.Tuple[str, int], exec_result.ExecResult]: """Execute command on multiple remotes in async mode. @@ -822,6 +835,8 @@ def execute_together( :type expected: typing.Optional[typing.Iterable[]] :param raise_on_err: Raise exception on unexpected return code :type raise_on_err: bool + :param exception_class: Exception to raise on error. Mandatory subclass of exceptions.ParallelCallProcessError + :type exception_class: typing.Type[exceptions.ParallelCallProcessError] :param kwargs: additional parameters for execute_async call. :type kwargs: typing.Any :return: dictionary {(hostname, port): result} @@ -831,6 +846,7 @@ def execute_together( .. versionchanged:: 1.2.0 default timeout 1 hour .. versionchanged:: 1.2.0 log_mask_re regex rule for masking cmd + .. versionchanged:: 3.2.0 Exception class can be substituted """ @threaded.threadpooled @@ -880,7 +896,7 @@ def get_result(remote: "SSHClientBase") -> exec_result.ExecResult: if raised_exceptions: # always raise raise exceptions.ParallelCallExceptions(command, raised_exceptions, errors, results, expected=expected) if errors and raise_on_err: - raise exceptions.ParallelCallProcessError(command, errors, results, expected=expected) + raise exception_class(command, errors, results, expected=expected) return results def open(self, path: str, mode: str = "r") -> paramiko.SFTPFile: diff --git a/exec_helpers/api.py b/exec_helpers/api.py index 7eab339..f98f28d 100644 --- a/exec_helpers/api.py +++ b/exec_helpers/api.py @@ -188,6 +188,8 @@ def _exec_command( timeout: typing.Union[int, float, None], verbose: bool = False, log_mask_re: typing.Optional[str] = None, + *, + stdin: typing.Union[bytes, str, bytearray, None] = None, **kwargs: typing.Any ) -> exec_result.ExecResult: """Get exit status from channel with timeout. @@ -203,6 +205,8 @@ def _exec_command( :param log_mask_re: regex lookup rule to mask command for logger. all MATCHED groups will be replaced by '<*masked*>' :type log_mask_re: typing.Optional[str] + :param stdin: pass STDIN text to the process + :type stdin: typing.Union[bytes, str, bytearray, None] :param kwargs: additional parameters for call. :type kwargs: typing.Any :return: Execution result @@ -254,6 +258,8 @@ def check_call( error_info: typing.Optional[str] = None, expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] = None, raise_on_err: bool = True, + *, + exception_class: "typing.Type[exceptions.CalledProcessError]" = exceptions.CalledProcessError, **kwargs: typing.Any ) -> exec_result.ExecResult: """Execute command and check for return code. @@ -270,6 +276,8 @@ def check_call( :type expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] :param raise_on_err: Raise exception on unexpected return code :type raise_on_err: bool + :param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory. + :type exception_class: typing.Type[exceptions.CalledProcessError] :param kwargs: additional parameters for call. :type kwargs: typing.Any :return: Execution result @@ -278,6 +286,7 @@ def check_call( :raises CalledProcessError: Unexpected exit code .. versionchanged:: 1.2.0 default timeout 1 hour + .. versionchanged:: 3.2.0 Exception class can be substituted """ expected_codes = proc_enums.exit_codes_to_enums(expected) ret = self.execute(command, verbose, timeout, **kwargs) @@ -290,7 +299,7 @@ def check_call( ) self.logger.error(msg=message) if raise_on_err: - raise exceptions.CalledProcessError(result=ret, expected=expected_codes) + raise exception_class(result=ret, expected=expected_codes) return ret def check_stderr( @@ -300,6 +309,9 @@ def check_stderr( timeout: typing.Union[int, float, None] = constants.DEFAULT_TIMEOUT, error_info: typing.Optional[str] = None, raise_on_err: bool = True, + *, + expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] = None, + exception_class: "typing.Type[exceptions.CalledProcessError]" = exceptions.CalledProcessError, **kwargs: typing.Any ) -> exec_result.ExecResult: """Execute command expecting return code 0 and empty STDERR. @@ -314,6 +326,10 @@ def check_stderr( :type error_info: typing.Optional[str] :param raise_on_err: Raise exception on unexpected return code :type raise_on_err: bool + :param expected: expected return codes (0 by default) + :type expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] + :param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory. + :type exception_class: typing.Type[exceptions.CalledProcessError] :param kwargs: additional parameters for call. :type kwargs: typing.Any :return: Execution result @@ -322,9 +338,17 @@ def check_stderr( :raises CalledProcessError: Unexpected exit code or stderr presents .. versionchanged:: 1.2.0 default timeout 1 hour + .. versionchanged:: 3.2.0 Exception class can be substituted """ ret = self.check_call( - command, verbose, timeout=timeout, error_info=error_info, raise_on_err=raise_on_err, **kwargs + command, + verbose, + timeout=timeout, + error_info=error_info, + raise_on_err=raise_on_err, + expected=expected, + exception_class=exception_class, + **kwargs ) if ret.stderr: message = ( @@ -333,7 +357,7 @@ def check_stderr( ) self.logger.error(msg=message) if raise_on_err: - raise exceptions.CalledProcessError(result=ret, expected=kwargs.get("expected")) + raise exception_class(result=ret, expected=expected) return ret @staticmethod diff --git a/exec_helpers/async_api/api.py b/exec_helpers/async_api/api.py index 5920c9f..135ab44 100644 --- a/exec_helpers/async_api/api.py +++ b/exec_helpers/async_api/api.py @@ -67,6 +67,8 @@ async def _exec_command( # type: ignore timeout: typing.Union[int, float, None], verbose: bool = False, log_mask_re: typing.Optional[str] = None, + *, + stdin: typing.Union[bytes, str, bytearray, None] = None, **kwargs: typing.Any ) -> exec_result.ExecResult: """Get exit status from channel with timeout. @@ -86,6 +88,8 @@ async def _exec_command( # type: ignore :param log_mask_re: regex lookup rule to mask command for logger. all MATCHED groups will be replaced by '<*masked*>' :type log_mask_re: typing.Optional[str] + :param stdin: pass STDIN text to the process + :type stdin: typing.Union[bytes, str, bytearray, None] :param kwargs: additional parameters for call. :type kwargs: typing.Any :return: Execution result @@ -175,6 +179,8 @@ async def check_call( # type: ignore error_info: typing.Optional[str] = None, expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] = None, raise_on_err: bool = True, + *, + exception_class: "typing.Type[exceptions.CalledProcessError]" = exceptions.CalledProcessError, **kwargs: typing.Any ) -> exec_result.ExecResult: """Execute command and check for return code. @@ -191,6 +197,8 @@ async def check_call( # type: ignore :type expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] :param raise_on_err: Raise exception on unexpected return code :type raise_on_err: bool + :param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory. + :type exception_class: typing.Type[exceptions.CalledProcessError] :param kwargs: additional parameters for call. :type kwargs: typing.Any :return: Execution result @@ -209,7 +217,7 @@ async def check_call( # type: ignore ) self.logger.error(msg=message) if raise_on_err: - raise exceptions.CalledProcessError(result=ret, expected=expected_codes) + raise exception_class(result=ret, expected=expected_codes) return ret async def check_stderr( # type: ignore @@ -219,6 +227,9 @@ async def check_stderr( # type: ignore timeout: typing.Union[int, float, None] = constants.DEFAULT_TIMEOUT, error_info: typing.Optional[str] = None, raise_on_err: bool = True, + *, + expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] = None, + exception_class: "typing.Type[exceptions.CalledProcessError]" = exceptions.CalledProcessError, **kwargs: typing.Any ) -> exec_result.ExecResult: """Execute command expecting return code 0 and empty STDERR. @@ -233,6 +244,10 @@ async def check_stderr( # type: ignore :type error_info: typing.Optional[str] :param raise_on_err: Raise exception on unexpected return code :type raise_on_err: bool + :param expected: expected return codes (0 by default) + :type expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] + :param exception_class: Exception class for errors. Subclass of CalledProcessError is mandatory. + :type exception_class: typing.Type[exceptions.CalledProcessError] :param kwargs: additional parameters for call. :type kwargs: typing.Any :return: Execution result @@ -241,7 +256,14 @@ async def check_stderr( # type: ignore :raises CalledProcessError: Unexpected exit code or stderr presents """ ret = await self.check_call( - command, verbose, timeout=timeout, error_info=error_info, raise_on_err=raise_on_err, **kwargs + command, + verbose, + timeout=timeout, + error_info=error_info, + raise_on_err=raise_on_err, + expected=expected, + exception_class=exception_class, + **kwargs ) if ret.stderr: message = ( @@ -250,5 +272,5 @@ async def check_stderr( # type: ignore ) self.logger.error(msg=message) if raise_on_err: - raise exceptions.CalledProcessError(result=ret, expected=kwargs.get("expected")) + raise exception_class(result=ret, expected=expected) return ret diff --git a/exec_helpers/async_api/subprocess_runner.py b/exec_helpers/async_api/subprocess_runner.py index 89cf739..506fd8e 100644 --- a/exec_helpers/async_api/subprocess_runner.py +++ b/exec_helpers/async_api/subprocess_runner.py @@ -32,8 +32,6 @@ from exec_helpers import _log_templates from exec_helpers import _subprocess_helpers -logger = logging.getLogger(__name__) # type: logging.Logger - # noinspection PyTypeHints,PyTypeChecker class SubprocessExecuteAsyncResult(subprocess_runner.SubprocessExecuteAsyncResult): @@ -68,16 +66,19 @@ class Subprocess(api.ExecHelper, metaclass=metaclasses.SingleLock): __slots__ = () - def __init__(self, logger: logging.Logger = logger, log_mask_re: typing.Optional[str] = None) -> None: + def __init__( + self, log_mask_re: typing.Optional[str] = None, *, logger: logging.Logger = logging.getLogger(__name__) + ) -> None: """Subprocess helper with timeouts and lock-free FIFO. - :param logger: logger instance to use - :type logger: logging.Logger :param log_mask_re: regex lookup rule to mask command for logger. all MATCHED groups will be replaced by '<*masked*>' :type log_mask_re: typing.Optional[str] + :param logger: logger instance to use + :type logger: logging.Logger .. versionchanged:: 3.1.0 Not singleton anymore. Only lock is shared between all instances. + .. versionchanged:: 3.2.0 Logger can be enforced. """ super(Subprocess, self).__init__(logger=logger, log_mask_re=log_mask_re) @@ -88,6 +89,8 @@ async def _exec_command( # type: ignore timeout: typing.Union[int, float, None], verbose: bool = False, log_mask_re: typing.Optional[str] = None, + *, + stdin: typing.Union[bytes, str, bytearray, None] = None, **kwargs: typing.Any ) -> exec_result.ExecResult: """Get exit status from channel with timeout. @@ -103,6 +106,8 @@ async def _exec_command( # type: ignore :param log_mask_re: regex lookup rule to mask command for logger. all MATCHED groups will be replaced by '<*masked*>' :type log_mask_re: typing.Optional[str] + :param stdin: pass STDIN text to the process + :type stdin: typing.Union[bytes, str, bytearray, None] :param kwargs: additional parameters for call. :type kwargs: typing.Any :return: Execution result @@ -113,16 +118,16 @@ async def _exec_command( # type: ignore async def poll_stdout() -> None: """Sync stdout poll.""" - await result.read_stdout(src=async_result.stdout, log=logger, verbose=verbose) + await result.read_stdout(src=async_result.stdout, log=self.logger, verbose=verbose) async def poll_stderr() -> None: """Sync stderr poll.""" - await result.read_stderr(src=async_result.stderr, log=logger, verbose=verbose) + await result.read_stderr(src=async_result.stderr, log=self.logger, verbose=verbose) # Store command with hidden data cmd_for_log = self._mask_command(cmd=command, log_mask_re=log_mask_re) - result = exec_result.ExecResult(cmd=cmd_for_log, stdin=kwargs.get("stdin")) + result = exec_result.ExecResult(cmd=cmd_for_log, stdin=stdin) stdout_task = asyncio.ensure_future(poll_stdout()) stderr_task = asyncio.ensure_future(poll_stderr()) @@ -140,7 +145,7 @@ async def poll_stderr() -> None: # Wait for 1 ms: check close exit_code = await asyncio.wait_for(async_result.interface.wait(), timeout=0.001) if exit_code is not None: # Nothing to kill - logger.warning( + self.logger.warning( "{!s} has been completed just after timeout: please validate timeout.".format(command) ) result.exit_code = exit_code @@ -151,9 +156,10 @@ async def poll_stderr() -> None: stderr_task.cancel() wait_err_msg = _log_templates.CMD_WAIT_ERROR.format(result=result, timeout=timeout) - logger.debug(wait_err_msg) + self.logger.debug(wait_err_msg) raise exceptions.ExecHelperTimeoutError(result=result, timeout=timeout) # type: ignore + # pylint: disable=arguments-differ async def execute_async( # type: ignore self, command: str, @@ -162,6 +168,11 @@ async def execute_async( # type: ignore open_stderr: bool = True, verbose: bool = False, log_mask_re: typing.Optional[str] = None, + *, + cwd: typing.Optional[typing.Union[str, bytes]] = None, + env: typing.Optional[ + typing.Union[typing.Mapping[bytes, typing.Union[bytes, str]], typing.Mapping[str, typing.Union[bytes, str]]] + ] = None, **kwargs: typing.Any ) -> SubprocessExecuteAsyncResult: """Execute command in async mode and return Popen with IO objects. @@ -179,6 +190,10 @@ async def execute_async( # type: ignore :param log_mask_re: regex lookup rule to mask command for logger. all MATCHED groups will be replaced by '<*masked*>' :type log_mask_re: typing.Optional[str] + :param cwd: Sets the current directory before the child is executed. + :type cwd: typing.Optional[typing.Union[str, bytes]] + :param env: Defines the environment variables for the new process. + :type env: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]] :param kwargs: additional parameters for call. :type kwargs: typing.Any :return: Tuple with control interface and file-like objects for STDIN/STDERR/STDOUT @@ -204,8 +219,8 @@ async def execute_async( # type: ignore stdout=asyncio.subprocess.PIPE if open_stdout else asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.PIPE if open_stderr else asyncio.subprocess.DEVNULL, stdin=asyncio.subprocess.PIPE, - cwd=kwargs.get("cwd", None), - env=kwargs.get("env", None), + cwd=cwd, + env=env, universal_newlines=False, **_subprocess_helpers.subprocess_kw ) @@ -244,3 +259,5 @@ async def execute_async( # type: ignore process_stdin = None return SubprocessExecuteAsyncResult(process, process_stdin, process.stderr, process.stdout) + + # pylint: enable=arguments-differ diff --git a/exec_helpers/exceptions.py b/exec_helpers/exceptions.py index 1190744..c28d9d2 100644 --- a/exec_helpers/exceptions.py +++ b/exec_helpers/exceptions.py @@ -67,10 +67,10 @@ def __init__(self, result: "exec_result.ExecResult", timeout: typing.Union[int, :param timeout: timeout for command :type timeout: typing.Union[int, float] """ - self.result = result - self.timeout = timeout message = _log_templates.CMD_WAIT_ERROR.format(result=result, timeout=timeout) super(ExecHelperTimeoutError, self).__init__(message) + self.result = result + self.timeout = timeout @property def cmd(self) -> str: @@ -96,14 +96,14 @@ class CalledProcessError(ExecCalledProcessError): def __init__( self, result: "exec_result.ExecResult", - expected: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]] = None, + expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] = None, ) -> None: """Exception for error on process calls. :param result: execution result :type result: exec_result.ExecResult :param expected: expected return codes - :type expected: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]] + :type expected: typing.Optional[typing.Iterable[typing.Union[int, proc_enums.ExitCodes]]] .. versionchanged:: 1.1.1 - provide full result """ @@ -140,91 +140,101 @@ def stderr(self) -> str: return self.result.stderr_str -class ParallelCallExceptions(ExecCalledProcessError): - """Exception raised during parallel call as result of exceptions.""" +class ParallelCallProcessError(ExecCalledProcessError): + """Exception during parallel execution.""" - __slots__ = ("cmd", "exceptions", "errors", "results", "expected") + __slots__ = ("cmd", "errors", "results", "expected") def __init__( self, command: str, - exceptions: typing.Dict[typing.Tuple[str, int], Exception], errors: typing.Dict[typing.Tuple[str, int], "exec_result.ExecResult"], results: typing.Dict[typing.Tuple[str, int], "exec_result.ExecResult"], expected: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]] = None, + *, + _message: typing.Optional[str] = None ) -> None: - """Exception raised during parallel call as result of exceptions. + """Exception during parallel execution. :param command: command :type command: str - :param exceptions: Exceptions on connections - :type exceptions: typing.Dict[typing.Tuple[str, int], Exception] :param errors: results with errors :type errors: typing.Dict[typing.Tuple[str, int], ExecResult] :param results: all results :type results: typing.Dict[typing.Tuple[str, int], ExecResult] :param expected: expected return codes :type expected: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]] + :param _message: message override + :type _message: typing.Optional[str] """ expected = expected or [proc_enums.ExitCodes.EX_OK] - self.expected = proc_enums.exit_codes_to_enums(expected) - self.cmd = command - self.exceptions = exceptions - self.errors = errors - self.results = results - message = ( - "Command {self.cmd!r} " - "during execution raised exceptions: \n" - "\t{exceptions}".format( - self=self, - exceptions="\n\t".join( - "{host}:{port} - {exc} ".format(host=host, port=port, exc=exc) - for (host, port), exc in exceptions.items() + prep_expected = proc_enums.exit_codes_to_enums(expected) + message = _message or ( + "Command {cmd!r} " + "returned unexpected exit codes on several hosts\n" + "Expected: {expected}\n" + "Got:\n" + "\t{errors}".format( + cmd=command, + expected=prep_expected, + errors="\n\t".join( + "{host}:{port} - {code} ".format(host=host, port=port, code=result.exit_code) + for (host, port), result in errors.items() ), ) ) - super(ParallelCallExceptions, self).__init__(message) + super(ParallelCallProcessError, self).__init__(message) + self.cmd = command + self.errors = errors + self.results = results + self.expected = prep_expected -class ParallelCallProcessError(ExecCalledProcessError): - """Exception during parallel execution.""" +class ParallelCallExceptions(ParallelCallProcessError): + """Exception raised during parallel call as result of exceptions.""" - __slots__ = ("cmd", "errors", "results", "expected") + __slots__ = ("cmd", "exceptions") def __init__( self, command: str, + exceptions: typing.Dict[typing.Tuple[str, int], Exception], errors: typing.Dict[typing.Tuple[str, int], "exec_result.ExecResult"], results: typing.Dict[typing.Tuple[str, int], "exec_result.ExecResult"], expected: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]] = None, + *, + _message: typing.Optional[str] = None ) -> None: - """Exception during parallel execution. + """Exception raised during parallel call as result of exceptions. :param command: command :type command: str + :param exceptions: Exceptions on connections + :type exceptions: typing.Dict[typing.Tuple[str, int], Exception] :param errors: results with errors :type errors: typing.Dict[typing.Tuple[str, int], ExecResult] :param results: all results :type results: typing.Dict[typing.Tuple[str, int], ExecResult] :param expected: expected return codes :type expected: typing.Optional[typing.List[typing.Union[int, proc_enums.ExitCodes]]] + :param _message: message override + :type _message: typing.Optional[str] """ expected = expected or [proc_enums.ExitCodes.EX_OK] - self.expected = proc_enums.exit_codes_to_enums(expected) - self.cmd = command - self.errors = errors - self.results = results - message = ( - "Command {self.cmd!r} " - "returned unexpected exit codes on several hosts\n" - "Expected: {self.expected}\n" - "Got:\n" - "\t{errors}".format( - self=self, - errors="\n\t".join( - "{host}:{port} - {code} ".format(host=host, port=port, code=result.exit_code) - for (host, port), result in errors.items() + prep_expected = proc_enums.exit_codes_to_enums(expected) + message = _message or ( + "Command {cmd!r} " + "during execution raised exceptions: \n" + "\t{exceptions}".format( + cmd=command, + exceptions="\n\t".join( + "{host}:{port} - {exc} ".format(host=host, port=port, exc=exc) + for (host, port), exc in exceptions.items() ), ) ) - super(ParallelCallProcessError, self).__init__(message) + super(ParallelCallExceptions, self).__init__( + command=command, errors=errors, results=results, expected=prep_expected, _message=message + ) + self.cmd = command + self.exceptions = exceptions diff --git a/exec_helpers/subprocess_runner.py b/exec_helpers/subprocess_runner.py index ac95661..6f05f0c 100644 --- a/exec_helpers/subprocess_runner.py +++ b/exec_helpers/subprocess_runner.py @@ -33,8 +33,6 @@ from exec_helpers import _log_templates from exec_helpers import _subprocess_helpers -logger = logging.getLogger(__name__) # type: logging.Logger - # noinspection PyTypeHints class SubprocessExecuteAsyncResult(api.ExecuteAsyncResult): @@ -64,7 +62,9 @@ def stdout(self) -> typing.Optional[typing.IO]: # type: ignore class Subprocess(api.ExecHelper, metaclass=metaclasses.SingleLock): """Subprocess helper with timeouts and lock-free FIFO.""" - def __init__(self, log_mask_re: typing.Optional[str] = None) -> None: + def __init__( + self, log_mask_re: typing.Optional[str] = None, *, logger: logging.Logger = logging.getLogger(__name__) + ) -> None: """Subprocess helper with timeouts and lock-free FIFO. For excluding race-conditions we allow to run 1 command simultaneously @@ -72,9 +72,12 @@ def __init__(self, log_mask_re: typing.Optional[str] = None) -> None: :param log_mask_re: regex lookup rule to mask command for logger. all MATCHED groups will be replaced by '<*masked*>' :type log_mask_re: typing.Optional[str] + :param logger: logger instance to use + :type logger: logging.Logger .. versionchanged:: 1.2.0 log_mask_re regex rule for masking cmd .. versionchanged:: 3.1.0 Not singleton anymore. Only lock is shared between all instances. + .. versionchanged:: 3.2.0 Logger can be enforced. """ super(Subprocess, self).__init__(logger=logger, log_mask_re=log_mask_re) @@ -85,6 +88,8 @@ def _exec_command( # type: ignore timeout: typing.Union[int, float, None], verbose: bool = False, log_mask_re: typing.Optional[str] = None, + *, + stdin: typing.Union[bytes, str, bytearray, None] = None, **kwargs: typing.Any ) -> exec_result.ExecResult: """Get exit status from channel with timeout. @@ -100,6 +105,8 @@ def _exec_command( # type: ignore :param log_mask_re: regex lookup rule to mask command for logger. all MATCHED groups will be replaced by '<*masked*>' :type log_mask_re: typing.Optional[str] + :param stdin: pass STDIN text to the process + :type stdin: typing.Union[bytes, str, bytearray, None] :param kwargs: additional parameters for call. :type kwargs: typing.Any :return: Execution result @@ -130,7 +137,7 @@ def close_streams() -> None: # Store command with hidden data cmd_for_log = self._mask_command(cmd=command, log_mask_re=log_mask_re) - result = exec_result.ExecResult(cmd=cmd_for_log, stdin=kwargs.get("stdin")) + result = exec_result.ExecResult(cmd=cmd_for_log, stdin=stdin) # pylint: disable=assignment-from-no-return # noinspection PyNoneFunctionAssignment @@ -183,7 +190,7 @@ def close_streams() -> None: self.logger.debug(wait_err_msg) raise exceptions.ExecHelperTimeoutError(result=result, timeout=timeout) - def execute_async( + def execute_async( # pylint: disable=arguments-differ self, command: str, stdin: typing.Union[str, bytes, bytearray, None] = None, @@ -191,6 +198,11 @@ def execute_async( open_stderr: bool = True, verbose: bool = False, log_mask_re: typing.Optional[str] = None, + *, + cwd: typing.Optional[typing.Union[str, bytes]] = None, + env: typing.Optional[ + typing.Union[typing.Mapping[bytes, typing.Union[bytes, str]], typing.Mapping[str, typing.Union[bytes, str]]] + ] = None, **kwargs: typing.Any ) -> SubprocessExecuteAsyncResult: """Execute command in async mode and return Popen with IO objects. @@ -208,6 +220,10 @@ def execute_async( :param log_mask_re: regex lookup rule to mask command for logger. all MATCHED groups will be replaced by '<*masked*>' :type log_mask_re: typing.Optional[str] + :param cwd: Sets the current directory before the child is executed. + :type cwd: typing.Optional[typing.Union[str, bytes]] + :param env: Defines the environment variables for the new process. + :type env: typing.Optional[typing.Mapping[typing.Union[str, bytes], typing.Union[str, bytes]]] :param kwargs: additional parameters for call. :type kwargs: typing.Any :return: Tuple with control interface and file-like objects for STDIN/STDERR/STDOUT @@ -224,6 +240,7 @@ def execute_async( .. versionadded:: 1.2.0 .. versionchanged:: 2.1.0 Use typed NamedTuple as result + .. versionchanged:: 3.2.0 Expose cwd and env as optional keyword-only arguments """ cmd_for_log = self._mask_command(cmd=command, log_mask_re=log_mask_re) @@ -237,8 +254,8 @@ def execute_async( stderr=subprocess.PIPE if open_stderr else subprocess.DEVNULL, stdin=subprocess.PIPE, shell=True, - cwd=kwargs.get("cwd", None), - env=kwargs.get("env", None), + cwd=cwd, + env=env, universal_newlines=False, **_subprocess_helpers.subprocess_kw ) diff --git a/pyproject.toml b/pyproject.toml index 708fb53..0076248 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,3 +9,4 @@ requires = [ [tool.black] line-length = 120 safe = true +py36 = false diff --git a/setup.py b/setup.py index d5f11e7..0f4951c 100644 --- a/setup.py +++ b/setup.py @@ -29,14 +29,13 @@ import shutil import sys +import setuptools try: # noinspection PyPackageRequirements from Cython.Build import cythonize except ImportError: cythonize = None -import setuptools - with open(os.path.join(os.path.dirname(__file__), "exec_helpers", "__init__.py")) as f: source = f.read() diff --git a/tox.ini b/tox.ini index f704928..610416c 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ [tox] minversion = 2.0 -envlist = black, pep8, pylint, mypy, bandit, pep257, py{35,36,37,py3}, docs, py{35,36,37}-nocov +envlist = pep8, pylint, mypy, bandit, pep257, py{35,36,37,py3}, docs, py{35,36,37}-nocov skipsdist = True skip_missing_interpreters = True