Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-smbprotocol for 
openSUSE:Factory checked in at 2026-07-08 17:38:48
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-smbprotocol (Old)
 and      /work/SRC/openSUSE:Factory/.python-smbprotocol.new.1982 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-smbprotocol"

Wed Jul  8 17:38:48 2026 rev:26 rq:1364480 version:1.17.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-smbprotocol/python-smbprotocol.changes    
2026-04-04 19:09:49.903064263 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-smbprotocol.new.1982/python-smbprotocol.changes
  2026-07-08 17:41:52.932645825 +0200
@@ -1,0 +2,25 @@
+Tue Jul  7 14:04:54 UTC 2026 - Martin Hauke <[email protected]>
+
+- Update to version 1.17.0
+  * Drop support for Python 3.9, minimum verison if now 3.10
+  * Added the hostname_override kwarg to smbprotocol.Session that
+    can override the hostname used in the SPN for authentication.
+  * Correctly perform a DFS referral lookup in the query_directory
+    on a directory.
+  * Fix the FlagField str representation when the flag_type
+    contains a dunder attribute with an int value.
+  * Added some extra flags from MS-SMB2 that have been added
+    recently.
+  * Fix Connection.client_security_mode to be
+    SMB2_NEGOTIATE_SIGNING_REQUIRED when server reports signing is
+    required.
+  * Solve numerous issues with smbclient.rmtree and ignore_errors
+    not working with various exceptions.
+  * Make smbclient.scandir return a context manager that can close
+    SMB resources.
+  * Handle STATUS_NO_SUCH_FILE during smbclient.scandir when using
+    a pattern that matches no file in the specified path.
+  * Improve various threading race conditions and deadlocks in the
+    underlying client handler.
+
+-------------------------------------------------------------------

Old:
----
  python-smbprotocol-1.16.1.tar.gz

New:
----
  python-smbprotocol-1.17.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-smbprotocol.spec ++++++
--- /var/tmp/diff_new_pack.E7BGCw/_old  2026-07-08 17:41:53.876678722 +0200
+++ /var/tmp/diff_new_pack.E7BGCw/_new  2026-07-08 17:41:53.880678862 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python-smbprotocol
-Version:        1.16.1
+Version:        1.17.0
 Release:        0
 Summary:        SMBv2/v3 client for Python 2 and 3
 License:        MIT

++++++ python-smbprotocol-1.16.1.tar.gz -> python-smbprotocol-1.17.0.tar.gz 
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/.github/workflows/ci.yml 
new/smbprotocol-1.17.0/.github/workflows/ci.yml
--- old/smbprotocol-1.16.1/.github/workflows/ci.yml     2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/.github/workflows/ci.yml     2026-07-07 
05:14:30.000000000 +0200
@@ -27,7 +27,7 @@
 
     steps:
     - name: Check out code
-      uses: actions/checkout@v5
+      uses: actions/checkout@v6
 
     - name: Build smbprotocol
       run: |
