Repository: tomerfiliba/plumbum

Issue:
StdinDataRedirection commands fail upon modification (TEE, FG, etc.)
Commands to which standard input has been supplied from a Python string – which are `StdinDataRedirection` commands – erroneously throw exception `RedirectionError` when combined with built-in modifiers –

    stdin is already redirected

– even though `stdin` is only being redirected the one time.

---

```python
>>> from plumbum import TEE, FG, local                                          

# this works

>>> local['ls']['-d', '/'] & FG                                                                                                                               
/                                                                              
>>> local['ls']['-d', '/'] & TEE                                                                                                                              
/                                     
(0, '/\n', '')

# this does not work

>>> (local['cat']['-'] << 'this breaks') & FG                                   
Traceback (most recent call last):                                                                                                                            
    …
    raise RedirectionError("stdin is already redirected")
plumbum.commands.base.RedirectionError: stdin is already redirected
```

---

Affected modifiers invoke `cmd.run(stdin=None, …)`. It appears sufficient to change these to simply **omit the keyword** pair `stdin=None`.

However, it's unclear whether this would have unwanted side-effects; and, it seems rather that `StdinDataRedirection` is in the wrong, in throwing an exception over the value `None`.

`BaseRedirection.popen` permits either `PIPE` or `None`:

```python
if self.KWARG in kwargs and kwargs[self.KWARG] not in (PIPE, None):
    raise RedirectionError(…)
```

`StdinDataRedirection.popen` only permits `PIPE`:

```python
if "stdin" in kwargs and kwargs["stdin"] != PIPE:
    raise RedirectionError(…)
```

→ As such, the resolution is perhaps that the above check in `StdinDataRedirection.popen` be made to mirror `BaseRedirection.popen`.

Comments:

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0da79ea..03c4749 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -27,7 +27,7 @@ jobs:
       with:
         fetch-depth: 0
     - uses: actions/setup-python@v2
-    - uses: pre-commit/action@v2.0.3
+    - uses: pre-commit/action@v3.0.0
     - name: pylint
       run: |
         echo "::add-matcher::$GITHUB_WORKSPACE/.github/matchers/pylint.json"
diff --git a/noxfile.py b/noxfile.py
index 9b68bfe..9fae464 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -22,7 +22,7 @@ def pylint(session):
     Run pylint.
     """
 
-    session.install(".", "paramiko", "ipython", "pylint")
+    session.install(".", "paramiko", "ipython", "pylint~=2.14.3")
     session.run("pylint", "plumbum", *session.posargs)
 
 
diff --git a/plumbum/cli/progress.py b/plumbum/cli/progress.py
index 3d21a8a..fce6b3c 100644
--- a/plumbum/cli/progress.py
+++ b/plumbum/cli/progress.py
@@ -69,7 +69,7 @@ class ProgressBase(ABC):
         return rval
 
     def next(self):
-        return self.__next__()
+        return next(self)
 
     @property
     def value(self):
diff --git a/plumbum/commands/base.py b/plumbum/commands/base.py
index 51e667d..eafbde3 100644
--- a/plumbum/commands/base.py
+++ b/plumbum/commands/base.py
@@ -547,7 +547,7 @@ class StdinDataRedirection(BaseCommand):
         return self.cmd.machine
 
     def popen(self, args=(), **kwargs):
-        if "stdin" in kwargs and kwargs["stdin"] != PIPE:
+        if kwargs.get("stdin") not in (PIPE, None):
             raise RedirectionError("stdin is already redirected")
         data = self.data
         if isinstance(data, str) and self._get_encoding() is not None:
@@ -558,8 +558,9 @@ class StdinDataRedirection(BaseCommand):
             f.write(chunk)
             data = data[self.CHUNK_SIZE :]
         f.seek(0)
+        kwargs["stdin"] = f
         # try:
-        return self.cmd.popen(args, stdin=f, **kwargs)
+        return self.cmd.popen(args, **kwargs)
         # finally:
         #    f.close()
 
diff --git a/plumbum/fs/atomic.py b/plumbum/fs/atomic.py
index 5aebc80..d796b2f 100644
--- a/plumbum/fs/atomic.py
+++ b/plumbum/fs/atomic.py
@@ -290,7 +290,7 @@ class PidFile:
             return
         self._ctx = self.atomicfile.locked(blocking=False)
         try:
-            self._ctx.__enter__()
+            self._ctx.__enter__()  # pylint: disable=unnecessary-dunder-call
         except OSError:
             self._ctx = None
             try:
diff --git a/pyproject.toml b/pyproject.toml
index b34fa89..27ddb99 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -68,6 +68,7 @@ ignore = [
 [tool.pylint]
 master.py-version = "3.6"
 master.jobs = "0"
+master.load-plugins = ["pylint.extensions.no_self_use"]
 reports.output-format = "colorized"
 similarities.ignore-imports = "yes"
 messages_control.enable = [
@@ -103,4 +104,5 @@ messages_control.disable = [
   "too-many-return-statements",
   "too-many-statements",
   "unidiomatic-typecheck", # TODO: might be able to remove
+  "unnecessary-lambda-assignment", # TODO: 4 instances
 ]


Test patch:
diff --git a/tests/test_local.py b/tests/test_local.py
index dc76a7b..d96d39b 100644
--- a/tests/test_local.py
+++ b/tests/test_local.py
@@ -639,6 +639,25 @@ class TestLocalMachine:
             assert result[1] == EXPECT
             assert EXPECT == capfd.readouterr()[0]
 
+    @skip_on_windows
+    @pytest.mark.parametrize(
+        "modifier, expected",
+        [
+            (FG, None),
+            (TF(FG=True), True),
+            (RETCODE(FG=True), 0),
+            (TEE, (0, "meow", "")),
+        ],
+    )
+    def test_redirection_stdin_modifiers_fg(self, modifier, expected, capfd):
+        "StdinDataRedirection compatible with modifiers which write to stdout"
+        from plumbum.cmd import cat
+
+        cmd = cat << "meow"
+
+        assert cmd & modifier == expected
+        assert capfd.readouterr() == ("meow", "")
+
     @skip_on_windows
     def test_logger_pipe(self):
         from plumbum.cmd import bash