4 import importlib
.machinery
12 from pathlib
import Path
13 from zipfile
import ZipFile
15 from .compat
import functools
# isort: split
18 get_system_config_dirs
,
24 PACKAGE_NAME
= 'yt_dlp_plugins'
25 COMPAT_PACKAGE_NAME
= 'ytdlp_plugins'
28 class PluginLoader(importlib
.abc
.Loader
):
29 """Dummy loader for virtual namespace packages"""
31 def exec_module(self
, module
):
36 def dirs_in_zip(archive
):
38 with
ZipFile(archive
) as zip_
:
39 return set(itertools
.chain
.from_iterable(
40 Path(file).parents
for file in zip_
.namelist()))
41 except FileNotFoundError
:
43 except Exception as e
:
44 write_string(f
'WARNING: Could not read zip file {archive}: {e}\n')
48 class PluginFinder(importlib
.abc
.MetaPathFinder
):
50 This class provides one or multiple namespace packages.
51 It searches in sys.path and yt-dlp config folders for
52 the existing subdirectories from which the modules can be imported
55 def __init__(self
, *packages
):
56 self
._zip
_content
_cache
= {}
57 self
.packages
= set(itertools
.chain
.from_iterable(
58 itertools
.accumulate(name
.split('.'), lambda a
, b
: '.'.join((a
, b
)))
59 for name
in packages
))
61 def search_locations(self
, fullname
):
62 candidate_locations
= []
64 def _get_package_paths(*root_paths
, containing_folder
='plugins'):
65 for config_dir
in orderedSet(map(Path
, root_paths
), lazy
=True):
66 with contextlib
.suppress(OSError):
67 yield from (config_dir
/ containing_folder
).iterdir()
69 # Load from yt-dlp config folders
70 candidate_locations
.extend(_get_package_paths(
71 *get_user_config_dirs('yt-dlp'),
72 *get_system_config_dirs('yt-dlp'),
73 containing_folder
='plugins'))
75 # Load from yt-dlp-plugins folders
76 candidate_locations
.extend(_get_package_paths(
77 get_executable_path(),
78 *get_user_config_dirs(''),
79 *get_system_config_dirs(''),
80 containing_folder
='yt-dlp-plugins'))
82 candidate_locations
.extend(map(Path
, sys
.path
)) # PYTHONPATH
83 with contextlib
.suppress(ValueError): # Added when running __main__.py directly
84 candidate_locations
.remove(Path(__file__
).parent
)
86 parts
= Path(*fullname
.split('.'))
87 for path
in orderedSet(candidate_locations
, lazy
=True):
88 candidate
= path
/ parts
89 if candidate
.is_dir():
91 elif path
.suffix
in ('.zip', '.egg', '.whl') and path
.is_file():
92 if parts
in dirs_in_zip(path
):
95 def find_spec(self
, fullname
, path
=None, target
=None):
96 if fullname
not in self
.packages
:
99 search_locations
= list(map(str, self
.search_locations(fullname
)))
100 if not search_locations
:
103 spec
= importlib
.machinery
.ModuleSpec(fullname
, PluginLoader(), is_package
=True)
104 spec
.submodule_search_locations
= search_locations
107 def invalidate_caches(self
):
108 dirs_in_zip
.cache_clear()
109 for package
in self
.packages
:
110 if package
in sys
.modules
:
111 del sys
.modules
[package
]
115 spec
= importlib
.util
.find_spec(PACKAGE_NAME
)
116 return spec
.submodule_search_locations
if spec
else []
119 def iter_modules(subpackage
):
120 fullname
= f
'{PACKAGE_NAME}.{subpackage}'
121 with contextlib
.suppress(ModuleNotFoundError
):
122 pkg
= importlib
.import_module(fullname
)
123 yield from pkgutil
.iter_modules(path
=pkg
.__path
__, prefix
=f
'{fullname}.')
126 def load_module(module
, module_name
, suffix
):
127 return inspect
.getmembers(module
, lambda obj
: (
129 and obj
.__name
__.endswith(suffix
)
130 and obj
.__module
__.startswith(module_name
)
131 and not obj
.__name
__.startswith('_')
132 and obj
.__name
__ in getattr(module
, '__all__', [obj
.__name
__])))
135 def load_plugins(name
, suffix
):
138 for finder
, module_name
, _
in iter_modules(name
):
139 if any(x
.startswith('_') for x
in module_name
.split('.')):
142 if sys
.version_info
< (3, 10) and isinstance(finder
, zipimport
.zipimporter
):
143 # zipimporter.load_module() is deprecated in 3.10 and removed in 3.12
144 # The exec_module branch below is the replacement for >= 3.10
145 # See: https://docs.python.org/3/library/zipimport.html#zipimport.zipimporter.exec_module
146 module
= finder
.load_module(module_name
)
148 spec
= finder
.find_spec(module_name
)
149 module
= importlib
.util
.module_from_spec(spec
)
150 sys
.modules
[module_name
] = module
151 spec
.loader
.exec_module(module
)
153 write_string(f
'Error while importing module {module_name!r}\n{traceback.format_exc(limit=-1)}')
155 classes
.update(load_module(module
, module_name
, suffix
))
157 # Compat: old plugin system using __init__.py
158 # Note: plugins imported this way do not show up in directories()
159 # nor are considered part of the yt_dlp_plugins namespace package
160 with contextlib
.suppress(FileNotFoundError
):
161 spec
= importlib
.util
.spec_from_file_location(
162 name
, Path(get_executable_path(), COMPAT_PACKAGE_NAME
, name
, '__init__.py'))
163 plugins
= importlib
.util
.module_from_spec(spec
)
164 sys
.modules
[spec
.name
] = plugins
165 spec
.loader
.exec_module(plugins
)
166 classes
.update(load_module(plugins
, spec
.name
, suffix
))
171 sys
.meta_path
.insert(0, PluginFinder(f
'{PACKAGE_NAME}.extractor', f
'{PACKAGE_NAME}.postprocessor'))
173 __all__
= ['directories', 'load_plugins', 'PACKAGE_NAME', 'COMPAT_PACKAGE_NAME']