Skip to content

Commit

Permalink
Command: Add .check_call(), .check_output()
Browse files Browse the repository at this point in the history
Working like the subprocess equivalents

Also, Command.__call__ is now delegating to .check_output()
  • Loading branch information
userzimmermann committed Jul 26, 2019
1 parent 3a2b9ed commit 7427d1c
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 47 deletions.
6 changes: 5 additions & 1 deletion nodely/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@
# __version__ module is created by setuptools_scm during setup
from .__version__ import version as __version__

__all__ = ('install', 'uninstall', 'which', 'Popen', 'call')
from .error import NodeCommandError

__all__ = (
'install', 'uninstall', 'which', 'Popen', 'call',
'NodeCommandError')


#: The absolute path to the local node_modules/ sub-directory used for
Expand Down
120 changes: 87 additions & 33 deletions nodely/bin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
Command proxy to the ``node_modules/.bin/`` directory
"""

from subprocess import CalledProcessError, PIPE
from types import ModuleType
import os
import platform
import sys

Expand All @@ -30,6 +32,8 @@

from nodely import NODE_MODULES_DIR

from .error import NodeCommandError

__all__ = ['Command']


Expand All @@ -43,42 +47,43 @@

class Module(ModuleType):
"""
Wrapper module class for :mod:`nodely.bin`
Wrapper module class for :mod:`nodely.bin`.
Makes module directly act as command proxy to the ``node_modules/.bin/``
directory via :meth:`.__getitem__` and :meth:`.__getattr__`
"""

def __init__(self):
"""
Get ``__name__`` and ``__doc__`` and update ``.__dict__`` from
original ``nodely.bin`` module
Set ``.__name__`` and ``.__doc__``, and update ``.__dict__``.
All taken from original ``nodely.bin`` module
"""
super(Module, self).__init__(__name__, ORIGIN.__doc__)
self.__dict__.update(ORIGIN.__dict__)

def __getitem__(self, cmdname):
"""
Get a :class:`nodely.bin.Command` instance for given `cmdname`
Get a :class:`nodely.bin.Command` instance for given `cmdname`.
:raises OSError: if executable can not be found
"""
return Command(cmdname)

def __getattr__(self, name):
"""
Get a :class:`nodely.bin.Command` instance for given command `name`
Get a :class:`nodely.bin.Command` instance for given command `name`.
:raises OSError: if executable can not be found
:raises OSError: if executable cannot be found
"""
try: # first check if original module has the given attribute
return getattr(ORIGIN, name)
except AttributeError:
pass
# and don't treat special Python member names as Node.js commands
if name.startswith('__'):
raise AttributeError("{!r} has no attribute {!r}"
.format(self, name))
raise AttributeError(
"{!r} has no attribute {!r}".format(self, name))
return self[name]

def __dir__(self):
Expand All @@ -95,18 +100,19 @@ def __dir__(self):

class Command(zetup.object, Path):
"""
A Node.js executable from ``node_modules/.bin/`` of current Python
environment
A Node.js executable from current Python environment.
Residing in ``node_modules/.bin/``
"""

def __new__(cls, name):
"""
Check existance an store absolute path in ``path.Path`` base
Check existance and store absolute path in ``path.Path`` base.
:param name:
The basename of the executable in ``node_modules/.bin``
The basename of the executable in ``node_modules/.bin``
:raises OSError:
if executable can not be found
if executable cannot be found
"""
if WIN: # pragma: no cover
name += '.cmd'
Expand All @@ -115,49 +121,97 @@ def __new__(cls, name):
return cmd

def __init__(self, name):
"""
:meth:`.__new__` does all the work :)
"""
""":meth:`.__new__` does all the work :)."""
pass

@property
def name(self):
"""
The name of this Node.js command
"""
"""Get the name of this Node.js command."""
name = Path(self).basename()
if WIN: # pragma: no cover
# remove .cmd file extension
name = name.splitext()[0]
name = name.splitext( # pylint: disable=no-value-for-parameter
)[0]
return str(name)

def Popen(self, cmdargs=None, **kwargs):
def Popen(self, cmdargs=None, **options):
"""
Create a ``subprocess`` for this Node.js command with optional
sequence of `cmdargs` strings and optional `kwargs` for
``zetup.Popen``, including all options for ``subprocess.Popen``
Create a ``subprocess`` for this Node.js command.
:param cmdargs:
Optional sequence of command argument strings.
:param options:
General options for ``zetup.call``, including all options for
``subprocess.call``.
"""
command = [str(self)]
if cmdargs is not None:
command += cmdargs
return zetup.Popen(command, **kwargs)
return zetup.Popen(command, **options)

def call(self, cmdargs=None, **kwargs):
def call(self, cmdargs=None, **options):
"""
Call this Node.js command with optional sequence of `cmdargs` strings
and optional `kwargs` for ``zetup.call``, including all options for
``subprocess.call``
Call this Node.js command.
:param cmdargs:
Optional sequence of command argument strings.
:param options:
General options for ``zetup.call``, including all options for
``subprocess.call``.
"""
command = [str(self)]
if cmdargs is not None:
command += cmdargs
return zetup.call(command, **kwargs)
return zetup.call(command, **options)

def __call__(self, cmdargs=None, **kwargs):
def check_call(self, cmdargs=None, **options):
"""
Alternative to :meth:`.call`
Call this Node.js command and check its return code.
:param cmdargs:
Optional sequence of command argument strings.
:param options:
General options for ``zetup.call``, including all options for
``subprocess.call``.
:raises subprocess.CalledProcessError:
When return code is not zero.
"""
return self.call(cmdargs, **kwargs)
command = [str(self)]
if cmdargs is not None:
command += cmdargs

returncode = zetup.call(command, **options)
if returncode:
raise NodeCommandError(returncode, command, os.getcwd())

def check_output(self, cmdargs=None, **options):
"""
Call this Node.js command, check return code, and return its stdout.
:param cmdargs:
Optional sequence of command argument strings.
:param options:
General options for ``zetup.call``, including all options for
``subprocess.call``.
:raises subprocess.CalledProcessError:
When return code is not zero.
"""
command = [str(self)]
if cmdargs is not None:
command += cmdargs

process = zetup.Popen(command, stdout=PIPE, **options)
out, _ = process.communicate()
if process.returncode:
raise NodeCommandError(process.returncode, command, os.getcwd())

return out

def __call__(self, cmdargs=None, **kwargs):
"""Alternative to :meth:`.check_output`."""
return self.check_call(cmdargs, **kwargs)

def __repr__(self):
return "{}[{!r}]".format(__name__, self.name)
18 changes: 18 additions & 0 deletions nodely/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""nodely exceptions."""

