# -*- coding: utf-8 -*- """Installable PyCharm application descriptions.""" __all__ = ['PyCharmProApp', 'PyCharmCEApp'] import contextlib import json import os import re import typing import webbrowser from anaconda_navigator import config as navigator_config from anaconda_navigator.static import images from . import base from . import detectors if typing.TYPE_CHECKING: import typing_extensions from anaconda_navigator.api import process from anaconda_navigator.config import user as user_config MAC_APPS: typing.Optional[str] = detectors.join( detectors.Mac.home, 'Library', 'Application Support', 'JetBrains', 'Toolbox', 'apps', ) LINUX_APPS: typing.Optional[str] = detectors.join( detectors.Linux.home, '.local', 'share', 'JetBrains', 'Toolbox', 'apps', ) WIN_APPS: typing.Optional[str] = detectors.join( detectors.Win.local_app_data, 'JetBrains', 'Toolbox', 'apps', ) class CheckVersion: # pylint: disable=too-few-public-methods """ Detect version of the PyCharm application. :param product_code: acceptable product code(s) Known options: - PY: PyCharm Pro - PC: PyCharm Community - PE: PyCharm Edu :param product_info_path: Path to the `product-info.json` file relative to the root of the application. :param build_path: Path to the `build.txt` file relative to the root of the application. """ __slots__ = ('__build_path', '__product_code', '__product_info_path') build_pattern: 'typing_extensions.Final[typing.Pattern[str]]' = re.compile( r'(?P[a-zA-Z0-9]+)-(?P[0-9.]+)', ) known_builds: 'typing_extensions.Final[typing.Mapping[str, str]]' = { '101.15': '1.1.1', '105.58': '1.2.1', '107.756': '1.5.4', '111.291': '2.0.2', '117.663': '2.5.2', '121.378': '2.6.3', '125.57': '2.7', '125.92': '2.7.1', '129.314': '2.7.2', '129.782': '2.7.3', '129.1566': '2.7.4', '131.19': '3.0', '131.339': '3.0.1', '131.618': '3.0.2', '131.849': '3.0.3', '133.804': '3.1', '133.881': '3.1.1', '133.1229': '3.1.2', '133.1347': '3.1.3', '133.1884': '3.1.4', '135.973': '3.4', '135.1057': '3.4.1', '135.1317': '3.4.2', '135.1318': '3.4.3', '135.1357': '3.4.4', '139.487': '4.0', '139.574': '4.0.1', '139.711': '4.0.2', '139.781': '4.0.3', '139.1001': '4.0.4', '139.1547': '4.0.5', '139.1659': '4.0.6', '139.1803': '4.0.7', '141.1116': '4.5', '141.1245': '4.5.1', '141.158': '4.5.2', '141.1899': '4.5.3', '141.2569': '4.5.4', '141.3058': '4.5.5', '143.589': '5.0', '143.595': '5.0.1', '143.1184': '5.0.2', '143.1559': '5.0.3', '143.1919': '5.0.4', '143.2370': '5.0.5', '143.2371': '5.0.6', '145.26': '2016.1', '145.598': '2016.1.1', '145.844': '2016.1.2', '145.971': '2016.1.3', '145.1504': '2016.1.4', '145.2073.10': '2016.1.5', '162.1237.1': '2016.2', '162.1628.8': '2016.2.1', '162.1812.1': '2016.2.2', '162.1967.10': '2016.2.3', '163.8233.8': '2016.3', '163.9735.8': '2016.3.1', '163.10154.50': '2016.3.2', '163.15188.4': '2016.3.3', '163.15529.21': '2016.3.4', '163.15529.24': '2016.3.5', '163.15529.25': '2016.3.6', '171.3780.115': '2017.1', '171.4249.47': '2017.1.2', '171.4424.42': '2017.1.3', '171.4694.38': '2017.1.4', '171.4694.67': '2017.1.5', '171.4694.79': '2017.1.6', '171.4694.87': '2017.1.7', '171.4694.94': '2017.1.8', '172.3317.103': '2017.2', '172.3544.46': '2017.2.1', '172.3757.67': '2017.2.2', '172.3968.37': '2017.2.3', '172.4343.24': '2017.2.4', '172.4574.27': '2017.2.5', '172.4574.33': '2017.2.6', '172.4574.37': '2017.2.7', '173.3727.137': '2017.3', '173.3942.36': '2017.3.1', '173.4127.16': '2017.3.2', '173.4301.16': '2017.3.3', '173.4674.37': '2017.3.4', '173.4674.54': '2017.3.5', '173.4674.57': '2017.3.6', '173.4674.62': '2017.3.7', '181.4203.547': '2018.1', '181.4445.76': '2018.1.1', '181.4668.75': '2018.1.2', '181.4892.64': '2018.1.3', '181.5087.37': '2018.1.4', '181.5540.17': '2018.1.5', '181.5540.34': '2018.1.6', '182.3684.100': '2018.2', '182.3911.33': '2018.2.1', '182.4129.34': '2018.2.2', '182.4323.49': '2018.2.3', '182.4505.26': '2018.2.4', '182.5107.22': '2018.2.5', '182.5107.44': '2018.2.6', '182.5107.56': '2018.2.7', '182.5262.4': '2018.2.8', '183.4284.139': '2018.3', '183.4588.64': '2018.3.1', '183.4886.43': '2018.3.2', '183.5153.39': '2018.3.3', '183.5429.31': '2018.3.4', '183.5912.18': '2018.3.5', '183.6156.13': '2018.3.6', '183.6156.16': '2018.3.7', '191.6183.50': '2019.1', '191.6605.12': '2019.1.1', '191.7141.48': '2019.1.2', '191.7479.30': '2019.1.3', '191.8026.44': '2019.1.4', '192.5728.105': '2019.2', '192.6262.63': '2019.2.1', '192.6603.34': '2019.2.2', '192.6817.19': '2019.2.3', '192.7142.42': '2019.2.4', '192.7142.56': '2019.2.5', '192.7142.79': '2019.2.6', '193.5233.109': '2019.3', '193.5662.61': '2019.3.1', '193.6015.41': '2019.3.2', '193.6494.30': '2019.3.3', '193.6911.25': '2019.3.4', '193.7288.30': '2019.3.5', '201.6668.115': '2020.1', '201.7223.92': '2020.1.1', '201.7846.77': '2020.1.2', '201.8538.36': '2020.1.3', '201.8743.11': '2020.1.4', '201.8743.20': '2020.1.5', '202.6397.98': '2020.2', '202.6948.78': '2020.2.1', '202.7319.64': '2020.2.2', '202.7660.27': '2020.2.3', '202.8194.15': '2020.2.4', '202.8194.22': '2020.2.5', '203.5981.165': '2020.3', '203.6682.86': '2020.3.1', '203.6682.179': '2020.3.2', '203.7148.72': '2020.3.3', '203.7717.65': '2020.3.4', '203.7717.81': '2020.3.5', '211.6693.115': '2021.1', '211.7142.13': '2021.1.1', '211.7442.45': '2021.1.2', '211.7628.24': '2021.1.3', '212.4746.96': '2021.2', '212.5080.64': '2021.2.1', '212.5284.44': '2021.2.2', '212.5457.59': '2021.2.3', } def __init__( self, product_code: typing.Union[str, typing.Iterable[str]], product_info_path: str = 'product-info.json', build_path: str = 'build.txt', ) -> None: """Initialize new :class:`~CheckVersion` instance.""" if isinstance(product_code, str): product_code = (product_code,) else: product_code = tuple(product_code) self.__product_code: 'typing_extensions.Final[typing.Tuple[str, ...]]' = product_code self.__product_info_path: 'typing_extensions.Final[str]' = product_info_path self.__build_path: 'typing_extensions.Final[str]' = build_path def __parse_product_info(self, root: str) -> typing.Optional[str]: """Parse `product-info.json` file for application version.""" with contextlib.suppress(BaseException): stream: typing.TextIO data: typing.Mapping[str, typing.Any] with open(os.path.join(root, self.__product_info_path), 'rt', encoding='utf-8') as stream: data = json.load(stream) if data['productCode'] in self.__product_code: return data['version'] return None def __parse_build(self, root: str) -> typing.Optional[str]: """Parse `build.txt` file for application version.""" with contextlib.suppress(BaseException): stream: typing.TextIO match: typing.Optional[typing.Match[str]] with open(os.path.join(root, self.__build_path), 'rt', encoding='utf-8') as stream: match = self.build_pattern.fullmatch(stream.read().strip()) if match and (match.group('product') in self.__product_code): build: str = match.group('build') return self.known_builds.get(build, f'build {build}') return None def __call__( self, parent: typing.Iterator[detectors.DetectedApplication], ) -> typing.Iterator[detectors.DetectedApplication]: """Iterate through detected applications.""" application: detectors.DetectedApplication for application in parent: if not application.root: continue version: typing.Optional[str] = self.__parse_product_info(application.root) if version is None: version = self.__parse_build(application.root) if version is not None: yield application.replace(version=version) class BasePyCharmApp(base.BaseInstallableApp): """Common parts for PyCharm applications.""" def __init__( # pylint: disable=too-many-arguments self, app_name: str, display_name: str, description: str, image_path: str, detector: 'detectors.Source', process_api: 'process.WorkerManager', config: 'user_config.UserConfig', ) -> None: """Initialize new :class:`~BasePyCharmApp` instance.""" super().__init__( app_name=app_name, display_name=display_name, description=description, image_path=image_path, detector=detector, is_available=navigator_config.BITS_64, process_api=process_api, config=config, extra_arguments=tuple(), ) def install_extensions(self) -> 'process.ProcessWorker': """Install app extensions.""" return self._process_api.create_process_worker(['python', '--version']) def update_config(self, prefix: str) -> None: """Update user config to use selected Python prefix interpreter.""" class PyCharmProApp(BasePyCharmApp): """PyCharm Professional application.""" def __init__(self, process_api: 'process.WorkerManager', config: 'user_config.UserConfig') -> None: """Initialize new :class:`~PyCharmProApp` instance.""" detector: 'typing_extensions.Final[detectors.Source]' = detectors.Group( detectors.Group( detectors.CheckConfiguredRoots('pycharm_pro_path', configuration=config), detectors.mac_only(), detectors.check_known_mac_roots('PyCharm.app'), detectors.Group( detectors.CheckKnownRoots(detectors.join(MAC_APPS, 'PyCharm-P')), detectors.StepIntoRoot(starts_with='ch-'), detectors.StepIntoRoot(reverse=True), detectors.StepIntoRoot(equals='PyCharm.app'), ), detectors.AppendExecutable( os.path.join('Contents', 'MacOS', 'pycharm'), ), CheckVersion( product_code='PY', product_info_path=os.path.join('Contents', 'Resources', 'product-info.json'), build_path=os.path.join('Contents', 'Resources', 'build.txt'), ), ), detectors.Group( detectors.CheckConfiguredRoots('pycharm_pro_path', configuration=config), detectors.linux_only(), detectors.Group( detectors.CheckKnownRoots( detectors.join(detectors.Linux.root, 'opt'), ), detectors.StepIntoRoot(starts_with='pycharm', reverse=True), ), detectors.Group( detectors.CheckKnownRoots( detectors.join(LINUX_APPS, 'PyCharm-P'), ), detectors.StepIntoRoot(starts_with='ch-'), detectors.StepIntoRoot(reverse=True), ), detectors.CheckKnownRoots( detectors.join(detectors.Linux.root, 'snap', 'pycharm-professional', 'current'), detectors.join( detectors.Linux.root, 'var', 'lib', 'snapd', 'snap', 'pycharm-professional', 'current' ), ), detectors.AppendExecutable(os.path.join('bin', 'pycharm.sh')), CheckVersion( product_code='PY', ), ), detectors.Group( detectors.CheckConfiguredRoots('pycharm_pro_path', configuration=config), detectors.win_only(), detectors.Group( detectors.CheckKnownRoots( detectors.join(detectors.Win.program_files_x64, 'JetBrains'), detectors.join(detectors.Win.program_files_x86, 'JetBrains'), ), detectors.StepIntoRoot(starts_with='PyCharm', reverse=True), ), detectors.Group( detectors.CheckKnownRoots( detectors.join(WIN_APPS, 'PyCharm-P'), ), detectors.StepIntoRoot(starts_with='ch-'), detectors.StepIntoRoot(reverse=True), ), detectors.AppendExecutable( os.path.join('bin', 'pycharm64.exe' if navigator_config.BITS_64 else 'pycharm32.exe'), os.path.join('bin', 'pycharm32.exe'), os.path.join('bin', 'pycharm.exe'), ), CheckVersion( product_code='PY', ), ), ) super().__init__( app_name='pycharm_pro', display_name='PyCharm Professional', description=( 'A full-fledged IDE by JetBrains for both Scientific and Web Python development. Supports HTML, JS, ' 'and SQL.' ), image_path=images.PYCHARM_ICON_1024_PATH, detector=detector, process_api=process_api, config=config, ) @staticmethod def install() -> None: """Install application.""" webbrowser.open_new_tab('https://www.anaconda.com/pycharm_navigator') class PyCharmCEApp(BasePyCharmApp): """PyCharm Community Edition application.""" def __init__(self, process_api: 'process.WorkerManager', config: 'user_config.UserConfig') -> None: """Initialize new :class:`~PyCharmCEApp` instance.""" detector: 'typing_extensions.Final[detectors.Source]' = detectors.Group( detectors.Group( detectors.CheckConfiguredRoots('pycharm_pro_path', configuration=config), detectors.mac_only(), detectors.check_known_mac_roots('PyCharm CE.app', 'PyCharm Edu.app'), detectors.Group( detectors.CheckKnownRoots( detectors.join(MAC_APPS, 'PyCharm-C'), detectors.join(MAC_APPS, 'PyCharm-E'), ), detectors.StepIntoRoot(starts_with='ch-'), detectors.StepIntoRoot(reverse=True), detectors.StepIntoRoot(equals=('PyCharm CE.app', 'PyCharm Edu.app')), ), detectors.AppendExecutable( os.path.join('Contents', 'MacOS', 'pycharm'), ), CheckVersion( product_code=('PC', 'PE'), product_info_path=os.path.join('Contents', 'Resources', 'product-info.json'), build_path=os.path.join('Contents', 'Resources', 'build.txt'), ), ), detectors.Group( detectors.CheckConfiguredRoots('pycharm_pro_path', configuration=config), detectors.linux_only(), detectors.Group( detectors.CheckKnownRoots( detectors.join(detectors.Linux.root, 'opt'), ), detectors.StepIntoRoot(starts_with='pycharm', reverse=True), ), detectors.Group( detectors.CheckKnownRoots( detectors.join(LINUX_APPS, 'PyCharm-C'), detectors.join(LINUX_APPS, 'PyCharm-E'), ), detectors.StepIntoRoot(starts_with='ch-'), detectors.StepIntoRoot(reverse=True), ), detectors.CheckKnownRoots( detectors.join(detectors.Linux.root, 'snap', 'pycharm-community', 'current'), detectors.join( detectors.Linux.root, 'var', 'lib', 'snapd', 'snap', 'pycharm-community', 'current' ), detectors.join(detectors.Linux.root, 'snap', 'pycharm-educational', 'current'), detectors.join( detectors.Linux.root, 'var', 'lib', 'snapd', 'snap', 'pycharm-educational', 'current' ), ), detectors.AppendExecutable(os.path.join('bin', 'pycharm.sh')), CheckVersion( product_code=('PC', 'PE'), ), ), detectors.Group( detectors.CheckConfiguredRoots('pycharm_pro_path', configuration=config), detectors.win_only(), detectors.Group( detectors.CheckKnownRoots( detectors.join(detectors.Win.program_files_x64, 'JetBrains'), detectors.join(detectors.Win.program_files_x86, 'JetBrains'), ), detectors.StepIntoRoot(starts_with='PyCharm', reverse=True), ), detectors.Group( detectors.CheckKnownRoots( detectors.join(WIN_APPS, 'PyCharm-C'), detectors.join(WIN_APPS, 'PyCharm-E'), ), detectors.StepIntoRoot(starts_with='ch-'), detectors.StepIntoRoot(reverse=True), ), detectors.AppendExecutable( os.path.join('bin', 'pycharm64.exe' if navigator_config.BITS_64 else 'pycharm32.exe'), os.path.join('bin', 'pycharm32.exe'), os.path.join('bin', 'pycharm.exe'), ), CheckVersion( product_code=('PC', 'PE'), ), ), ) super().__init__( app_name='pycharm_ce', display_name='PyCharm Community', description=( 'An IDE by JetBrains for pure Python development. Supports code completion, listing, and debugging.' ), image_path=images.PYCHARM_CE_ICON_1024_PATH, detector=detector, process_api=process_api, config=config, )