nbserver.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. """
  2. This module contains a Jupyter Server extension that attempts to
  3. make classic server and notebook extensions work in the new server.
  4. Unfortunately, you'll notice that requires some major monkey-patching.
  5. The goal is that this extension will only be used as a temporary
  6. patch to transition extension authors from classic notebook server to jupyter_server.
  7. """
  8. import os
  9. import types
  10. import inspect
  11. from functools import wraps
  12. from jupyter_core.paths import jupyter_config_path
  13. from traitlets.traitlets import is_trait
  14. from jupyter_server.services.config.manager import ConfigManager
  15. from .traits import NotebookAppTraits
  16. class ClassProxyError(Exception):
  17. pass
  18. def proxy(obj1, obj2, name, overwrite=False):
  19. """Redirects a method, property, or trait from object 1 to object 2."""
  20. if hasattr(obj1, name) and overwrite is False:
  21. raise ClassProxyError(
  22. "Cannot proxy the attribute '{name}' from {cls2} because "
  23. "{cls1} already has this attribute.".format(
  24. name=name,
  25. cls1=obj1.__class__,
  26. cls2=obj2.__class__
  27. )
  28. )
  29. attr = getattr(obj2, name)
  30. # First check if this thing is a trait (see traitlets)
  31. cls_attr = getattr(obj2.__class__, name)
  32. if is_trait(cls_attr) or type(attr) == property:
  33. thing = property(lambda self: getattr(obj2, name))
  34. elif isinstance(attr, types.MethodType):
  35. @wraps(attr)
  36. def thing(self, *args, **kwargs):
  37. return attr(*args, **kwargs)
  38. # Anything else appended on the class is just an attribute of the class.
  39. else:
  40. thing = attr
  41. setattr(obj1.__class__, name, thing)
  42. def public_members(obj):
  43. members = inspect.getmembers(obj)
  44. return [m for m, _ in members if not m.startswith('_')]
  45. def diff_members(obj1, obj2):
  46. """Return all attribute names found in obj2 but not obj1"""
  47. m1 = public_members(obj1)
  48. m2 = public_members(obj2)
  49. return set(m2).difference(m1)
  50. def get_nbserver_extensions(config_dirs):
  51. cm = ConfigManager(read_config_path=config_dirs)
  52. section = cm.get("jupyter_notebook_config")
  53. extensions = section.get('NotebookApp', {}).get('nbserver_extensions', {})
  54. return extensions
  55. def _link_jupyter_server_extension(serverapp):
  56. # Get the extension manager from the server
  57. manager = serverapp.extension_manager
  58. logger = serverapp.log
  59. # Hack that patches the enabled extensions list, prioritizing
  60. # jupyter nbclassic. In the future, it would be much better
  61. # to incorporate a dependency injection system in the
  62. # Extension manager that allows extensions to list
  63. # their dependency tree and sort that way.
  64. def sorted_extensions(self):
  65. """Dictionary with extension package names as keys
  66. and an ExtensionPackage objects as values.
  67. """
  68. # Sort the keys and
  69. keys = sorted(self.extensions.keys())
  70. keys.remove("notebook_shim")
  71. keys = ["notebook_shim"] + keys
  72. return {key: self.extensions[key] for key in keys}
  73. manager.__class__.sorted_extensions = property(sorted_extensions)
  74. # Look to see if nbclassic is enabled. if so,
  75. # link the nbclassic extension here to load
  76. # its config. Then, port its config to the serverapp
  77. # for backwards compatibility.
  78. try:
  79. pkg = manager.extensions["notebook_shim"]
  80. pkg.link_point("notebook_shim", serverapp)
  81. point = pkg.extension_points["notebook_shim"]
  82. nbapp = point.app
  83. except Exception:
  84. nbapp = NotebookAppTraits()
  85. # Proxy NotebookApp traits through serverapp to notebookapp.
  86. members = diff_members(serverapp, nbapp)
  87. for m in members:
  88. proxy(serverapp, nbapp, m)
  89. # Find jupyter server extensions listed as notebook server extensions.
  90. jupyter_paths = jupyter_config_path()
  91. config_dirs = jupyter_paths + [serverapp.config_dir]
  92. nbserver_extensions = get_nbserver_extensions(config_dirs)
  93. # Link all extensions found in the old locations for
  94. # notebook server extensions.
  95. for name, enabled in nbserver_extensions.items():
  96. # If the extension is already enabled in the manager, i.e.
  97. # because it was discovered already by Jupyter Server
  98. # through its jupyter_server_config, then don't re-enable here.
  99. if name not in manager.extensions:
  100. successful = manager.add_extension(name, enabled=enabled)
  101. if successful:
  102. logger.info(
  103. "{name} | extension was found and enabled by notebook_shim. "
  104. "Consider moving the extension to Jupyter Server's "
  105. "extension paths.".format(name=name)
  106. )
  107. manager.link_extension(name)
  108. def _load_jupyter_server_extension(serverapp):
  109. # Patch the config service manager to find the
  110. # proper path for old notebook frontend extensions
  111. config_manager = serverapp.config_manager
  112. read_config_path = config_manager.read_config_path
  113. read_config_path += [os.path.join(p, 'nbconfig')
  114. for p in jupyter_config_path()]
  115. config_manager.read_config_path = read_config_path