1# encoding: utf-8
2"""
3An application for managing IPython profiles.
4
5To be invoked as the `ipython profile` subcommand.
6
7Authors:
8
9* Min RK
10
11"""
12
13#-----------------------------------------------------------------------------
14#  Copyright (C) 2008  The IPython Development Team
15#
16#  Distributed under the terms of the BSD License.  The full license is in
17#  the file COPYING, distributed as part of this software.
18#-----------------------------------------------------------------------------
19
20#-----------------------------------------------------------------------------
21# Imports
22#-----------------------------------------------------------------------------
23
24import os
25
26from traitlets.config.application import Application
27from IPython.core.application import (
28    BaseIPythonApplication, base_flags
29)
30from IPython.core.profiledir import ProfileDir
31from IPython.utils.importstring import import_item
32from IPython.paths import get_ipython_dir, get_ipython_package_dir
33from traitlets import Unicode, Bool, Dict, observe
34
35#-----------------------------------------------------------------------------
36# Constants
37#-----------------------------------------------------------------------------
38
39create_help = """Create an IPython profile by name
40
41Create an ipython profile directory by its name or
42profile directory path. Profile directories contain
43configuration, log and security related files and are named
44using the convention 'profile_<name>'. By default they are
45located in your ipython directory. Once created, you will
46can edit the configuration files in the profile
47directory to configure IPython. Most users will create a
48profile directory by name,
49`ipython profile create myprofile`, which will put the directory
50in `<ipython_dir>/profile_myprofile`.
51"""
52list_help = """List available IPython profiles
53
54List all available profiles, by profile location, that can
55be found in the current working directly or in the ipython
56directory. Profile directories are named using the convention
57'profile_<profile>'.
58"""
59profile_help = """Manage IPython profiles
60
61Profile directories contain
62configuration, log and security related files and are named
63using the convention 'profile_<name>'. By default they are
64located in your ipython directory.  You can create profiles
65with `ipython profile create <name>`, or see the profiles you
66already have with `ipython profile list`
67
68To get started configuring IPython, simply do:
69
70$> ipython profile create
71
72and IPython will create the default profile in <ipython_dir>/profile_default,
73where you can edit ipython_config.py to start configuring IPython.
74
75"""
76
77_list_examples = "ipython profile list  # list all profiles"
78
79_create_examples = """
80ipython profile create foo         # create profile foo w/ default config files
81ipython profile create foo --reset # restage default config files over current
82ipython profile create foo --parallel # also stage parallel config files
83"""
84
85_main_examples = """
86ipython profile create -h  # show the help string for the create subcommand
87ipython profile list -h    # show the help string for the list subcommand
88
89ipython locate profile foo # print the path to the directory for profile 'foo'
90"""
91
92#-----------------------------------------------------------------------------
93# Profile Application Class (for `ipython profile` subcommand)
94#-----------------------------------------------------------------------------
95
96
97def list_profiles_in(path):
98    """list profiles in a given root directory"""
99    profiles = []
100
101    # for python 3.6+ rewrite to: with os.scandir(path) as dirlist:
102    files = os.scandir(path)
103    for f in files:
104        if f.is_dir() and f.name.startswith('profile_'):
105            profiles.append(f.name.split('_', 1)[-1])
106    return profiles
107
108
109def list_bundled_profiles():
110    """list profiles that are bundled with IPython."""
111    path = os.path.join(get_ipython_package_dir(), u'core', u'profile')
112    profiles = []
113
114    # for python 3.6+ rewrite to: with os.scandir(path) as dirlist:
115    files =  os.scandir(path)
116    for profile in files:
117        if profile.is_dir() and profile.name != "__pycache__":
118            profiles.append(profile.name)
119    return profiles
120
121
122class ProfileLocate(BaseIPythonApplication):
123    description = """print the path to an IPython profile dir"""
124
125    def parse_command_line(self, argv=None):
126        super(ProfileLocate, self).parse_command_line(argv)
127        if self.extra_args:
128            self.profile = self.extra_args[0]
129
130    def start(self):
131        print(self.profile_dir.location)
132
133
134class ProfileList(Application):
135    name = u'ipython-profile'
136    description = list_help
137    examples = _list_examples
138
139    aliases = Dict({
140        'ipython-dir' : 'ProfileList.ipython_dir',
141        'log-level' : 'Application.log_level',
142    })
143    flags = Dict(dict(
144        debug = ({'Application' : {'log_level' : 0}},
145            "Set Application.log_level to 0, maximizing log output."
146        )
147    ))
148
149    ipython_dir = Unicode(get_ipython_dir(),
150        help="""
151        The name of the IPython directory. This directory is used for logging
152        configuration (through profiles), history storage, etc. The default
153        is usually $HOME/.ipython. This options can also be specified through
154        the environment variable IPYTHONDIR.
155        """
156    ).tag(config=True)
157
158
159    def _print_profiles(self, profiles):
160        """print list of profiles, indented."""
161        for profile in profiles:
162            print('    %s' % profile)
163
164    def list_profile_dirs(self):
165        profiles = list_bundled_profiles()
166        if profiles:
167            print()
168            print("Available profiles in IPython:")
169            self._print_profiles(profiles)
170            print()
171            print("    The first request for a bundled profile will copy it")
172            print("    into your IPython directory (%s)," % self.ipython_dir)
173            print("    where you can customize it.")
174
175        profiles = list_profiles_in(self.ipython_dir)
176        if profiles:
177            print()
178            print("Available profiles in %s:" % self.ipython_dir)
179            self._print_profiles(profiles)
180
181        profiles = list_profiles_in(os.getcwd())
182        if profiles:
183            print()
184            print("Available profiles in current directory (%s):" % os.getcwd())
185            self._print_profiles(profiles)
186
187        print()
188        print("To use any of the above profiles, start IPython with:")
189        print("    ipython --profile=<name>")
190        print()
191
192    def start(self):
193        self.list_profile_dirs()
194
195
196create_flags = {}
197create_flags.update(base_flags)
198# don't include '--init' flag, which implies running profile create in other apps
199create_flags.pop('init')
200create_flags['reset'] = ({'ProfileCreate': {'overwrite' : True}},
201                        "reset config files in this profile to the defaults.")
202create_flags['parallel'] = ({'ProfileCreate': {'parallel' : True}},
203                        "Include the config files for parallel "
204                        "computing apps (ipengine, ipcontroller, etc.)")
205
206
207class ProfileCreate(BaseIPythonApplication):
208    name = u'ipython-profile'
209    description = create_help
210    examples = _create_examples
211    auto_create = Bool(True)
212    def _log_format_default(self):
213        return "[%(name)s] %(message)s"
214
215    def _copy_config_files_default(self):
216        return True
217
218    parallel = Bool(False,
219        help="whether to include parallel computing config files"
220    ).tag(config=True)
221
222    @observe('parallel')
223    def _parallel_changed(self, change):
224        parallel_files = [   'ipcontroller_config.py',
225                            'ipengine_config.py',
226                            'ipcluster_config.py'
227                        ]
228        if change['new']:
229            for cf in parallel_files:
230                self.config_files.append(cf)
231        else:
232            for cf in parallel_files:
233                if cf in self.config_files:
234                    self.config_files.remove(cf)
235
236    def parse_command_line(self, argv):
237        super(ProfileCreate, self).parse_command_line(argv)
238        # accept positional arg as profile name
239        if self.extra_args:
240            self.profile = self.extra_args[0]
241
242    flags = Dict(create_flags)
243
244    classes = [ProfileDir]
245
246    def _import_app(self, app_path):
247        """import an app class"""
248        app = None
249        name = app_path.rsplit('.', 1)[-1]
250        try:
251            app = import_item(app_path)
252        except ImportError:
253            self.log.info("Couldn't import %s, config file will be excluded", name)
254        except Exception:
255            self.log.warning('Unexpected error importing %s', name, exc_info=True)
256        return app
257
258    def init_config_files(self):
259        super(ProfileCreate, self).init_config_files()
260        # use local imports, since these classes may import from here
261        from IPython.terminal.ipapp import TerminalIPythonApp
262        apps = [TerminalIPythonApp]
263        for app_path in (
264            'ipykernel.kernelapp.IPKernelApp',
265        ):
266            app = self._import_app(app_path)
267            if app is not None:
268                apps.append(app)
269        if self.parallel:
270            from ipyparallel.apps.ipcontrollerapp import IPControllerApp
271            from ipyparallel.apps.ipengineapp import IPEngineApp
272            from ipyparallel.apps.ipclusterapp import IPClusterStart
273            apps.extend([
274                IPControllerApp,
275                IPEngineApp,
276                IPClusterStart,
277            ])
278        for App in apps:
279            app = App()
280            app.config.update(self.config)
281            app.log = self.log
282            app.overwrite = self.overwrite
283            app.copy_config_files=True
284            app.ipython_dir=self.ipython_dir
285            app.profile_dir=self.profile_dir
286            app.init_config_files()
287
288    def stage_default_config_file(self):
289        pass
290
291
292class ProfileApp(Application):
293    name = u'ipython profile'
294    description = profile_help
295    examples = _main_examples
296
297    subcommands = Dict(dict(
298        create = (ProfileCreate, ProfileCreate.description.splitlines()[0]),
299        list = (ProfileList, ProfileList.description.splitlines()[0]),
300        locate = (ProfileLocate, ProfileLocate.description.splitlines()[0]),
301    ))
302
303    def start(self):
304        if self.subapp is None:
305            print("No subcommand specified. Must specify one of: %s"%(self.subcommands.keys()))
306            print()
307            self.print_description()
308            self.print_subcommands()
309            self.exit(1)
310        else:
311            return self.subapp.start()
312