Repository: pyinstaller/pyinstaller

Issue:
Bug in TOC type (its __sub__ function)
<!--
Welcome to the PyInstaller issue tracker! Before creating an issue, please heed the following:

1. This tracker should only be used to report bugs and request features / enhancements to PyInstaller
    - For questions and general support, use the mailing list, see
	  <http://www.pyinstaller.org/support.html#mailing-list>
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
   the original discussion.
3. When making a bug report, make sure you provide all required information. The easier it is for
   maintainers to reproduce, the faster it'll be fixed.
-->

<!-- +++ ONLY TEXT +++ DO NOT POST IMAGES +++ -->

## Description of the issue

The [documentation](https://pyinstaller.readthedocs.io/en/stable/advanced-topics.html#toc-class-table-of-contents) shows you can exclude binaries by adding something like the following to your spec file:

```python
a.binaries -= [('badmodule', None, None)]
```

I noticed that if you then subtract again, `a.binaries` becomes an empty list.

For example:

```python
toc = TOC([('module1', None, None), ('module2', None, None), ('badmodule', None, None)])
toc -= [('badmodule', None, None)]  # -> [('module1', None, None), ('module2', None, None)]
toc -= [('module1', None, None)]  # -> []
```

I believe [L117 in `PyInstaller/building/datastruct.py`](https://github.com/pyinstaller/pyinstaller/blob/15f23a8a89be5453b3520df8fc3e667346e103a6/PyInstaller/building/datastruct.py#L117) is the culprit. If I change it to `result.append(entry)` it fixes the issue. Otherwise the result `__sub__` returns has an empty set for its `filenames`, which then causes subsequent subtractions to fail, because the subtraction relies on `filenames` being set properly (see [L109](https://github.com/pyinstaller/pyinstaller/blob/15f23a8a89be5453b3520df8fc3e667346e103a6/PyInstaller/building/datastruct.py#L109)).

### Context information (for bug reports)

(this is probably irrelevant but just in case)

* Output of `pyinstaller --version`: ```4.9```
* Version of Python: <!-- e.g. 3.7 --> ```3.7.4```
* Platform: <!-- e.g GNU/Linux (distribution), Windows (language settings), OS X, FreeBSD --> ```Windows 10 1909 en-GB```
* How you installed Python: <!-- e.g. python.org/downloads, conda, brew, pyenv, apt, Windows store --> ```python.org/downloads```
* Did you also try this on another platform? Does it work there? -- I was also able to reproduce this on a Ubuntu 16.10 VM with Python 3.9.9 and PyInstaller 4.10.

### A minimal example program which shows the error

```python
from PyInstaller.building.datastruct import TOC


toc = TOC([('module1', None, None), ('module2', None, None), ('badmodule', None, None)])

toc -= [('badmodule', None, None)]
assert toc == TOC([('module1', None, None), ('module2', None, None)])  # passes

toc -= [('module1', None, None)]
assert toc == TOC([('module2', None, None)])  # fails
```

### Stacktrace / full error message

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/tmp/py249370e", line 10, in <module>
AssertionError
```

and if you `print(toc)` at this point you get `[]`.


Discussion:
Nice catch!

Resolving patch:
diff --git a/PyInstaller/building/datastruct.py b/PyInstaller/building/datastruct.py
--- a/PyInstaller/building/datastruct.py
+++ b/PyInstaller/building/datastruct.py
@@ -105,17 +105,9 @@ def extend(self, other):
             self.append(entry)
 
     def __sub__(self, other):
+        # Construct new TOC with entries not contained in the other TOC
         other = TOC(other)
-        filenames = self.filenames - other.filenames
-        result = TOC()
-        for entry in self:
-            unique = unique_name(entry)
-
-            if unique in filenames:
-                # Directly use list.append() instead of TOC.append() to avoid unnecessary uniqueness checks.
-                # Hence the use of super(TOC, result).
-                super(TOC, result).append(entry)
-        return result
+        return TOC([entry for entry in self if unique_name(entry) not in other.filenames])
 
     def __rsub__(self, other):
         result = TOC(other)


Test patch:
diff --git a/tests/unit/test_TOC.py b/tests/unit/test_TOC.py
--- a/tests/unit/test_TOC.py
+++ b/tests/unit/test_TOC.py
@@ -264,6 +264,14 @@ def test_sub_after_setitem():
     assert len(toc) == 3
 
 
+def test_sub_after_sub():
+    toc = TOC(ELEMS1)
+    toc -= [ELEMS1[0]]
+    toc -= [ELEMS1[1]]
+    expected = list(ELEMS1[2:])
+    assert toc == expected
+
+
 def test_setitem_1():
     toc = TOC()
     toc[:] = ELEMS1