plugins.py 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200
  1. # Copyright (c) Alibaba, Inc. and its affiliates.
  2. # This file is adapted from the AllenNLP library at https://github.com/allenai/allennlp
  3. # Part of the implementation is borrowed from wimglenn/johnnydep
  4. import copy
  5. import filecmp
  6. import importlib
  7. import os
  8. import pkgutil
  9. import shutil
  10. import sys
  11. from contextlib import contextmanager
  12. from fnmatch import fnmatch
  13. from pathlib import Path
  14. from typing import Any, Iterable, List, Optional, Set, Union
  15. import json
  16. import pkg_resources
  17. from modelscope import snapshot_download
  18. from modelscope.fileio.file import LocalStorage
  19. from modelscope.utils.ast_utils import FilesAstScanning
  20. from modelscope.utils.constant import DEFAULT_MODEL_REVISION
  21. from modelscope.utils.file_utils import get_modelscope_cache_dir
  22. from modelscope.utils.logger import get_logger
  23. logger = get_logger()
  24. storage = LocalStorage()
  25. MODELSCOPE_FILE_DIR = get_modelscope_cache_dir()
  26. MODELSCOPE_DYNAMIC_MODULE = 'modelscope_modules'
  27. BASE_MODULE_DIR = os.path.join(MODELSCOPE_FILE_DIR, MODELSCOPE_DYNAMIC_MODULE)
  28. PLUGINS_FILENAME = '.modelscope_plugins'
  29. OFFICIAL_PLUGINS = [
  30. {
  31. 'name': 'adaseq',
  32. 'desc':
  33. 'Provide hundreds of additions NERs algorithms, check: https://github.com/modelscope/AdaSeq',
  34. 'version': '',
  35. 'url': ''
  36. },
  37. ]
  38. LOCAL_PLUGINS_FILENAME = '.modelscope_plugins'
  39. GLOBAL_PLUGINS_FILENAME = os.path.join(Path.home(), '.modelscope', 'plugins')
  40. DEFAULT_PLUGINS = []
  41. @contextmanager
  42. def pushd(new_dir: str, verbose: bool = False):
  43. """
  44. Changes the current directory to the given path and prepends it to `sys.path`.
  45. This method is intended to use with `with`, so after its usage, the current
  46. directory will be set to the previous value.
  47. """
  48. previous_dir = os.getcwd()
  49. if verbose:
  50. logger.info(f'Changing directory to {new_dir}') # type: ignore
  51. os.chdir(new_dir)
  52. try:
  53. yield
  54. finally:
  55. if verbose:
  56. logger.info(f'Changing directory back to {previous_dir}')
  57. os.chdir(previous_dir)
  58. @contextmanager
  59. def push_python_path(path: str):
  60. """
  61. Prepends the given path to `sys.path`.
  62. This method is intended to use with `with`, so after its usage, its value
  63. will be removed from `sys.path`.
  64. """
  65. path = Path(path).resolve()
  66. path = str(path)
  67. sys.path.insert(0, path)
  68. try:
  69. yield
  70. finally:
  71. sys.path.remove(path)
  72. def discover_file_plugins(
  73. filename: str = LOCAL_PLUGINS_FILENAME) -> Iterable[str]:
  74. """
  75. Discover plugins from file
  76. """
  77. with open(filename) as f:
  78. for module_name in f:
  79. module_name = module_name.strip()
  80. if module_name:
  81. yield module_name
  82. def discover_plugins(requirement_path=None) -> Iterable[str]:
  83. """
  84. Discover plugins
  85. Args:
  86. requirement_path: The file path of requirement
  87. """
  88. plugins: Set[str] = set()
  89. if requirement_path is None:
  90. if os.path.isfile(LOCAL_PLUGINS_FILENAME):
  91. with push_python_path('.'):
  92. for plugin in discover_file_plugins(LOCAL_PLUGINS_FILENAME):
  93. if plugin in plugins:
  94. continue
  95. yield plugin
  96. plugins.add(plugin)
  97. if os.path.isfile(GLOBAL_PLUGINS_FILENAME):
  98. for plugin in discover_file_plugins(GLOBAL_PLUGINS_FILENAME):
  99. if plugin in plugins:
  100. continue
  101. yield plugin
  102. plugins.add(plugin)
  103. else:
  104. if os.path.isfile(requirement_path):
  105. for plugin in discover_file_plugins(requirement_path):
  106. if plugin in plugins:
  107. continue
  108. yield plugin
  109. plugins.add(plugin)
  110. def import_all_plugins(plugins: List[str] = None) -> List[str]:
  111. """
  112. Imports default plugins, input plugins and file discovered plugins.
  113. """
  114. import_module_and_submodules(
  115. 'modelscope',
  116. include={
  117. 'modelscope.metrics.builder',
  118. 'modelscope.models.builder',
  119. 'modelscope.pipelines.builder',
  120. 'modelscope.preprocessors.builder',
  121. 'modelscope.trainers.builder',
  122. },
  123. exclude={
  124. 'modelscope.metrics.*',
  125. 'modelscope.models.*',
  126. 'modelscope.pipelines.*',
  127. 'modelscope.preprocessors.*',
  128. 'modelscope.trainers.*',
  129. 'modelscope.msdatasets',
  130. 'modelscope.utils',
  131. 'modelscope.exporters',
  132. })
  133. imported_plugins: List[str] = []
  134. imported_plugins.extend(import_plugins(DEFAULT_PLUGINS))
  135. imported_plugins.extend(import_plugins(plugins))
  136. imported_plugins.extend(import_file_plugins())
  137. return imported_plugins
  138. def import_plugins(plugins: List[str] = None) -> List[str]:
  139. """
  140. Imports the plugins listed in the arguments.
  141. """
  142. imported_plugins: List[str] = []
  143. if plugins is None or len(plugins) == 0:
  144. return imported_plugins
  145. # Workaround for a presumed Python issue where spawned processes can't find modules in the current directory.
  146. cwd = os.getcwd()
  147. if cwd not in sys.path:
  148. sys.path.append(cwd)
  149. for module_name in plugins:
  150. try:
  151. # TODO: include and exclude should be configurable, hard code now
  152. import_module_and_submodules(
  153. module_name,
  154. include={
  155. 'easycv.toolkit.modelscope',
  156. 'easycv.hooks',
  157. 'easycv.models',
  158. 'easycv.core',
  159. 'easycv.toolkit',
  160. 'easycv.predictors',
  161. },
  162. exclude={
  163. 'easycv.toolkit.*',
  164. 'easycv.*',
  165. })
  166. logger.info('Plugin %s available', module_name)
  167. imported_plugins.append(module_name)
  168. except ModuleNotFoundError as e:
  169. logger.error(f'Plugin {module_name} could not be loaded: {e}')
  170. return imported_plugins
  171. def import_file_plugins(requirement_path=None) -> List[str]:
  172. """
  173. Imports the plugins found with `discover_plugins()`.
  174. Args:
  175. requirement_path: The file path of requirement
  176. """
  177. imported_plugins: List[str] = []
  178. # Workaround for a presumed Python issue where spawned processes can't find modules in the current directory.
  179. cwd = os.getcwd()
  180. if cwd not in sys.path:
  181. sys.path.append(cwd)
  182. for module_name in discover_plugins(requirement_path):
  183. try:
  184. importlib.import_module(module_name)
  185. logger.info('Plugin %s available', module_name)
  186. imported_plugins.append(module_name)
  187. except ModuleNotFoundError as e:
  188. logger.error(f'Plugin {module_name} could not be loaded: {e}')
  189. return imported_plugins
  190. def import_module_and_submodules(package_name: str,
  191. include: Optional[Set[str]] = None,
  192. exclude: Optional[Set[str]] = None) -> None:
  193. """
  194. Import all public submodules under the given package.
  195. """
  196. # take care of None
  197. include = include if include else set()
  198. exclude = exclude if exclude else set()
  199. def fn_in(package_name: str, pattern_set: Set[str]) -> bool:
  200. for pattern in pattern_set:
  201. if fnmatch(package_name, pattern):
  202. return True
  203. return False
  204. if not fn_in(package_name, include) and fn_in(package_name, exclude):
  205. return
  206. importlib.invalidate_caches()
  207. # For some reason, python doesn't always add this by default to your path, but you pretty much
  208. # always want it when using `--include-package`. And if it's already there, adding it again at
  209. # the end won't hurt anything.
  210. with push_python_path('.'):
  211. # Import at top level
  212. try:
  213. module = importlib.import_module(package_name)
  214. path = getattr(module, '__path__', [])
  215. path_string = '' if not path else path[0]
  216. # walk_packages only finds immediate children, so need to recurse.
  217. for module_finder, name, _ in pkgutil.iter_modules(path):
  218. # Sometimes when you import third-party libraries that are on your path,
  219. # `pkgutil.walk_packages` returns those too, so we need to skip them.
  220. # `pkgutil.iter_modules` avoid import those package
  221. if path_string and module_finder.path != path_string: # type: ignore[union-attr]
  222. continue
  223. if name.startswith('_'):
  224. # skip directly importing private subpackages
  225. continue
  226. if name.startswith('test'):
  227. # skip tests
  228. continue
  229. subpackage = f'{package_name}.{name}'
  230. import_module_and_submodules(
  231. subpackage, include=include, exclude=exclude)
  232. except SystemExit as e:
  233. # this case is specific for easy_cv's tools/predict.py exit
  234. logger.warning(
  235. f'{package_name} not imported: {str(e)}, but should continue')
  236. pass
  237. except Exception as e:
  238. logger.warning(f'{package_name} not imported: {str(e)}')
  239. if len(package_name.split('.')) == 1:
  240. raise ModuleNotFoundError('Package not installed')
  241. def install_module_from_requirements(requirement_path, ):
  242. """ install module from requirements
  243. Args:
  244. requirement_path: The path of requirement file
  245. No returns, raise error if failed
  246. """
  247. install_list = []
  248. with open(requirement_path, 'r', encoding='utf-8') as f:
  249. requirements = f.read().splitlines()
  250. for req in requirements:
  251. if req == '':
  252. continue
  253. installed, _ = PluginsManager.check_plugin_installed(req)
  254. if not installed:
  255. install_list.append(req)
  256. if len(install_list) > 0:
  257. status_code, _, args = PluginsManager.pip_command(
  258. 'install',
  259. install_list,
  260. )
  261. if status_code != 0:
  262. raise ImportError(
  263. f'Failed to install requirements from {requirement_path}')
  264. def import_module_from_file(module_name, file_path):
  265. """ install module by name with file path
  266. Args:
  267. module_name: the module name need to be import
  268. file_path: the related file path that matched with the module name
  269. Returns: return the module class
  270. """
  271. spec = importlib.util.spec_from_file_location(module_name, file_path)
  272. module = importlib.util.module_from_spec(spec)
  273. spec.loader.exec_module(module)
  274. return module
  275. def create_module_from_files(file_list, file_prefix, module_name):
  276. """
  277. Create a python module from a list of files by copying them to the destination directory.
  278. Args:
  279. file_list (List[str]): List of relative file paths to be copied.
  280. file_prefix (str): Path prefix for each file in file_list.
  281. module_name (str): Name of the module.
  282. Returns:
  283. None
  284. """
  285. def create_empty_file(file_path):
  286. with open(file_path, 'w') as _:
  287. pass
  288. dest_dir = os.path.join(BASE_MODULE_DIR, module_name)
  289. for file_path in file_list:
  290. file_dir = os.path.dirname(file_path)
  291. target_dir = os.path.join(dest_dir, file_dir)
  292. os.makedirs(target_dir, exist_ok=True)
  293. init_file = os.path.join(target_dir, '__init__.py')
  294. if not os.path.exists(init_file):
  295. create_empty_file(init_file)
  296. target_file = os.path.join(target_dir, os.path.basename(file_path))
  297. src_file = os.path.join(file_prefix, file_path)
  298. if not os.path.exists(target_file) or not filecmp.cmp(
  299. src_file, target_file):
  300. shutil.copyfile(src_file, target_file)
  301. importlib.invalidate_caches()
  302. def import_module_from_model_dir(model_dir):
  303. """ import all the necessary module from a model dir
  304. Args:
  305. model_dir: model file location
  306. No returns, raise error if failed
  307. """
  308. from pathlib import Path
  309. file_scanner = FilesAstScanning()
  310. file_scanner.traversal_files(model_dir, include_init=True)
  311. file_dirs = file_scanner.file_dirs
  312. requirements = file_scanner.requirement_dirs
  313. # install the requirements firstly
  314. install_requirements_by_files(requirements)
  315. if BASE_MODULE_DIR not in sys.path:
  316. sys.path.append(BASE_MODULE_DIR)
  317. module_name = Path(model_dir).stem
  318. # in order to keep forward compatibility, we add module path to
  319. # sys.path so that submodule can be imported directly as before
  320. MODULE_PATH = os.path.join(BASE_MODULE_DIR, module_name)
  321. if MODULE_PATH not in sys.path:
  322. sys.path.append(MODULE_PATH)
  323. relative_file_dirs = [
  324. file.replace(model_dir.rstrip(os.sep) + os.sep, '')
  325. for file in file_dirs
  326. ]
  327. create_module_from_files(relative_file_dirs, model_dir, module_name)
  328. for file in relative_file_dirs:
  329. submodule = module_name + '.' + file.replace('.py', '').replace(
  330. os.sep, '.')
  331. importlib.import_module(submodule)
  332. def install_requirements_by_names(plugins: List[str]):
  333. """ install the requirements by names
  334. Args:
  335. plugins: name of plugins (pai-easyscv, transformers)
  336. No returns, raise error if failed
  337. """
  338. plugins_manager = PluginsManager()
  339. uninstalled_plugins = []
  340. for plugin in plugins:
  341. plugin_installed, version = plugins_manager.check_plugin_installed(
  342. plugin)
  343. if not plugin_installed:
  344. uninstalled_plugins.append(plugin)
  345. status, _ = plugins_manager.install_plugins(uninstalled_plugins)
  346. if status != 0:
  347. raise EnvironmentError(
  348. f'The required packages {",".join(uninstalled_plugins)} are not installed.',
  349. f'Please run the command `modelscope plugin install {" ".join(uninstalled_plugins)}` to install them.'
  350. )
  351. def install_requirements_by_files(requirements: List[str]):
  352. """ install the requirements by files
  353. Args:
  354. requirements: a list of files including requirements info (requirements.txt)
  355. No returns, raise error if failed
  356. """
  357. for requirement in requirements:
  358. install_module_from_requirements(requirement)
  359. def register_plugins_repo(plugins: List[str]) -> None:
  360. """ Try to install and import plugins from repo"""
  361. if plugins is not None:
  362. install_requirements_by_names(plugins)
  363. modules = []
  364. for plugin in plugins:
  365. module_name, module_version, _ = get_modules_from_package(plugin)
  366. modules.extend(module_name)
  367. import_plugins(modules)
  368. def register_modelhub_repo(model_dir, allow_remote=False) -> None:
  369. """ Try to install and import remote model from modelhub"""
  370. if allow_remote:
  371. logger.warning(
  372. f'Use allow_remote=True. Will invoke codes from {model_dir}. Please make sure '
  373. 'that you can trust the external codes.')
  374. try:
  375. import_module_from_model_dir(model_dir)
  376. except KeyError:
  377. logger.warning(
  378. 'Multi component keys in the hub are registered in same file')
  379. pass
  380. DEFAULT_INDEX = 'https://pypi.org/simple/'
  381. def get_modules_from_package(package):
  382. """ to get the modules from an installed package
  383. Args:
  384. package: The distribution name or package name
  385. Returns:
  386. import_names: The modules that in the package distribution
  387. import_version: The version of those modules, should be same and identical
  388. package_name: The package name, if installed by whl file, the package is unknown, should be passed
  389. """
  390. from zipfile import ZipFile
  391. from tempfile import mkdtemp
  392. from subprocess import check_output, STDOUT
  393. from glob import glob
  394. import hashlib
  395. from urllib.parse import urlparse
  396. from urllib import request as urllib2
  397. from pip._internal.utils.packaging import get_requirement
  398. def urlretrieve(url, filename, data=None, auth=None):
  399. if auth is not None:
  400. # https://docs.python.org/2.7/howto/urllib2.html#id6
  401. password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
  402. # Add the username and password.
  403. # If we knew the realm, we could use it instead of None.
  404. username, password = auth
  405. top_level_url = urlparse(url).netloc
  406. password_mgr.add_password(None, top_level_url, username, password)
  407. handler = urllib2.HTTPBasicAuthHandler(password_mgr)
  408. # create "opener" (OpenerDirector instance)
  409. opener = urllib2.build_opener(handler)
  410. else:
  411. opener = urllib2.build_opener()
  412. res = opener.open(url, data=data)
  413. headers = res.info()
  414. with open(filename, 'wb') as fp:
  415. fp.write(res.read())
  416. return filename, headers
  417. def compute_checksum(target, algorithm='sha256', blocksize=2**13):
  418. hashtype = getattr(hashlib, algorithm)
  419. hash_ = hashtype()
  420. logger.debug('computing checksum', target=target, algorithm=algorithm)
  421. with open(target, 'rb') as f:
  422. for chunk in iter(lambda: f.read(blocksize), b''):
  423. hash_.update(chunk)
  424. result = hash_.hexdigest()
  425. logger.debug('computed checksum', result=result)
  426. return result
  427. def _get_pip_version():
  428. # try to get pip version without actually importing pip
  429. # setuptools gets upset if you import pip before importing setuptools..
  430. try:
  431. import importlib.metadata # Python 3.8+
  432. return importlib.metadata.version('pip')
  433. except Exception:
  434. pass
  435. import pip
  436. return pip.__version__
  437. def _download_dist(url, scratch_file, index_url, extra_index_url):
  438. auth = None
  439. if index_url:
  440. parsed = urlparse(index_url)
  441. if parsed.username and parsed.password and parsed.hostname == urlparse(
  442. url).hostname:
  443. # handling private PyPI credentials in index_url
  444. auth = (parsed.username, parsed.password)
  445. if extra_index_url:
  446. parsed = urlparse(extra_index_url)
  447. if parsed.username and parsed.password and parsed.hostname == urlparse(
  448. url).hostname:
  449. # handling private PyPI credentials in extra_index_url
  450. auth = (parsed.username, parsed.password)
  451. target, _headers = urlretrieve(url, scratch_file, auth=auth)
  452. return target, _headers
  453. def _get_wheel_args(index_url, env, extra_index_url):
  454. args = [
  455. sys.executable,
  456. '-m',
  457. 'pip',
  458. 'wheel',
  459. '-vvv', # --verbose x3
  460. '--no-deps',
  461. '--no-cache-dir',
  462. '--disable-pip-version-check',
  463. ]
  464. if index_url is not None:
  465. args += ['--index-url', index_url]
  466. if index_url != DEFAULT_INDEX:
  467. hostname = urlparse(index_url).hostname
  468. if hostname:
  469. args += ['--trusted-host', hostname]
  470. if extra_index_url is not None:
  471. args += [
  472. '--extra-index-url', extra_index_url, '--trusted-host',
  473. urlparse(extra_index_url).hostname
  474. ]
  475. if env is None:
  476. pip_version = _get_pip_version()
  477. else:
  478. pip_version = dict(env)['pip_version']
  479. args[0] = dict(env)['python_executable']
  480. pip_major, pip_minor = pip_version.split('.')[0:2]
  481. pip_major = int(pip_major)
  482. pip_minor = int(pip_minor)
  483. if pip_major >= 10:
  484. args.append('--progress-bar=off')
  485. if (20, 3) <= (pip_major, pip_minor) < (21, 1):
  486. # See https://github.com/pypa/pip/issues/9139#issuecomment-735443177
  487. args.append('--use-deprecated=legacy-resolver')
  488. return args
  489. def get(dist_name,
  490. index_url=None,
  491. env=None,
  492. extra_index_url=None,
  493. tmpdir=None,
  494. ignore_errors=False):
  495. args = _get_wheel_args(index_url, env, extra_index_url) + [dist_name]
  496. scratch_dir = mkdtemp(dir=tmpdir)
  497. logger.debug(
  498. 'wheeling and dealing',
  499. scratch_dir=os.path.abspath(scratch_dir),
  500. args=' '.join(args))
  501. try:
  502. out = check_output(
  503. args, stderr=STDOUT, cwd=scratch_dir).decode('utf-8')
  504. except ChildProcessError as err:
  505. out = getattr(err, 'output', b'').decode('utf-8')
  506. logger.warning(out)
  507. if not ignore_errors:
  508. raise
  509. logger.debug('wheel command completed ok', dist_name=dist_name)
  510. links = []
  511. local_links = []
  512. lines = out.splitlines()
  513. for i, line in enumerate(lines):
  514. line = line.strip()
  515. if line.startswith('Downloading from URL '):
  516. parts = line.split()
  517. link = parts[3]
  518. links.append(link)
  519. elif line.startswith('Downloading '):
  520. parts = line.split()
  521. last = parts[-1]
  522. if len(parts) == 3 and last.startswith('(') and last.endswith(
  523. ')'):
  524. link = parts[-2]
  525. elif len(parts) == 4 and parts[-2].startswith(
  526. '(') and last.endswith(')'):
  527. link = parts[-3]
  528. if not urlparse(link).scheme:
  529. # newest pip versions have changed to not log the full url
  530. # in the download event. it is becoming more and more annoying
  531. # to preserve compatibility across a wide range of pip versions
  532. next_line = lines[i + 1].strip()
  533. if next_line.startswith(
  534. 'Added ') and ' to build tracker' in next_line:
  535. link = next_line.split(
  536. ' to build tracker')[0].split()[-1]
  537. else:
  538. link = last
  539. links.append(link)
  540. elif line.startswith(
  541. 'Source in ') and 'which satisfies requirement' in line:
  542. link = line.split()[-1]
  543. links.append(link)
  544. elif line.startswith('Added ') and ' from file://' in line:
  545. [link] = [x for x in line.split() if x.startswith('file://')]
  546. local_links.append(link)
  547. if not links:
  548. # prefer http scheme over file
  549. links += local_links
  550. links = list(dict.fromkeys(links)) # order-preserving dedupe
  551. if not links:
  552. logger.warning('could not find download link', out=out)
  553. raise Exception('failed to collect dist')
  554. if len(links) == 2:
  555. # sometimes we collect the same link, once with a url fragment/checksum and once without
  556. first, second = links
  557. if first.startswith(second):
  558. del links[1]
  559. elif second.startswith(first):
  560. del links[0]
  561. if len(links) > 1:
  562. logger.debug('more than 1 link collected', out=out, links=links)
  563. # Since PEP 517, maybe an sdist will also need to collect other distributions
  564. # for the build system, even with --no-deps specified. pendulum==1.4.4 is one
  565. # example, which uses poetry and doesn't publish any python37 wheel to PyPI.
  566. # However, the dist itself should still be the first one downloaded.
  567. link = links[0]
  568. whls = glob(os.path.join(os.path.abspath(scratch_dir), '*.whl'))
  569. try:
  570. [whl] = whls
  571. except ValueError:
  572. if ignore_errors:
  573. whl = ''
  574. else:
  575. raise
  576. url, _sep, checksum = link.partition('#')
  577. url = url.replace(
  578. '/%2Bf/', '/+f/'
  579. ) # some versions of pip did not unquote this fragment in the log
  580. if not checksum.startswith('md5=') and not checksum.startswith(
  581. 'sha256='):
  582. # PyPI gives you the checksum in url fragment, as a convenience. But not all indices are so kind.
  583. algorithm = 'md5'
  584. if os.path.basename(whl).lower() == url.rsplit('/', 1)[-1].lower():
  585. target = whl
  586. else:
  587. scratch_file = os.path.join(scratch_dir, os.path.basename(url))
  588. target, _headers = _download_dist(url, scratch_file, index_url,
  589. extra_index_url)
  590. checksum = compute_checksum(target=target, algorithm=algorithm)
  591. checksum = '='.join([algorithm, checksum])
  592. result = {'path': whl, 'url': url, 'checksum': checksum}
  593. return result
  594. def discover_import_names(whl_file):
  595. import re
  596. logger.debug('finding import names')
  597. zipfile = ZipFile(file=whl_file)
  598. namelist = zipfile.namelist()
  599. [top_level_fname
  600. ] = [x for x in namelist if x.endswith('top_level.txt')]
  601. [metadata_fname
  602. ] = [x for x in namelist if x.endswith('.dist-info/METADATA')]
  603. all_names = zipfile.read(top_level_fname).decode(
  604. 'utf-8').strip().splitlines()
  605. metadata = zipfile.read(metadata_fname).decode('utf-8')
  606. public_names = [n for n in all_names if not n.startswith('_')]
  607. version_pattern = re.compile(r'^Version: (?P<version>.+)$',
  608. re.MULTILINE)
  609. name_pattern = re.compile(r'^Name: (?P<name>.+)$', re.MULTILINE)
  610. version_match = version_pattern.search(metadata)
  611. name_match = name_pattern.search(metadata)
  612. module_version = version_match.group('version')
  613. module_name = name_match.group('name')
  614. return public_names, module_version, module_name
  615. tmpdir = mkdtemp()
  616. if package.endswith('.whl'):
  617. """if user using .whl file then parse the whl to get the module name"""
  618. if not os.path.isfile(package):
  619. file_name = os.path.basename(package)
  620. file_path = os.path.join(tmpdir, file_name)
  621. whl_file, _ = _download_dist(package, file_path, None, None)
  622. else:
  623. whl_file = package
  624. else:
  625. """if user using package name then generate whl file and parse the file to get the module name by
  626. the discover_import_names method
  627. """
  628. req = get_requirement(package)
  629. package = req.name
  630. data = get(package, tmpdir=tmpdir)
  631. whl_file = data['path']
  632. import_names, import_version, package_name = discover_import_names(
  633. whl_file)
  634. shutil.rmtree(tmpdir)
  635. return import_names, import_version, package_name
  636. class PluginsManager(object):
  637. """
  638. plugins manager class
  639. """
  640. def __init__(self,
  641. cache_dir=MODELSCOPE_FILE_DIR,
  642. plugins_file=PLUGINS_FILENAME):
  643. cache_dir = os.getenv('MODELSCOPE_CACHE', cache_dir)
  644. plugins_file = os.getenv('MODELSCOPE_PLUGINS_FILE', plugins_file)
  645. self._file_path = os.path.join(cache_dir, plugins_file)
  646. @property
  647. def file_path(self):
  648. return self._file_path
  649. @file_path.setter
  650. def file_path(self, value):
  651. self._file_path = value
  652. @staticmethod
  653. def check_plugin_installed(package):
  654. """ Check if the plugin is installed, and if the version is valid
  655. Args:
  656. package: the package name need to be installed
  657. Returns:
  658. if_installed: True if installed
  659. version: the version of installed or None if not installed
  660. """
  661. if package.split('.')[-1] == 'whl':
  662. # install from whl should test package name instead of module name
  663. _, module_version, package_name = get_modules_from_package(package)
  664. local_installed, version = PluginsManager._check_plugin_installed(
  665. package_name)
  666. if local_installed and module_version != version:
  667. return False, version
  668. elif not local_installed:
  669. return False, version
  670. return True, module_version
  671. else:
  672. return PluginsManager._check_plugin_installed(package)
  673. @staticmethod
  674. def _check_plugin_installed(package, verified_version=None):
  675. from pip._internal.utils.packaging import get_requirement, specifiers
  676. req = get_requirement(package)
  677. try:
  678. importlib.reload(pkg_resources)
  679. package_meta_info = pkg_resources.working_set.by_key[req.name]
  680. version = package_meta_info.version
  681. # To test if the package is installed
  682. installed = True
  683. # If installed, test if the version is correct
  684. for spec in req.specifier:
  685. installed_valid_version = spec.contains(version)
  686. if not installed_valid_version:
  687. installed = False
  688. break
  689. except KeyError:
  690. version = ''
  691. installed = False
  692. if installed and verified_version is not None and verified_version != version:
  693. return False, verified_version
  694. else:
  695. return installed, version
  696. @staticmethod
  697. def pip_command(
  698. command,
  699. command_args: List[str],
  700. ):
  701. """
  702. Args:
  703. command: install, uninstall command
  704. command_args: the args to be used with command, should be in list
  705. such as ['-r', 'requirements']
  706. Returns:
  707. status_code: The pip command status code, 0 if success, else is failed
  708. options: parsed option from system args by pip command
  709. args: the unknown args that could be parsed by pip command
  710. """
  711. from pip._internal.commands import create_command
  712. importlib.reload(pkg_resources)
  713. if command == 'install':
  714. command_args.append('-f')
  715. command_args.append(
  716. 'https://modelscope.oss-cn-beijing.aliyuncs.com/releases/repo.html'
  717. )
  718. command = create_command(command)
  719. options, args = command.parse_args(command_args)
  720. status_code = command.main(command_args)
  721. # reload the pkg_resources in order to get the latest pkgs information
  722. importlib.reload(pkg_resources)
  723. return status_code, options, args
  724. def install_plugins(self,
  725. install_args: List[str],
  726. index_url: Optional[str] = None,
  727. force_update=False) -> Any:
  728. """Install packages via pip
  729. Args:
  730. install_args (list): List of arguments passed to `pip install`.
  731. index_url (str, optional): The pypi index url.
  732. force_update: If force update on or off
  733. """
  734. if len(install_args) == 0:
  735. return 0, []
  736. if index_url is not None:
  737. install_args += ['-i', index_url]
  738. if force_update is not False:
  739. install_args += ['-f']
  740. status_code, options, args = PluginsManager.pip_command(
  741. 'install',
  742. install_args,
  743. )
  744. if status_code == 0:
  745. logger.info(f'The plugins {",".join(args)} is installed')
  746. # TODO Add Ast index for ast update record
  747. # Add the plugins info to the local record
  748. installed_package = self.parse_args_info(args, options)
  749. self.update_plugins_file(installed_package)
  750. return status_code, install_args
  751. def parse_args_info(self, args: List[str], options):
  752. """
  753. parse arguments input info
  754. Args:
  755. args: the list of args from pip command output
  756. options: the options that parsed from system args by pip command method
  757. Returns:
  758. installed_package: generate installed package info in order to store in the file
  759. the info includes: name, url and desc of the package
  760. """
  761. installed_package = []
  762. # the case of install with requirements
  763. if len(args) == 0:
  764. src_dir = options.src_dir
  765. requirements = options.requirments
  766. for requirement in requirements:
  767. package_info = {
  768. 'name': requirement,
  769. 'url': os.path.join(src_dir, requirement),
  770. 'desc': '',
  771. 'version': ''
  772. }
  773. installed_package.append(package_info)
  774. def get_package_info(package_name):
  775. from pathlib import Path
  776. package_info = {
  777. 'name': package_name,
  778. 'url': options.index_url,
  779. 'desc': ''
  780. }
  781. # the case with git + http
  782. if package_name.split('.')[-1] == 'git':
  783. package_name = Path(package_name).stem
  784. plugin_installed, version = self.check_plugin_installed(
  785. package_name)
  786. if plugin_installed:
  787. package_info['version'] = version
  788. package_info['name'] = package_name
  789. else:
  790. logger.warning(
  791. f'The package {package_name} is not in the lib, this might be happened'
  792. f' when installing the package with git+https method, should be ignored'
  793. )
  794. package_info['version'] = ''
  795. return package_info
  796. for package in args:
  797. package_info = get_package_info(package)
  798. installed_package.append(package_info)
  799. return installed_package
  800. def uninstall_plugins(self,
  801. uninstall_args: Union[str, List],
  802. is_yes=False):
  803. """
  804. uninstall plugins
  805. Args:
  806. uninstall_args: args used to uninstall by pip command
  807. is_yes: force yes without verified
  808. Returns: status code, and uninstall args
  809. """
  810. if is_yes is not None:
  811. uninstall_args += ['-y']
  812. status_code, options, args = PluginsManager.pip_command(
  813. 'uninstall',
  814. uninstall_args,
  815. )
  816. if status_code == 0:
  817. logger.info(f'The plugins {",".join(args)} is uninstalled')
  818. # TODO Add Ast index for ast update record
  819. # Add to the local record
  820. self.remove_plugins_from_file(args)
  821. return status_code, uninstall_args
  822. def _get_plugins_from_file(self):
  823. """ get plugins from file
  824. """
  825. logger.info(f'Loading plugins information from {self.file_path}')
  826. if os.path.exists(self.file_path):
  827. local_plugins_info_bytes = storage.read(self.file_path)
  828. local_plugins_info = json.loads(local_plugins_info_bytes)
  829. else:
  830. local_plugins_info = {}
  831. return local_plugins_info
  832. def _update_plugins(
  833. self,
  834. new_plugins_list,
  835. local_plugins_info,
  836. override=False,
  837. ):
  838. for item in new_plugins_list:
  839. package_name = item.pop('name')
  840. # update package information if existed
  841. if package_name in local_plugins_info and not override:
  842. original_item = local_plugins_info[package_name]
  843. from pkg_resources import parse_version
  844. item_version = parse_version(
  845. item['version'] if item['version'] != '' else '0.0.0')
  846. origin_version = parse_version(
  847. original_item['version']
  848. if original_item['version'] != '' else '0.0.0')
  849. desc = item['desc']
  850. if original_item['desc'] != '' and desc == '':
  851. desc = original_item['desc']
  852. item = item if item_version > origin_version else original_item
  853. item['desc'] = desc
  854. # Double-check if the item is installed with the version number
  855. if item['version'] == '':
  856. plugin_installed, version = self.check_plugin_installed(
  857. package_name)
  858. item['version'] = version
  859. local_plugins_info[package_name] = item
  860. return local_plugins_info
  861. def _print_plugins_info(self, local_plugins_info):
  862. print('{:<15} |{:<10} |{:<100}'.format('NAME', 'VERSION',
  863. 'DESCRIPTION'))
  864. print('')
  865. for k, v in local_plugins_info.items():
  866. print('{:<15} |{:<10} |{:<100}'.format(k, v['version'], v['desc']))
  867. def list_plugins(
  868. self,
  869. show_all=False,
  870. ):
  871. """
  872. Args:
  873. show_all: show installed and official supported if True, else only those installed
  874. Returns:
  875. local_plugins_info: show the list of plugins info
  876. """
  877. local_plugins_info = self._get_plugins_from_file()
  878. # update plugins with default
  879. local_official_plugins = copy.deepcopy(OFFICIAL_PLUGINS)
  880. local_plugins_info = self._update_plugins(local_official_plugins,
  881. local_plugins_info)
  882. if show_all is True:
  883. self._print_plugins_info(local_plugins_info)
  884. return local_plugins_info
  885. # Consider those package with version is installed
  886. not_installed_list = []
  887. for item in local_plugins_info:
  888. if local_plugins_info[item]['version'] == '':
  889. not_installed_list.append(item)
  890. for item in not_installed_list:
  891. local_plugins_info.pop(item)
  892. self._print_plugins_info(local_plugins_info)
  893. return local_plugins_info
  894. def update_plugins_file(
  895. self,
  896. plugins_list,
  897. override=False,
  898. ):
  899. """update the plugins file in order to maintain the latest plugins information
  900. Args:
  901. plugins_list: The plugins list contain the information of plugins
  902. name, version, introduction, install url and the status of delete or update
  903. override: Override the file by the list if True, else only update.
  904. Returns:
  905. local_plugins_info_json: the json version of updated plugins info
  906. """
  907. local_plugins_info = self._get_plugins_from_file()
  908. # local_plugins_info is empty if first time loading, should add OFFICIAL_PLUGINS information
  909. if local_plugins_info == {}:
  910. plugins_list.extend(copy.deepcopy(OFFICIAL_PLUGINS))
  911. local_plugins_info = self._update_plugins(plugins_list,
  912. local_plugins_info, override)
  913. local_plugins_info_json = json.dumps(local_plugins_info)
  914. storage.write(local_plugins_info_json.encode(), self.file_path)
  915. return local_plugins_info_json
  916. def remove_plugins_from_file(
  917. self,
  918. package_names: Union[str, list],
  919. ):
  920. """remove the plugins from file
  921. Args:
  922. package_names: package name
  923. Returns:
  924. local_plugins_info_json: the json version of updated plugins info
  925. """
  926. local_plugins_info = self._get_plugins_from_file()
  927. if type(package_names) is str:
  928. package_names = list(package_names)
  929. for item in package_names:
  930. if item in local_plugins_info:
  931. local_plugins_info.pop(item)
  932. local_plugins_info_json = json.dumps(local_plugins_info)
  933. storage.write(local_plugins_info_json.encode(), self.file_path)
  934. return local_plugins_info_json
  935. class EnvsManager(object):
  936. name = 'envs'
  937. def __init__(self,
  938. model_id,
  939. model_revision=DEFAULT_MODEL_REVISION,
  940. cache_dir=MODELSCOPE_FILE_DIR):
  941. """
  942. Args:
  943. model_id: id of the model, not dir
  944. model_revision: revision of the model, default as master
  945. cache_dir: the system modelscope cache dir
  946. """
  947. cache_dir = os.getenv('MODELSCOPE_CACHE', cache_dir)
  948. self.env_dir = os.path.join(cache_dir, EnvsManager.name, model_id)
  949. model_dir = snapshot_download(model_id, revision=model_revision)
  950. from modelscope.utils.hub import read_config
  951. cfg = read_config(model_dir)
  952. self.plugins = cfg.get('plugins', [])
  953. self.allow_remote = cfg.get('allow_remote', False)
  954. import venv
  955. self.env_builder = venv.EnvBuilder(
  956. system_site_packages=True,
  957. clear=False,
  958. symlinks=True,
  959. with_pip=False)
  960. def get_env_dir(self):
  961. return self.env_dir
  962. def get_activate_dir(self):
  963. return os.path.join(self.env_dir, 'bin', 'activate')
  964. def check_if_need_env(self):
  965. if len(self.plugins) or self.allow_remote:
  966. return True
  967. else:
  968. return False
  969. def create_env(self):
  970. if not os.path.exists(self.env_dir):
  971. os.makedirs(self.env_dir)
  972. try:
  973. self.env_builder.create(self.env_dir)
  974. except Exception as e:
  975. self.clean_env()
  976. raise EnvironmentError(
  977. f'Failed to create virtual env at {self.env_dir} with error: {e}'
  978. )
  979. def clean_env(self):
  980. if os.path.exists(self.env_dir):
  981. self.env_builder.clear_directory(self.env_dir)
  982. @staticmethod
  983. def run_process(cmd):
  984. import subprocess
  985. status, result = subprocess.getstatusoutput(cmd)
  986. logger.debug('The status and the results are: {}, {}'.format(
  987. status, result))
  988. if status != 0:
  989. raise Exception(
  990. 'running the cmd: {} failed, with message: {}'.format(
  991. cmd, result))
  992. return result
  993. if __name__ == '__main__':
  994. install_requirements_by_files(['adaseq'])
  995. import_name, import_version, package_name = get_modules_from_package(
  996. 'pai-easycv')