jenkins-bot has submitted this change. ( 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1354003?usp=email )

Change subject: family: Fix ZIP discovery and duplicate import paths
......................................................................

family: Fix ZIP discovery and duplicate import paths

Find parent ZIP archives before rejecting family folders as missing.
Preserve archive member paths and restrict subdirectory discovery to
the requested prefix so registered families can be imported.

Only append a family directory to sys.path when it is not already
present, avoiding duplicate entries when loading distinct families.

Change-Id: If0ec6843cd092071f6b8ddefd5c323d69f70eeb7
---
M pywikibot/config.py
M pywikibot/family.py
M tests/config_tests.py
3 files changed, 76 insertions(+), 12 deletions(-)

Approvals:
  jenkins-bot: Verified
  Xqt: Looks good to me, approved




diff --git a/pywikibot/config.py b/pywikibot/config.py
index 7e47bfa..c5e2f09 100644
--- a/pywikibot/config.py
+++ b/pywikibot/config.py
@@ -434,12 +434,6 @@
     suffix = '_family.py'

     folder = Path(folder_path)
-    if not folder.exists():
-        if not_exists_ok:
-            return
-        raise FileNotFoundError(
-            f'Family folder {folder_path!r} does not exist')
-
     if folder.is_dir():
         for file_path in folder.iterdir():
             if file_path.name.endswith(suffix):
@@ -452,20 +446,33 @@
     path = folder
     if not is_zipfile(path):
         for parent in path.parents:
-            if is_zipfile(path):
+            if is_zipfile(parent):
                 path = parent
                 break
         else:
+            if not folder.exists():
+                if not_exists_ok:
+                    return
+                raise FileNotFoundError(
+                    f'Family folder {folder_path!r} does not exist')
             raise NotADirectoryError('20', 'Not a directory', folder_path)

     # read the family files from zip folder
-    # assume that all files ending with suffix reside in family folder
-    zip_file = ZipFile(path)
-    for file_name in zip_file.namelist():
+    with ZipFile(path) as zip_file:
+        file_names = zip_file.namelist()
+    if folder != path:
+        prefix = folder.relative_to(path).as_posix() + '/'
+        file_names = [name for name in file_names if name.startswith(prefix)]
+        if not file_names:
+            if not_exists_ok:
+                return
+            raise FileNotFoundError(
+                f'Family folder {folder_path!r} does not exist')
+    for file_name in file_names:
         if file_name.endswith(suffix):
             file_path = Path(file_name)
             family_name = file_path.name.removesuffix(suffix)
-            family_files[family_name] = str(folder / file_path.name)
+            family_files[family_name] = str(path / file_path)


 # Get the names of all known families, and initialize with empty dictionaries.
diff --git a/pywikibot/family.py b/pywikibot/family.py
index 66afc3d..b5613ae 100644
--- a/pywikibot/family.py
+++ b/pywikibot/family.py
@@ -385,7 +385,9 @@
             #     RuntimeWarning's while loading.
             with warnings.catch_warnings():
                 warnings.simplefilter('ignore', RuntimeWarning)
-                sys.path.append(dirname(family_file))
+                family_dir = dirname(family_file)
+                if family_dir not in sys.path:
+                    sys.path.append(family_dir)
                 mod = import_module(splitext(basename(family_file))[0])
         except ImportError:
             raise UnknownFamilyError(f'Family {fam} does not exist')
diff --git a/tests/config_tests.py b/tests/config_tests.py
index 870be2b..91b7043 100755
--- a/tests/config_tests.py
+++ b/tests/config_tests.py
@@ -7,12 +7,16 @@
 """Test cases for the :mod:`config` module."""
 from __future__ import annotations

+import sys
 import unittest
 from contextlib import suppress
 from pathlib import Path, PurePosixPath, PureWindowsPath
+from tempfile import TemporaryDirectory
 from unittest.mock import patch
+from zipfile import ZipFile

 from pywikibot import config
+from pywikibot.family import Family
 from tests.aspects import TestCase


@@ -22,6 +26,57 @@

     net = False

+    def test_register_families_zip(self) -> None:
+        """Discover and load archived families without duplicate paths."""
+        with TemporaryDirectory() as tmp, \
+                patch.dict(config.family_files, {}, clear=True), \
+                patch.dict(Family._families, {}, clear=True), \
+                patch.dict(sys.modules), \
+                patch.object(sys, 'path', sys.path.copy()):
+            archive = Path(tmp) / 'families.zip'
+            folder = archive / 'families'
+            expected = {}
+            with ZipFile(archive, 'w') as zip_file:
+                for name in ('zipalpha', 'zipbeta'):
+                    filename = name + '_family.py'
+                    zip_file.writestr(
+                        'families/' + filename,
+                        'from pywikibot.family import Family as BaseFamily\n'
+                        'class Family(BaseFamily):\n'
+                        f'    name = {name!r}\n'
+                        "    langs = {'en': 'example.invalid'}\n")
+                    expected[name] = str(folder / filename)
+                zip_file.writestr('other/unused_family.py', '')
+
+            config.register_families_folder(folder, not_exists_ok=True)
+            self.assertEqual(config.family_files, expected)
+            for name in expected:
+                self.assertEqual(Family.load(name).name, name)
+            self.assertEqual(sys.path.count(str(folder)), 1)
+
+            config.register_families_folder(archive)
+            expected['unused'] = str(archive / 'other/unused_family.py')
+            self.assertEqual(config.family_files, expected)
+
+    def test_register_families_missing_folder(self) -> None:
+        """Retain missing-folder and non-directory error handling."""
+        with TemporaryDirectory() as tmp, \
+                patch.dict(config.family_files, {}, clear=True):
+            archive = Path(tmp) / 'families.zip'
+            with ZipFile(archive, 'w') as zip_file:
+                zip_file.writestr('families/example_family.py', '')
+            for folder in (Path(tmp) / 'missing', archive / 'missing'):
+                with self.subTest(folder=folder):
+                    with self.assertRaises(FileNotFoundError):
+                        config.register_families_folder(folder)
+                    config.register_families_folder(folder,
+                                                    not_exists_ok=True)
+            file_path = Path(tmp) / 'plain.txt'
+            file_path.touch()
+            with self.assertRaises(NotADirectoryError):
+                config.register_families_folder(file_path)
+            self.assertEqual(config.family_files, {})
+
     def test_shortpath_path_containment(self) -> None:
         """Test shortpath uses path components for containment."""
         base_dir = Path.cwd() / 'pywikibot'

--
To view, visit 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1354003?usp=email
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.wikimedia.org/r/settings?usp=email

Gerrit-MessageType: merged
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: If0ec6843cd092071f6b8ddefd5c323d69f70eeb7
Gerrit-Change-Number: 1354003
Gerrit-PatchSet: 2
Gerrit-Owner: Mahveotm <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to