@@ -35,7 +35,7 @@
         python -m build
 
     - name: Upload built smbprotocol
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v7
       with:
         name: artifact
         path: ./dist/*
@@ -54,7 +54,6 @@
         - macOS-latest
         - windows-latest
         python-version:
-        - 3.9
         - '3.10'
         - '3.11'
         - '3.12'
@@ -62,13 +61,13 @@
         - '3.14'
 
     steps:
-    - uses: actions/checkout@v5
+    - uses: actions/checkout@v6
 
     - uses: actions/setup-python@v6
       with:
         python-version: ${{ matrix.python-version }}
 
-    - uses: actions/download-artifact@v5
+    - uses: actions/download-artifact@v8
       with:
         name: artifact
         path: ./dist
@@ -94,21 +93,21 @@
 
     - name: Upload Test Results
       if: always()
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v7
       with:
         name: Unit Test Results (${{ matrix.os }} ${{ matrix.python-version }} 
${{ matrix.python-arch }})
         path: ./junit/test-results.xml
 
     - name: Upload Coverage Results
       if: always()
-      uses: actions/upload-artifact@v4
+      uses: actions/upload-artifact@v7
       with:
         name: Coverage Results (${{ matrix.os }} ${{ matrix.python-version }} 
${{ matrix.python-arch }})
         path: ./coverage.xml
 
     - name: Upload Coverage to codecov
       if: always()
-      uses: codecov/codecov-action@v5
+      uses: codecov/codecov-action@v6
       with:
         files: ./coverage.xml
         flags: ${{ steps.os.outputs.name }},py${{ matrix.python-version }},${{ 
matrix.python-arch }}
@@ -124,7 +123,7 @@
       id-token: write
 
     steps:
-    - uses: actions/download-artifact@v5
+    - uses: actions/download-artifact@v8
       with:
         name: artifact
         path: ./dist
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/.pre-commit-config.yaml 
new/smbprotocol-1.17.0/.pre-commit-config.yaml
--- old/smbprotocol-1.16.1/.pre-commit-config.yaml      2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/.pre-commit-config.yaml      2026-07-07 
05:14:30.000000000 +0200
@@ -1,10 +1,10 @@
 repos:
 - repo: https://github.com/psf/black
-  rev: 25.9.0
+  rev: 26.3.1
   hooks:
   - id: black
 
 - repo: https://github.com/PyCQA/isort
-  rev: 6.1.0
+  rev: 8.0.1
   hooks:
   - id: isort
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/CHANGELOG.md 
new/smbprotocol-1.17.0/CHANGELOG.md
--- old/smbprotocol-1.16.1/CHANGELOG.md 2026-04-02 04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/CHANGELOG.md 2026-07-07 05:14:30.000000000 +0200
@@ -1,5 +1,26 @@
 # Changelog
 
+## 1.17.0 - 2026-07-07
+
+* Drop support for Python 3.9, minimum verison if now 3.10
+* Added the `hostname_override` kwarg to `smbprotocol.Session` that can 
override the hostname used in the SPN for authentication
+* Correctly perform a DFS referral lookup in the `query_directory` on a 
directory
+* Fix the `FlagField` str representation when the `flag_type` contains a 
dunder attribute with an `int` value
+* Added some extra flags from MS-SMB2 that have been added recently
+* Fix `Connection.client_security_mode` to be 
`SMB2_NEGOTIATE_SIGNING_REQUIRED` when server reports signing is required
+* Solve numerous issues with `smbclient.rmtree` and `ignore_errors` not 
working with various exceptions
+* Make `smbclient.scandir` return a context manager that can close SMB 
resources. This method should now be called like
+  * The return value is still an iterable and will continue to work as it did 
before but code should still be updated to use the context manager to ensure 
resources are released
+
+```python
+with smbclient.scandir(...) as scan_gen:
+    for entry in scan_gen:
+        ...
+```
+
+* Handle `STATUS_NO_SUCH_FILE` during `smbclient.scandir` when using a pattern 
that matches no file in the specified path
+* Improve various threading race conditions and deadlocks in the underlying 
client handler
+
 ## 1.16.1 - 2026-04-02
 
 * Fix memory leaks in `Structure` and subclasses by converting lambda default 
values to static methods and using `weakref.proxy` to prevent circular 
references
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/README.md 
new/smbprotocol-1.17.0/README.md
--- old/smbprotocol-1.16.1/README.md    2026-04-02 04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/README.md    2026-07-07 05:14:30.000000000 +0200
@@ -32,7 +32,7 @@
 
 ## Requirements
 
-* Python 3.9+
+* Python 3.10+
 * For Kerberos auth on Linux
     * [python-gssapi](https://github.com/pythongssapi/python-gssapi)
     * [pykrb5](https://github.com/jborean93/pykrb5)
@@ -224,8 +224,9 @@
 ```bash
 # Install in current environment.
 # Recommend to have virtual environment installed at .venv path.
-pip install -r requirements-dev.txt
-pip install -e .
+# Dependency groups need pip 25.1+.
+python -m pip install "pip>=25.1"
+pip install -e . --group dev
 ```
 
 From there to run the basic tests run;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/build_helpers/lib.sh 
new/smbprotocol-1.17.0/build_helpers/lib.sh
--- old/smbprotocol-1.16.1/build_helpers/lib.sh 2026-04-02 04:49:28.000000000 
+0200
+++ new/smbprotocol-1.17.0/build_helpers/lib.sh 2026-07-07 05:14:30.000000000 
+0200
@@ -64,7 +64,9 @@
         --verbose
 
     echo "Installing dev dependencies"
-    python -m pip install -r requirements-dev.txt
+    # --group needs pip 25.1+ (PEP 735)
+    python -m pip install "pip>=25.1"
+    python -m pip install --group dev
 
     if [ x"${GITHUB_ACTIONS}" = "xtrue" ]; then
         echo "::endgroup::"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/codecov.yml 
new/smbprotocol-1.17.0/codecov.yml
--- old/smbprotocol-1.16.1/codecov.yml  1970-01-01 01:00:00.000000000 +0100
+++ new/smbprotocol-1.17.0/codecov.yml  2026-07-07 05:14:30.000000000 +0200
@@ -0,0 +1,5 @@
+coverage:
+  status:
+    project:
+      default:
+        informational: true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/pyproject.toml 
new/smbprotocol-1.17.0/pyproject.toml
--- old/smbprotocol-1.16.1/pyproject.toml       2026-04-02 04:49:28.000000000 
+0200
+++ new/smbprotocol-1.17.0/pyproject.toml       2026-07-07 05:14:30.000000000 
+0200
@@ -6,10 +6,10 @@
 
 [project]
 name = "smbprotocol"
-version = "1.16.1"
+version = "1.17.0"
 description = "Interact with a server using the SMB 2/3 Protocol"
 readme = "README.md"
-requires-python = ">=3.9"
+requires-python = ">=3.10"
 license = "MIT"
 authors = [
     { name = "Jordan Borean", email = "[email protected]" }
@@ -18,7 +18,6 @@
 classifiers = [
     "Development Status :: 4 - Beta",
     "Programming Language :: Python :: 3",
-    "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
@@ -36,6 +35,22 @@
 [project.urls]
 homepage = "https://github.com/jborean93/smbprotocol";
 
+[dependency-groups]
+test = [
+    "pytest",
+    "pytest-cov",
+    "pytest-mock",
+]
+lint = [
+    "black == 26.3.1",
+    "isort == 8.0.1",
+    "pre-commit",
+]
+dev = [
+    {include-group = "test"},
+    {include-group = "lint"},
+]
+
 [tool.setuptools.packages.find]
 where = ["src"]
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/requirements-dev.txt 
new/smbprotocol-1.17.0/requirements-dev.txt
--- old/smbprotocol-1.16.1/requirements-dev.txt 2026-04-02 04:49:28.000000000 
+0200
+++ new/smbprotocol-1.17.0/requirements-dev.txt 1970-01-01 01:00:00.000000000 
+0100
@@ -1,9 +0,0 @@
-black == 25.9.0
-build
-cryptography >= 2.0
-isort == 6.1.0
-pre-commit
-pyspnego
-pytest
-pytest-cov
-pytest-mock
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/src/smbclient/__init__.py 
new/smbprotocol-1.17.0/src/smbclient/__init__.py
--- old/smbprotocol-1.16.1/src/smbclient/__init__.py    2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/src/smbclient/__init__.py    2026-07-07 
05:14:30.000000000 +0200
@@ -8,6 +8,7 @@
     XATTR_REPLACE,
     SMBDirEntry,
     SMBDirEntryInformation,
+    SMBScandirIterator,
     SMBStatResult,
     SMBStatVolumeResult,
     copyfile,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/src/smbclient/_io.py 
new/smbprotocol-1.17.0/src/smbclient/_io.py
--- old/smbprotocol-1.16.1/src/smbclient/_io.py 2026-04-02 04:49:28.000000000 
+0200
+++ new/smbprotocol-1.17.0/src/smbclient/_io.py 2026-07-07 05:14:30.000000000 
+0200
@@ -3,11 +3,13 @@
 
 import io
 import logging
+import warnings
 
 from smbclient._pool import ClientConfig, dfs_request, get_smb_tree
 from smbprotocol import MAX_PAYLOAD_SIZE
 from smbprotocol.exceptions import (
     NoMoreFiles,
+    NoSuchFile,
     ObjectNameNotFound,
     ObjectPathNotFound,
     PathNotCovered,
@@ -159,6 +161,40 @@
         yield Open(tree, fd_path)
 
 
+def _next_dfs_referral(raw_io, attempted_paths):
+    """
+    Walk DFS referrals for ``raw_io`` and return the next candidate ``Open`` 
whose
+    (share_name, file_name) hasn't been attempted yet. Returns ``None`` if all
+    referrals are exhausted, letting the caller re-raise the original DFS 
error.
+
+    Used by both :class:`SMBFileTransaction` (open/CreateRequest path) and
+    :meth:`SMBDirectoryIO.query_directory` (already-open enumeration path) so
+    they share a single source of truth for DFS retry selection. Each caller
+    is still responsible for swapping the underlying fd after a candidate is
+    returned because that step differs (transaction mutates ``_actions`` while
+    the directory IO closes/reopens the fd).
+
+    :param raw_io: The SMBRawIO whose path returned STATUS_PATH_NOT_COVERED or
+        a generic name/path-not-found that's actually DFS.
+    :param attempted_paths: Mutable set used to dedupe referral targets across
+        multiple retries within a single operation. See
+        https://github.com/jborean93/smbprotocol/issues/228.
+    :return: A new ``Open`` instance for the caller to use, or ``None``.
+    """
+    for smb_open in _resolve_dfs(raw_io):
+        if smb_open.tree_connect.share_name == 
raw_io.fd.tree_connect.share_name:
+            continue
+
+        tested_path = 
f"{smb_open.tree_connect.share_name}{smb_open.file_name}".lower()
+        if tested_path in attempted_paths:
+            continue
+
+        attempted_paths.add(tested_path)
+        return smb_open
+
+    return None
+
+
 def ioctl_request(transaction, ctl_code, output_size=0, 
flags=IOCTLFlags.SMB2_0_IOCTL_IS_IOCTL, input_buffer=b""):
     """
     Sends an IOCTL request to the server.
@@ -303,32 +339,18 @@
                     if not (idx == 0 and isinstance(send_msgs[0], 
SMB2CreateRequest)):
                         raise
 
-                    for smb_open in _resolve_dfs(self.raw):
-                        if smb_open.tree_connect.share_name == 
self.raw.fd.tree_connect.share_name:
-                            continue
-
-                        # Ensure we don't continuously try the same DFS 
referral targets if it's already been attempted.
-                        # https://github.com/jborean93/smbprotocol/issues/228
-                        tested_path = 
f"{smb_open.tree_connect.share_name}{smb_open.file_name}".lower()
-                        if tested_path in self._attempted_dfs_paths:
-                            continue
-
-                        self._attempted_dfs_paths.add(tested_path)
-
-                        self.raw.fd = smb_open
-
-                        # In case this is a transaction with an explicit open 
we want to reopen it with the new params
-                        # before trying it again.
-                        self.raw.open(transaction=self)
-                        self._actions[0] = self._actions.pop(-1)
-
-                        try_again = True
-                        break
-
-                    else:
-                        # Either there wasn't any DFS referrals or none of 
them worked, just reraise the error.
+                    smb_open = _next_dfs_referral(self.raw, 
self._attempted_dfs_paths)
+                    if smb_open is None:
+                        # No DFS referrals or all of them already attempted; 
propagate the error.
                         raise
 
+                    self.raw.fd = smb_open
+                    # In case this is a transaction with an explicit open we 
want to reopen it with the new params
+                    # before trying it again.
+                    self.raw.open(transaction=self)
+                    self._actions[0] = self._actions.pop(-1)
+                    try_again = True
+
             except SMBResponseException as exc:
                 failures.append(SMBOSError(exc.status, self.raw.name))
 
@@ -359,7 +381,11 @@
     def __init__(
         self, path, mode="r", share_access=None, desired_access=None, 
file_attributes=None, create_options=0, **kwargs
     ):
-        tree, fd_path = get_smb_tree(path, **kwargs)
+        # get_smb_tree runs outside the SMBFileTransaction layer that normally 
translates these.
+        try:
+            tree, fd_path = get_smb_tree(path, **kwargs)
+        except SMBResponseException as exc:
+            raise SMBOSError(exc.status, path) from exc
 
         self.share_access = share_access
         self.fd = Open(tree, fd_path)
@@ -404,6 +430,17 @@
 
         super().__init__()
 
+    def __del__(self):
+        # io.IOBase.__del__ would call self.close(), which self-deadlocks if GC
+        # runs it on the SMB worker thread.
+        try:
+            closed = self.closed
+        except AttributeError:
+            return
+        if closed:
+            return
+        warnings.warn(f"unclosed SMB handle {self._name!r}", ResourceWarning, 
source=self)
+
     def __enter__(self):
         self.open()
         return self
@@ -615,11 +652,31 @@
 
     def query_directory(self, pattern, info_class):
         query_flags = QueryDirectoryFlags.SMB2_RESTART_SCANS
+        attempted_dfs_paths = set()
         while True:
             try:
                 entries = self.fd.query_directory(pattern, info_class, 
flags=query_flags)
-            except NoMoreFiles:
+            except (NoMoreFiles, NoSuchFile):
+                # MS-SMB2 3.3.5.18 lists both as QUERY_DIRECTORY end codes. A 
server
+                # returns STATUS_NO_SUCH_FILE when the search pattern matches 
no files.
+                # Treat either as end of enumeration.
                 break
+            except (PathNotCovered, ObjectNameNotFound, ObjectPathNotFound):
+                # The MS-DFSC docs state that STATUS_PATH_NOT_COVERED is used 
when encountering a DFS link to a
+                # different server during directory enumeration. Samba may 
return the generic name or path not found.
+                smb_open = _next_dfs_referral(self, attempted_dfs_paths)
+                if smb_open is None:
+                    # No DFS referral resolved; propagate the error.
+                    raise
+
+                try:
+                    self.fd.close()
+                except SMBResponseException:
+                    pass
+                self.fd = smb_open
+                self.open()
+                query_flags = QueryDirectoryFlags.SMB2_RESTART_SCANS
+                continue
 
             query_flags = 0  # Only the first request should have set 
SMB2_RESTART_SCANS
             yield from entries
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/src/smbclient/_os.py 
new/smbprotocol-1.17.0/src/smbclient/_os.py
--- old/smbprotocol-1.16.1/src/smbclient/_os.py 2026-04-02 04:49:28.000000000 
+0200
+++ new/smbprotocol-1.17.0/src/smbclient/_os.py 2026-07-07 05:14:30.000000000 
+0200
@@ -666,11 +666,46 @@
     _delete(SMBDirectoryIO, path, **kwargs)
 
 
-def scandir(path, search_pattern="*", **kwargs):
+class SMBScandirIterator:
+    """Iterator over SMB directory entries with ``with``-driven close.
+
+    Iterable directly and usable as a context manager whose exit releases
+    the SMB directory handle.
+    """
+
+    __slots__ = ("_gen",)
+
+    def __init__(self, gen: t.Generator[SMBDirEntry, None, None]) -> None:
+        self._gen = gen
+
+    def __iter__(self) -> SMBScandirIterator:
+        return self
+
+    def __next__(self) -> SMBDirEntry:
+        return next(self._gen)
+
+    def __enter__(self) -> SMBScandirIterator:
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb) -> None:
+        self.close()
+
+    def close(self) -> None:
+        self._gen.close()
+
+
+def scandir(path: str, search_pattern: str = "*", **kwargs: t.Any) -> 
SMBScandirIterator:
     """
     Return an iterator of DirEntry objects corresponding to the entries in the 
directory given by path. The entries are
     yielded in arbitrary order, and the special entries '.' and '..' are not 
included.
 
+    Mirrors stdlib ``os.scandir()``: the returned iterator also supports the 
context-manager protocol so callers can
+    release the SMB directory handle deterministically:
+
+        with smbclient.scandir(path) as it:
+            for entry in it:
+                ...
+
     Using scandir() instead of listdir() can significantly increase the 
performance of code that also needs file type
     or file attribute information, because DirEntry objects expose this 
information if the SMB server provides it when
     scanning a directory. All DirEntry methods may perform a SMB request, but 
is_dir(), is_file(), is_symlink() usually
@@ -678,11 +713,15 @@
     Python documentation for how DirEntry is set up and the methods and 
attributes that are available.
 
     :param path: The path to a directory to scan.
-    :param search_pattern: THe search string to match against the names of 
directories or files. This pattern can use
+    :param search_pattern: The search string to match against the names of 
directories or files. This pattern can use
     '*' as a wildcard for multiple chars and '?' as a wildcard for a single 
char. Does not support regex patterns.
     :param kwargs: Common SMB Session arguments for smbclient.
-    :return: An iterator of DirEntry objects in the directory.
+    :return: A context-manager iterator of DirEntry objects in the directory.
     """
+    return SMBScandirIterator(_scandir(path, search_pattern, **kwargs))
+
+
+def _scandir(path: str, search_pattern: str = "*", **kwargs: t.Any) -> 
t.Generator[SMBDirEntry, None, None]:
     connection_cache = kwargs.get("connection_cache", None)
     with SMBDirectoryIO(path, share_access="rwd", **kwargs) as fd:
         for raw_dir_info in fd.query_directory(search_pattern, 
FileInformationClass.FILE_ID_FULL_DIRECTORY_INFORMATION):
@@ -1023,26 +1062,27 @@
     dirs = []
     files = []
     bottom_up_dirs = []
-    while True:
-        try:
+    with scandir_gen:
+        while True:
             try:
-                entry = next(scandir_gen)
-            except StopIteration:
-                break
-        except OSError as err:
-            if onerror is not None:
-                onerror(err)
-            return
-
-        if not entry.is_dir():
-            files.append(entry.name)
-            continue
-
-        dirs.append(entry.name)
-        if not topdown and (follow_symlinks or not entry.is_symlink()):
-            # Add the directory to the bottom up list which is recursively 
walked below, we exclude symlink dirs if
-            # follow_symlinks is False.
-            bottom_up_dirs.append(entry.path)
+                try:
+                    entry = next(scandir_gen)
+                except StopIteration:
+                    break
+            except OSError as err:
+                if onerror is not None:
+                    onerror(err)
+                return
+
+            if not entry.is_dir():
+                files.append(entry.name)
+                continue
+
+            dirs.append(entry.name)
+            if not topdown and (follow_symlinks or not entry.is_symlink()):
+                # Add the directory to the bottom up list which is recursively 
walked below, we exclude symlink dirs
+                # if follow_symlinks is False.
+                bottom_up_dirs.append(entry.path)
 
     walk_kwargs = {"topdown": topdown, "onerror": onerror, "follow_symlinks": 
follow_symlinks}
     walk_kwargs.update(kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/src/smbclient/shutil.py 
new/smbprotocol-1.17.0/src/smbclient/shutil.py
--- old/smbprotocol-1.16.1/src/smbclient/shutil.py      2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/src/smbclient/shutil.py      2026-07-07 
05:14:30.000000000 +0200
@@ -310,7 +310,8 @@
     :return: The dst path.
     """
     if is_remote_path(src):
-        dir_entries = list(scandir(src, **kwargs))
+        with scandir(src, **kwargs) as scandir_gen:
+            dir_entries = list(scandir_gen)
     else:
         dir_entries = list(os.scandir(src))
 
@@ -411,41 +412,42 @@
         def onerror(*args):
             raise
 
-    if islink(path, **kwargs):
-        try:
+    # islink issues SMB requests, so route failures through onerror.
+    try:
+        if islink(path, **kwargs):
             raise OSError("Cannot call rmtree on a symbolic link")
-        except OSError:
-            onerror(islink, path, sys.exc_info())
-            return
-
-    scandir_gen = scandir(path, **kwargs)
-    while True:
-        try:
-            dir_entry = next(scandir_gen)
-        except StopIteration:
-            break
-        except OSError:
-            onerror(scandir, path, sys.exc_info())
-            continue
+    except OSError:
+        onerror(islink, path, sys.exc_info())
+        return
 
-        # In case the entry is a directory symbolic link we need to remove the 
dir itself and not recurse down into
-        # it with rmtree. Doing that would result in a symbolic link target 
having it's contents removed even if it's
-        # outside the rmtree scope.
-        if (
-            dir_entry.is_symlink()
-            and dir_entry.stat(follow_symlinks=False).st_file_attributes & 
FileAttributes.FILE_ATTRIBUTE_DIRECTORY
-        ):
+    with scandir(path, **kwargs) as scandir_gen:
+        while True:
             try:
-                rmdir(dir_entry.path, **kwargs)
+                dir_entry = next(scandir_gen)
+            except StopIteration:
+                break
             except OSError:
-                onerror(rmdir, dir_entry.path, sys.exc_info())
-        elif dir_entry.is_dir():
-            rmtree(dir_entry.path, ignore_errors, onerror, **kwargs)
-        else:
-            try:
-                remove(dir_entry.path, **kwargs)
-            except OSError:
-                onerror(remove, dir_entry.path, sys.exc_info())
+                onerror(scandir, path, sys.exc_info())
+                continue
+
+            # In case the entry is a directory symbolic link we need to remove 
the dir itself and not recurse down into
+            # it with rmtree. Doing that would result in a symbolic link 
target having it's contents removed even if
+            # it's outside the rmtree scope.
+            if (
+                dir_entry.is_symlink()
+                and dir_entry.stat(follow_symlinks=False).st_file_attributes & 
FileAttributes.FILE_ATTRIBUTE_DIRECTORY
+            ):
+                try:
+                    rmdir(dir_entry.path, **kwargs)
+                except OSError:
+                    onerror(rmdir, dir_entry.path, sys.exc_info())
+            elif dir_entry.is_dir():
+                rmtree(dir_entry.path, ignore_errors, onerror, **kwargs)
+            else:
+                try:
+                    remove(dir_entry.path, **kwargs)
+                except OSError:
+                    onerror(remove, dir_entry.path, sys.exc_info())
 
     try:
         rmdir(path, **kwargs)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/src/smbprotocol/connection.py 
new/smbprotocol-1.17.0/src/smbprotocol/connection.py
--- old/smbprotocol-1.16.1/src/smbprotocol/connection.py        2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/src/smbprotocol/connection.py        2026-07-07 
05:14:30.000000000 +0200
@@ -921,6 +921,7 @@
             SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED
         ):
             self.require_signing = True
+            self.client_security_mode = 
SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED
         log.info("Connection require signing: %s", self.require_signing)
 
         capabilities = smb_response["capabilities"]
@@ -1032,6 +1033,12 @@
         :param resolve_symlinks: Set to automatically resolve symlinks in the 
path when opening a file or directory.
         :return: SMB2HeaderResponse of the received message
         """
+        # Only the worker thread delivers responses, so calling receive() from 
it would wait
+        # on an event only it can set.
+        worker = self._t_worker
+        if worker is not None and threading.get_ident() == worker.ident:
+            raise SMBException("Connection.receive() called from the SMB 
worker thread, would self-deadlock")
+
         # Make sure the receiver is still active, if not this raises an 
exception.
         self._check_worker_running()
 
@@ -1431,7 +1438,9 @@
                         if request.message["reserved"].get_value() == 1:
                             self.outstanding_requests.pop(message_id, None)
         except Exception as exc:
-            # The exception is raised in _check_worker_running by the main 
thread when send/receive is called next.
+            log.exception("SMB receive worker died (outstanding=%d)", 
len(self.outstanding_requests))
+
+            # The exception is raised in _check_worker_running by the caller 
thread when send/receive is called next.
             self._t_exc = exc
 
             # While a caller of send/receive could theoretically catch this 
exception, we consider any failures
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/src/smbprotocol/open.py 
new/smbprotocol-1.17.0/src/smbprotocol/open.py
--- old/smbprotocol-1.16.1/src/smbprotocol/open.py      2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/src/smbprotocol/open.py      2026-07-07 
05:14:30.000000000 +0200
@@ -230,6 +230,7 @@
     """
 
     SMB2_READFLAG_READ_UNBUFFERED = 0x01
+    SMB2_READFLAG_REQUEST_COMPRESSED = 0x02
 
 
 class ReadWriteChannel:
@@ -1102,6 +1103,10 @@
 
 
 class Open:
+    # MS-SMB2 3.2.4.1.4 ChainedFileId: 0xFF * 16 signals "use the Open
+    # from the most recent CREATE in this related-compound chain".
+    _RELATED_COMPOUND_FILE_ID = b"\xff" * 16
+
     def __init__(self, tree, name):
         """
         [MS-SMB2] v53.0 2017-09-15
@@ -1125,10 +1130,7 @@
         self.file_attributes = None
 
         # properties used privately
-        # set to { 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF } to allow message
-        # compounding with the open as the first message, once opened this
-        # will be overwritten by the open response
-        self.file_id = b"\xff" * 16
+        self.file_id = self._RELATED_COMPOUND_FILE_ID
         self.tree_connect = tree
         self.connection = tree.session.connection
         self.oplock_level = None
@@ -1595,28 +1597,29 @@
         try:
             response = self.connection.receive(request)
         except FileClosed:
+            # already closed server-side
+            return
+        else:
+            c_resp = SMB2CloseResponse()
+            c_resp.unpack(response["data"].get_value())
+            log.debug(c_resp)
+
+            # update the attributes if requested
+            close_request = SMB2CloseRequest()
+            close_request.unpack(request.message["data"].get_value())
+            if 
close_request["flags"].has_flag(CloseFlags.SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB):
+                self.creation_time = c_resp["creation_time"].get_value()
+                self.last_access_time = c_resp["last_access_time"].get_value()
+                self.last_write_time = c_resp["last_write_time"].get_value()
+                self.change_time = c_resp["change_time"].get_value()
+                self.allocation_size = c_resp["allocation_size"].get_value()
+                self.end_of_file = c_resp["end_of_file"].get_value()
+                self.file_attributes = c_resp["file_attributes"].get_value()
+            return c_resp
+        finally:
+            # file_id may be unregistered (unsent compound) or already removed 
by a prior close
             self._connected = False
             self.tree_connect.session.open_table.pop(self.file_id, None)
-            return
-
-        c_resp = SMB2CloseResponse()
-        c_resp.unpack(response["data"].get_value())
-        log.debug(c_resp)
-        self._connected = False
-        del self.tree_connect.session.open_table[self.file_id]
-
-        # update the attributes if requested
-        close_request = SMB2CloseRequest()
-        close_request.unpack(request.message["data"].get_value())
-        if 
close_request["flags"].has_flag(CloseFlags.SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB):
-            self.creation_time = c_resp["creation_time"].get_value()
-            self.last_access_time = c_resp["last_access_time"].get_value()
-            self.last_write_time = c_resp["last_write_time"].get_value()
-            self.change_time = c_resp["change_time"].get_value()
-            self.allocation_size = c_resp["allocation_size"].get_value()
-            self.end_of_file = c_resp["end_of_file"].get_value()
-            self.file_attributes = c_resp["file_attributes"].get_value()
-        return c_resp
 
     def lock(self, locks, lsn=0, lsi=0, wait=True, send=True):
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/src/smbprotocol/session.py 
new/smbprotocol-1.17.0/src/smbprotocol/session.py
--- old/smbprotocol-1.16.1/src/smbprotocol/session.py   2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/src/smbprotocol/session.py   2026-07-07 
05:14:30.000000000 +0200
@@ -3,7 +3,6 @@
 
 import hashlib
 import logging
-import random
 from collections import OrderedDict
 from typing import Literal, Optional
 
@@ -177,6 +176,7 @@
         username: Optional[str] = None,
         password: Optional[str] = None,
         require_encryption=True,
+        hostname_override=None,
         auth_protocol: Literal["negotiate", "ntlm", "kerberos"] = "negotiate",
     ):
         """
@@ -219,6 +219,8 @@
         :param require_encryption: Whether any messages sent over the session
             require encryption regardless of the server settings (Dialects 3+),
             needs to be set to False for older dialects.
+        :param hostname_override: Specifies hostname context for Kerberos
+            authentication when given IP address to the connection context.
         :param auth_protocol: The protocol to use for authentication. Possible
             values are 'negotiate', 'ntlm' or 'kerberos'. Defaults to
             'negotiate'.
@@ -240,6 +242,7 @@
         self.connection = connection
         self.username = username
         self.password = password
+        self._hostname_override = hostname_override
 
         # No need to validate this as the spnego library will raise a 
ValueError
         self.auth_protocol = auth_protocol
@@ -274,7 +277,7 @@
                 self.username,
                 self.password,
                 service="cifs",
-                hostname=self.connection.server_name,
+                hostname=self._hostname_override or 
self.connection.server_name,
                 options=spnego.NegotiateOptions.session_key,
                 protocol=self.auth_protocol,
             )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/src/smbprotocol/structure.py 
new/smbprotocol-1.17.0/src/smbprotocol/structure.py
--- old/smbprotocol-1.16.1/src/smbprotocol/structure.py 2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/src/smbprotocol/structure.py 2026-07-07 
05:14:30.000000000 +0200
@@ -797,7 +797,8 @@
             return "0"
         flags = []
         for flag, value in vars(self.flag_type).items():
-            if isinstance(value, int) and self.has_flag(value):
+            # ignore and dunder __foo__ attributes.
+            if not flag.startswith("__") and isinstance(value, int) and 
self.has_flag(value):
                 flags.append(flag)
         flags.sort()
         return f"({field_value}) " + (", ".join(flags))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/src/smbprotocol/transport.py 
new/smbprotocol-1.17.0/src/smbprotocol/transport.py
--- old/smbprotocol-1.16.1/src/smbprotocol/transport.py 2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/src/smbprotocol/transport.py 2026-07-07 
05:14:30.000000000 +0200
@@ -135,10 +135,10 @@
                     return None, timeout  # pragma: no cover
 
                 read = select.select([self._sock], [], [], max(timeout, 1))[0]
-                timeout = timeout - (timeit.default_timer() - start_time)
+                elapsed = timeit.default_timer() - start_time
+                timeout = timeout - elapsed
                 if not read:
-                    log.debug("Socket recv(%s) timed out")
-                    raise TimeoutError()
+                    raise TimeoutError(f"recv idle for {elapsed:.1f}s 
({offset}/{length} bytes)")
 
                 try:
                     b_data = self._sock.recv(read_len)
@@ -148,6 +148,10 @@
                     if e.errno not in [errno.ESHUTDOWN, errno.ECONNRESET, 
errno.ECONNABORTED]:
                         # Avoid collecting coverage here to avoid CI failing 
due to race condition differences
                         raise  # pragma: no cover
+                    log.warning(
+                        "socket aborted by peer (%s), treating as EOF",
+                        errno.errorcode.get(e.errno, e.errno),
+                    )
                     b_data = b""
 
             read_len = len(b_data)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/src/smbprotocol/tree.py 
new/smbprotocol-1.17.0/src/smbprotocol/tree.py
--- old/smbprotocol-1.16.1/src/smbprotocol/tree.py      2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/src/smbprotocol/tree.py      2026-07-07 
05:14:30.000000000 +0200
@@ -74,6 +74,8 @@
     SMB2_SHAREFLAG_ENABLE_HASH_V2 = 0x00004000
     SMB2_SHAREFLAG_ENCRYPT_DATA = 0x00008000
     SMB2_SHAREFLAG_IDENTITY_REMOTING = 0x00040000
+    SMB2_SHAREFLAG_COMPRESS_DATA = 0x00100000
+    SMB2_SHAREFLAG_ISOLATED_TRANSPORT = 0x00200000
 
 
 class ShareCapabilities:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/tests/conftest.py 
new/smbprotocol-1.17.0/tests/conftest.py
--- old/smbprotocol-1.16.1/tests/conftest.py    2026-04-02 04:49:28.000000000 
+0200
+++ new/smbprotocol-1.17.0/tests/conftest.py    2026-07-07 05:14:30.000000000 
+0200
@@ -154,6 +154,32 @@
 )
 
 
+class StubScandirGen:
+    """Server-free stand-in for the _scandir() generator: injects a
+    mid-iteration failure and records close() calls, so the caller
+    close-on-exception tests need no SMB server."""
+
+    def __init__(self, closes, next_exc=None):
+        self._closes = closes
+        self._next_exc = next_exc
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        if self._next_exc is not None:
+            raise self._next_exc
+        raise StopIteration
+
+    def close(self):
+        self._closes.append(True)
+
+
[email protected]
+def stub_scandir_gen():
+    return StubScandirGen
+
+
 @pytest.fixture(scope="module")
 def smb_real():
     # for these tests to work the server at SMB_SERVER must support dialect
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/tests/test_connection.py 
new/smbprotocol-1.17.0/tests/test_connection.py
--- old/smbprotocol-1.16.1/tests/test_connection.py     2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/tests/test_connection.py     2026-07-07 
05:14:30.000000000 +0200
@@ -987,7 +987,11 @@
             assert len(connection.salt) == 32
             assert connection.sequence_window["low"] == 1
             assert connection.sequence_window["high"] == 2
-            assert connection.client_security_mode == 
SecurityMode.SMB2_NEGOTIATE_SIGNING_ENABLED
+            assert (
+                connection.client_security_mode == 
SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED
+                if connection.server_security_mode & 
SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED
+                else SecurityMode.SMB2_NEGOTIATE_SIGNING_ENABLED
+            )
             assert connection.supports_encryption
 
             if connection.server_security_mode & 
SecurityMode.SMB2_NEGOTIATE_SIGNING_REQUIRED:
@@ -1111,6 +1115,16 @@
         with pytest.raises(SMBConnectionClosed):
             connection.receive(None)
 
+    def test_receive_from_worker_thread_raises(self, mocker):
+        # receive() on the worker thread would wait for an event only the 
worker can set.
+        connection = Connection(uuid.uuid4(), "server", 445, True)
+        fake_worker = mocker.MagicMock()
+        fake_worker.ident = threading.get_ident()
+        connection._t_worker = fake_worker
+
+        with pytest.raises(SMBException, match="self-deadlock"):
+            connection.receive(None)
+
     def test_verify_fail_no_session(self, smb_real):
         connection = Connection(uuid.uuid4(), smb_real[2], smb_real[3], True)
         connection.connect()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/tests/test_open.py 
new/smbprotocol-1.17.0/tests/test_open.py
--- old/smbprotocol-1.16.1/tests/test_open.py   2026-04-02 04:49:28.000000000 
+0200
+++ new/smbprotocol-1.17.0/tests/test_open.py   2026-07-07 05:14:30.000000000 
+0200
@@ -23,7 +23,14 @@
     SMB2CreateResponseLeaseV2,
     SMB2CreateTimewarpToken,
 )
-from smbprotocol.exceptions import EndOfFile, SMBException, 
SMBUnsupportedFeature
+from smbprotocol.exceptions import (
+    EndOfFile,
+    FileClosed,
+    ObjectNameNotFound,
+    SMBException,
+    SMBResponseException,
+    SMBUnsupportedFeature,
+)
 from smbprotocol.file_info import (
     FileAttributes,
     FileEndOfFileInformation,
@@ -71,6 +78,16 @@
 from smbprotocol.tree import TreeConnect
 
 
[email protected]
+def closing_open(mocker):
+    """Open-shaped mock with the state `_close_response` mutates."""
+    open_obj = mocker.MagicMock()
+    open_obj._connected = True
+    open_obj.file_id = mocker.sentinel.file_id
+    open_obj.tree_connect.session.open_table = {open_obj.file_id: open_obj}
+    return open_obj
+
+
 class TestSMB2CreateRequest:
     def test_create_message(self):
         timewarp_token = SMB2CreateTimewarpToken()
@@ -2534,3 +2551,47 @@
 
         finally:
             connection.disconnect(True)
+
+    def test_close_response_marks_disconnected_on_receive_failure(self, 
closing_open, mocker):
+        closing_open.connection.receive.side_effect = ObjectNameNotFound()
+
+        with pytest.raises(SMBResponseException):
+            Open._close_response(closing_open, mocker.MagicMock())
+
+        assert not closing_open._connected
+        assert not closing_open.tree_connect.session.open_table
+
+    def test_close_response_marks_disconnected_on_file_closed(self, 
closing_open, mocker):
+        closing_open.connection.receive.side_effect = FileClosed()
+
+        assert Open._close_response(closing_open, mocker.MagicMock()) is None
+        assert not closing_open._connected
+        assert not closing_open.tree_connect.session.open_table
+
+    def test_close_response_marks_disconnected_on_success(self, closing_open, 
mocker):
+        closing_open.connection.receive.return_value = mocker.MagicMock()
+        mocker.patch("smbprotocol.open.SMB2CloseResponse")
+        close_req_cls = mocker.patch("smbprotocol.open.SMB2CloseRequest")
+        
close_req_cls.return_value.__getitem__.return_value.has_flag.return_value = 
False
+
+        Open._close_response(closing_open, mocker.MagicMock())
+
+        assert not closing_open._connected
+        assert not closing_open.tree_connect.session.open_table
+
+    def 
test_close_response_tolerates_already_popped_entry_on_receive_failure(self, 
closing_open, mocker):
+        closing_open.tree_connect.session.open_table.clear()
+        closing_open.connection.receive.side_effect = ObjectNameNotFound()
+
+        with pytest.raises(SMBResponseException):
+            Open._close_response(closing_open, mocker.MagicMock())
+
+        assert not closing_open._connected
+
+    def 
test_close_response_tolerates_already_popped_entry_on_file_closed(self, 
closing_open, mocker):
+        closing_open.tree_connect.session.open_table.clear()
+        closing_open.connection.receive.side_effect = FileClosed()
+
+        Open._close_response(closing_open, mocker.MagicMock())
+
+        assert not closing_open._connected
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/tests/test_smbclient_io.py 
new/smbprotocol-1.17.0/tests/test_smbclient_io.py
--- old/smbprotocol-1.16.1/tests/test_smbclient_io.py   1970-01-01 
01:00:00.000000000 +0100
+++ new/smbprotocol-1.17.0/tests/test_smbclient_io.py   2026-07-07 
05:14:30.000000000 +0200
@@ -0,0 +1,41 @@
+# Copyright: (c) 2026, Acconeer AB <[email protected]>
+# MIT License (see LICENSE or https://opensource.org/licenses/MIT)
+
+import warnings
+
+import pytest
+
+import smbclient._io as io
+from smbclient._io import SMBRawIO
+
+
[email protected]
+def raw(mocker):
+    """Construct an SMBRawIO without opening any socket.
+
+    SMBRawIO.__init__ normally calls get_smb_tree which establishes a 
connection.
+    Patching that and Open lets the finalizer be exercised deterministically.
+    """
+    mock_tree = mocker.MagicMock()
+    mocker.patch.object(io, "get_smb_tree", return_value=(mock_tree, "file"))
+    mocker.patch.object(io, "Open")
+
+    return SMBRawIO(r"\\server\share\file.txt", mode="r", share_access="r")
+
+
+def test_del_does_not_close_a_connected_handle(raw):
+    # The override must skip self.close() to avoid deadlocking the worker.
+    raw.fd.connected = True
+
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", ResourceWarning)
+        raw.__del__()
+
+    raw.fd.close.assert_not_called()
+
+
+def test_del_emits_resource_warning_for_leaked_handle(raw):
+    raw.fd.connected = True
+
+    with pytest.warns(ResourceWarning, match=r"unclosed SMB handle"):
+        raw.__del__()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/tests/test_smbclient_os.py 
new/smbprotocol-1.17.0/tests/test_smbclient_os.py
--- old/smbprotocol-1.16.1/tests/test_smbclient_os.py   2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/tests/test_smbclient_os.py   2026-07-07 
05:14:30.000000000 +0200
@@ -1292,6 +1292,52 @@
     assert names == ["file-test1.txt"]
 
 
+def test_scandir_empty_directory(smb_share):
+    # Some servers return STATUS_NO_SUCH_FILE here instead of 
STATUS_NO_MORE_FILES.
+    empty_dir = rf"{smb_share}\empty"
+    smbclient.mkdir(empty_dir)
+
+    assert list(smbclient.scandir(empty_dir)) == []
+
+
+def test_scandir_with_non_matching_pattern(smb_share):
+    # Samba returns STATUS_NO_SUCH_FILE here instead of STATUS_NO_MORE_FILES.
+    with smbclient.open_file(rf"{smb_share}\file.txt", mode="w") as fd:
+        fd.write("x")
+
+    assert list(smbclient.scandir(smb_share, search_pattern="nomatch_*")) == []
+
+
+def test_scandir_as_context_manager(smb_share):
+    for filename in ["file1.txt", "file2.txt"]:
+        with smbclient.open_file(rf"{smb_share}\{filename}", mode="w") as fd:
+            fd.write("content")
+
+    # Full iteration inside the context manager yields every entry.
+    with smbclient.scandir(smb_share) as scandir_gen:
+        assert isinstance(scandir_gen, smbclient.SMBScandirIterator)
+        assert sorted(entry.name for entry in scandir_gen) == ["file1.txt", 
"file2.txt"]
+
+    # Abandoning iteration early still releases the handle on block exit: the
+    # iterator is finalised, so resuming it stops instead of yielding the rest.
+    it = smbclient.scandir(smb_share)
+    with it:
+        assert next(it).name in ("file1.txt", "file2.txt")
+    with pytest.raises(StopIteration):
+        next(it)
+
+
+def test_scandir_iterator_contract():
+    it = smbclient.SMBScandirIterator(x for x in ["a", "b"])
+    with it as entered:
+        assert entered is it
+        assert next(it) == "a"
+
+    # __exit__ closed the underlying generator, so iteration is exhausted.
+    with pytest.raises(StopIteration):
+        next(it)
+
+
 @pytest.mark.skipif(
     os.name != "nt" and not os.environ.get("SMB_FORCE", False), reason="cannot 
create symlinks on Samba"
 )
@@ -1981,6 +2027,19 @@
     assert scanned_roots[src_dirname]["files"] == ["file.txt"]
 
 
+def test_walk_closes_scandir_iterator_on_unhandled_exception(monkeypatch, 
stub_scandir_gen):
+    closes = []
+    monkeypatch.setattr(
+        "smbclient._os._scandir",
+        lambda *a, **kw: stub_scandir_gen(closes, RuntimeError("simulated 
mid-iter failure")),
+    )
+
+    with pytest.raises(RuntimeError, match="simulated mid-iter failure"):
+        list(smbclient.walk(r"\\server\share\dir"))
+
+    assert closes == [True]
+
+
 def test_xattr_file(smb_share):
     filename = "%s\\file.txt" % smb_share
 
@@ -2170,3 +2229,14 @@
 
     with pytest.raises(SMBOSError):
         smbclient.lstat(fake_file)
+
+
+def test_open_file_missing_share_raises_os_error(smb_real):
+    # A missing share fails tree connect with STATUS_BAD_NETWORK_NAME, a 
non-OSError
+    # that must surface as SMBOSError so except OSError / ignore_errors 
callers catch it.
+    bad_path = rf"\\{smb_real[2]}\does_not_exist\file.txt"
+
+    with pytest.raises(SMBOSError) as exc_info:
+        smbclient.open_file(bad_path, username=smb_real[0], 
password=smb_real[1], port=smb_real[3])
+
+    assert exc_info.value.filename == bad_path
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/tests/test_smbclient_pool.py 
new/smbprotocol-1.17.0/tests/test_smbclient_pool.py
--- old/smbprotocol-1.16.1/tests/test_smbclient_pool.py 2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/tests/test_smbclient_pool.py 2026-07-07 
05:14:30.000000000 +0200
@@ -8,7 +8,13 @@
 import smbclient._io as io
 import smbclient._pool as pool
 from smbprotocol.dfs import DFSReferralResponse
-from smbprotocol.exceptions import BadNetworkName, InvalidParameter, 
ObjectPathNotFound
+from smbprotocol.exceptions import (
+    BadNetworkName,
+    InvalidParameter,
+    ObjectPathNotFound,
+    SMBOSError,
+)
+from smbprotocol.header import NtStatus
 
 from .conftest import DC_REFERRAL, DOMAIN_NAME, DOMAIN_REFERRAL
 
@@ -150,3 +156,16 @@
 
     with pytest.raises(ObjectPathNotFound):
         list(io._resolve_dfs(raw_io))
+
+
+def test_raw_io_translates_smb_response_to_os_error(mocker):
+    # Anything escaping get_smb_tree here must surface as OSError so the
+    # rmtree, remove, rmdir, and scandir error paths route it correctly.
+    mocker.patch.object(io, "get_smb_tree", side_effect=BadNetworkName())
+
+    path = r"\\server\share\missing"
+    with pytest.raises(SMBOSError) as exc_info:
+        io.SMBRawIO(path)
+
+    assert exc_info.value.ntstatus == NtStatus.STATUS_BAD_NETWORK_NAME
+    assert exc_info.value.filename == path
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/tests/test_smbclient_shutil.py 
new/smbprotocol-1.17.0/tests/test_smbclient_shutil.py
--- old/smbprotocol-1.16.1/tests/test_smbclient_shutil.py       2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/tests/test_smbclient_shutil.py       2026-07-07 
05:14:30.000000000 +0200
@@ -1583,3 +1583,54 @@
     assert callback_args[0][0].__name__ == "islink"
     assert callback_args[0][1] == dst_dirname
     assert isinstance(callback_args[0][2][1], OSError)
+
+
+def test_rmtree_islink_failure_invokes_onerror(mocker):
+    # islink issues SMB requests and can fail. Verify rmtree routes that 
failure
+    # through onerror so ignore_errors=True still suppresses it.
+    fake_path = r"\\server\share\dst"
+
+    def _failing_islink(*args, **kwargs):
+        raise SMBOSError(NtStatus.STATUS_DELETE_PENDING, fake_path)
+
+    _failing_islink.__name__ = "islink"
+    mocker.patch("smbclient.shutil.islink", new=_failing_islink)
+
+    # ignore_errors=True path: must not raise, must return cleanly.
+    rmtree(fake_path, ignore_errors=True)
+
+    # onerror path: callback receives (islink, path, exc_info).
+    callback_args = []
+    rmtree(fake_path, onerror=lambda *args: callback_args.append(args))
+
+    assert len(callback_args) == 1
+    assert callback_args[0][0].__name__ == "islink"
+    assert callback_args[0][1] == fake_path
+    assert isinstance(callback_args[0][2][1], SMBOSError)
+
+
+def test_rmtree_closes_scandir_iterator_on_unhandled_exception(monkeypatch, 
stub_scandir_gen):
+    closes = []
+    monkeypatch.setattr(
+        "smbclient._os._scandir",
+        lambda *a, **kw: stub_scandir_gen(closes, RuntimeError("simulated 
mid-iter failure")),
+    )
+    monkeypatch.setattr("smbclient.shutil.islink", lambda *a, **kw: False)
+
+    with pytest.raises(RuntimeError, match="simulated mid-iter failure"):
+        rmtree(r"\\server\share\dst")
+
+    assert closes == [True]
+
+
+def test_copytree_closes_scandir_iterator_on_unhandled_exception(monkeypatch, 
stub_scandir_gen):
+    closes = []
+    monkeypatch.setattr(
+        "smbclient._os._scandir",
+        lambda *a, **kw: stub_scandir_gen(closes, RuntimeError("simulated 
mid-iter failure")),
+    )
+
+    with pytest.raises(RuntimeError, match="simulated mid-iter failure"):
+        copytree(r"\\server\share\src", r"\\server\share\dst")
+
+    assert closes == [True]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/smbprotocol-1.16.1/tests/test_structure.py 
new/smbprotocol-1.17.0/tests/test_structure.py
--- old/smbprotocol-1.16.1/tests/test_structure.py      2026-04-02 
04:49:28.000000000 +0200
+++ new/smbprotocol-1.17.0/tests/test_structure.py      2026-07-07 
05:14:30.000000000 +0200
@@ -1647,6 +1647,21 @@
             field.set_value(0x00000082)
         assert str(exc.value) == "Invalid flag for field field value set 128"
 
+    def test_flag_str_ignores_dunder_attributes(self):
+        class MyClass:
+            __test__ = 3
+
+            FLAG1 = 1
+            FLAG2 = 2
+
+        expected = "(3) FLAG1, FLAG2"
+
+        field = FlagField(size=4, flag_type=MyClass)
+        field.set_value(3)
+        actual = str(field)
+
+        assert actual == expected
+
 
 class TestBoolField:
     class StructureTest(Structure):

Reply via email to