from subprocess import CalledProcessError

__all__ = ('NodeCommandError', )


class NodeCommandError(CalledProcessError):
"""A Node.js command returned non-zero exit code"""

def __init__(self, returncode, cmd, cwd):
super().__init__(returncode=returncode, cmd=cmd)
self.cwd = cwd
self.args += (cwd, )

def __str__(self):
return "{} in working directory {!r}".format(
super().__str__(), self.cwd)
52 changes: 39 additions & 13 deletions test/test_nodely_bin.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ def test__getattr__non_existent(self):
def test__getattr__non_existent__special__(self):
with pytest.raises(AttributeError) as exc:
getattr(nodely.bin, '__non_existent__')
exc.match(r"^{!r} has no attribute '__non_existent__'$"
.format(nodely.bin).replace('\\', r'\\'))
exc.match(
r"^{!r} has no attribute '__non_existent__'$"
.format(nodely.bin).replace('\\', r'\\'))

def test__dir__(self):
cmdnames = (f.basename()
Expand All @@ -74,34 +75,59 @@ def test_name(self, node_package_command):
assert Command.name is not Path.name
assert Command(node_package_command).name == node_package_command

def test_Popen(self, node_package_command, node_package_command_args,
node_package_command_output_regex):
def test_Popen(
self, node_package_command, node_package_command_args,
node_package_command_output_regex):
command = Command(node_package_command)
process = command.Popen(
node_package_command_args, stdout=PIPE, stderr=PIPE,
universal_newlines=True)

out, err = process.communicate()
assert node_package_command_output_regex.match(out.strip())
assert not err

def test_call(self, capfd, node_package_command,
node_package_command_args,
node_package_command_output_regex):
def test_call(
self, capfd, node_package_command, node_package_command_args,
node_package_command_output_regex):
command = Command(node_package_command)
assert command.call(node_package_command_args) is 0

out, err = capfd.readouterr()
assert node_package_command_output_regex.match(out.strip())
assert not err

def test__call__(self, capfd, node_package_command,
node_package_command_args,
node_package_command_output_regex):
def test_check_call(
self, capfd, node_package_command, node_package_command_args,
node_package_command_output_regex):
command = Command(node_package_command)
assert command(node_package_command_args) is 0
assert command.check_call(node_package_command_args) is None

out, err = capfd.readouterr()
assert node_package_command_output_regex.match(out.strip())
assert not err

def test_check_output(
self, capfd, node_package_command, node_package_command_args,
node_package_command_output_regex):
command = Command(node_package_command)
assert node_package_command_output_regex.match(
command.check_output(node_package_command_args))

out, err = capfd.readouterr()
assert not out and not err

def test__call__(
self, capfd, node_package_command, node_package_command_args,
node_package_command_output_regex):
command = Command(node_package_command)
assert node_package_command_output_regex.match(
command.check_output(node_package_command_args))

out, err = capfd.readouterr()
assert not out and not err

def test__repr__(self, node_package_command):
assert repr(Command(node_package_command)) \
== "nodely.bin[{!r}]".format(node_package_command)
assert (
repr(Command(node_package_command)) ==
"nodely.bin['{}']".format(node_package_command))

0 comments on commit 7427d1c

Please sign in to comment.