Skip to content

Commit

Permalink
Merge pull request #17 from fcollonval/kernel-companions
Browse files Browse the repository at this point in the history
Kernel companions
  • Loading branch information
fcollonval authored Sep 2, 2019
2 parents de60d5f + f96ab23 commit ff92155
Show file tree
Hide file tree
Showing 19 changed files with 805 additions and 107 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,21 @@ jupyter labextension install .

## Changelog

### 3.1.0

- Add ability to specify kernel companions; i.e. check that if some packages are installed in a
kernel, they must respect a certain version range. Companions can be specified through user
settings.
- `IEnvironmentManager.getPackageManager()` returns always the same `Conda.IPackageManager`
otherwise signaling package operations would have been meaningless.
- Request environment list access now `whitelist`=0 or 1 query arguments. If 1, the environment
list is filtered to respect `KernelSpecManager.whitelist`. Default is 0, but it could be modified
in user settings.
- Small UI tweaks

### 3.0.0

- Rework the server/client API to be more REST
- Rework the server/client API to be more REST and returns 202 status for long operations
- Cache available packages list in temp directory
- Improve greatly the coverage for the server extension
- JupyterLab extension only:
Expand Down
2 changes: 1 addition & 1 deletion jupyter_conda/_version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version_info = (3, 0, 0)
version_info = (3, 1, 0)
__version__ = ".".join(map(str, version_info))
34 changes: 27 additions & 7 deletions jupyter_conda/envmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from packaging.version import parse
from subprocess import Popen, PIPE

from nb_conda_kernels.manager import RUNNER_COMMAND
import tornado
from traitlets.config.configurable import LoggingConfigurable

Expand Down Expand Up @@ -43,7 +44,7 @@ def normalize_pkg_info(s: Dict[str, Any]) -> Dict[str, Union[str, List[str]]]:
"name": s.get("name"),
"platform": s.get("platform"),
"version": s.get("version"),
"summary": s.get("summary", ""),
"summary": s.get("summary", ""),
"home": s.get("home", ""),
"keywords": s.get("keywords", []),
"tags": s.get("tags", []),
Expand Down Expand Up @@ -101,8 +102,14 @@ def _execute(self, cmd: str, *args) -> Tuple[int, str]:
return returncode, output

@tornado.gen.coroutine
def list_envs(self) -> Dict[str, List[Dict[str, Union[str, bool]]]]:
def list_envs(
self, whitelist: bool = False
) -> Dict[str, List[Dict[str, Union[str, bool]]]]:
"""List all environments that conda knows about.
Args:
whitelist (bool): optional, filter the environment list to respect
KernelSpecManager.whitelist (default: False)
An environment is described by a dictionary:
{
Expand All @@ -128,6 +135,17 @@ def list_envs(self) -> Dict[str, List[Dict[str, Union[str, bool]]]]:
"is_default": info["root_prefix"] == default_env,
}

whitelist_env = set()
if whitelist:
# Build env path list - simplest way to compare kernel and environment
for entry in self.parent.kernel_spec_manager.get_all_specs().values():
spec = entry["spec"]
argv = spec.get("argv", [])
if 'conda_env_path' in spec["metadata"]:
whitelist_env.add(spec["metadata"]["conda_env_path"])
elif argv[:3] == RUNNER_COMMAND and len(argv[4]) > 0:
whitelist_env.add(argv[4])

def get_info(env):
base_dir = os.path.dirname(env)
if base_dir not in info["envs_dirs"]:
Expand All @@ -142,7 +160,9 @@ def get_info(env):
envs_list = [root_env]
for env in info["envs"]:
env_info = get_info(env)
if env_info is not None:
if env_info is not None and (
len(whitelist_env) == 0 or env_info["dir"] in whitelist_env
):
envs_list.append(env_info)

return {"environments": envs_list}
Expand Down Expand Up @@ -265,7 +285,7 @@ def import_env(
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=file_name) as f:
name = f.name
f.write(file_content)

ans = yield self._execute(
CONDA_EXE, "env", "create", "-q", "--json", "-n", env, "--file", name
)
Expand Down Expand Up @@ -455,7 +475,7 @@ def list_available(self) -> Dict[str, List[Dict[str, str]]]:
tr_channels = {}
for short_name, channel in channels.items():
tr_channels.update({uri: short_name for uri in channel})

# # Get top channel URI to request channeldata.json
# top_channels = set()
# for uri in tr_channels:
Expand Down Expand Up @@ -546,7 +566,7 @@ def list_available(self) -> Dict[str, List[Dict[str, str]]]:

return {
"packages": sorted(packages, key=lambda entry: entry.get("name")),
"with_description": len(pkg_info) > 0
"with_description": len(pkg_info) > 0,
}

@tornado.gen.coroutine
Expand Down Expand Up @@ -588,7 +608,7 @@ def package_search(self, q: str) -> Dict[str, List]:

return {
"packages": sorted(packages, key=lambda entry: entry.get("name")),
"with_description": False
"with_description": False,
}

@tornado.gen.coroutine
Expand Down
14 changes: 11 additions & 3 deletions jupyter_conda/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,14 @@ class EnvironmentsHandler(EnvBaseHandler):
def get(self):
"""`GET /environments` which lists the environments.
Query arguments:
whitelist (int): optional flag 0 or 1 to respect KernelSpecManager.whitelist
Raises:
500 if an error occurs
"""
list_envs = yield self.env_manager.list_envs()
whitelist = self.get_query_argument("whitelist", 0)
list_envs = yield self.env_manager.list_envs(int(whitelist) == 1)
if "error" in list_envs:
self.set_status(500)
self.finish(tornado.escape.json_encode(list_envs))
Expand Down Expand Up @@ -351,7 +355,9 @@ def get(self):
)

@tornado.gen.coroutine
def update_available(env_manager: EnvManager, cache_file: str, return_packages: bool = True) -> Dict:
def update_available(
env_manager: EnvManager, cache_file: str, return_packages: bool = True
) -> Dict:
answer = yield env_manager.list_available()
try:
with open(cache_file, "w+") as cache:
Expand All @@ -374,7 +380,9 @@ def update_available(env_manager: EnvManager, cache_file: str, return_packages:
# Request cache update in background
if not PackagesHandler.__is_listing_available:
PackagesHandler.__is_listing_available = True
self._stack.put(update_available, self.env_manager, cache_file, False)
self._stack.put(
update_available, self.env_manager, cache_file, False
)
# Return current cache
self.set_status(200)
self.finish(cache_data)
Expand Down
38 changes: 36 additions & 2 deletions jupyter_conda/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import uuid
import tempfile

from nb_conda_kernels import CondaKernelSpecManager
import tornado
from traitlets.config import Config

from jupyter_conda.handlers import AVAILABLE_CACHE, PackagesHandler
from jupyter_conda.tests.utils import ServerTest, assert_http_error
Expand Down Expand Up @@ -37,7 +39,7 @@ def wait_for_task(self, call, *args, **kwargs):
self.assertRegex(location, r"^/conda/tasks/\d+$")
return self.wait_task(location)

def mk_env(self, name=None):
def mk_env(self, name=None, packages=None):
envs = self.conda_api.envs()
env_names = map(lambda env: env["name"], envs["environments"])
new_name = name or generate_name()
Expand All @@ -46,7 +48,7 @@ def mk_env(self, name=None):
self.env_names.append(new_name)

return self.conda_api.post(
["environments"], body={"name": new_name, "packages": ["python"]}
["environments"], body={"name": new_name, "packages": packages or ["python"]}
)

def rm_env(self, name):
Expand Down Expand Up @@ -327,6 +329,38 @@ def g():
for p in expected:
self.assertIn(p, packages, "{} not found.".format(p))

class TestEnvironmentsHandlerWhiteList(JupyterCondaAPITest):

@mock.patch("nb_conda_kernels.manager.CACHE_TIMEOUT", 0)
def test_get_whitelist(self):
n = "banana"
self.wait_for_task(self.mk_env, n, packages=["ipykernel", ])
manager = CondaKernelSpecManager()
manager.whitelist = set(["conda-env-banana-py", ])
TestEnvironmentsHandlerWhiteList.notebook.kernel_spec_manager = manager

r = self.conda_api.get(["environments", ], params={"whitelist": 1})
self.assertEqual(r.status_code, 200)
envs = r.json()
env = None
for e in envs["environments"]:
if n == e["name"]:
env = e
break
self.assertIsNotNone(env)
self.assertEqual(env["name"], n)
self.assertTrue(os.path.isdir(env["dir"]))
self.assertFalse(env["is_default"])
found_env = len(envs["environments"])
self.assertEqual(found_env, 2)

n = generate_name()
self.wait_for_task(self.mk_env, n)
r = self.conda_api.get(["environments", ], params={"whitelist": 1})
self.assertEqual(r.status_code, 200)
envs = r.json()
self.assertEqual(len(envs["environments"]), found_env)


class TestEnvironmentHandler(JupyterCondaAPITest):
def test_delete(self):
Expand Down
11 changes: 8 additions & 3 deletions labextension/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jupyterlab_conda",
"version": "1.0.0",
"version": "1.1.0",
"description": "Manage your conda environments from JupyterLab",
"keywords": [
"jupyter",
Expand Down Expand Up @@ -31,7 +31,7 @@
"clean": "rimraf lib && rimraf tsconfig.tsbuildinfo",
"watch": "tsc -w",
"precommit": "pretty-quick --staged",
"prepare": "npm run clean && npm run build",
"prepare": "jlpm run clean && jlpm run build",
"prettier": "npx prettier --write \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
"test": "jest"
},
Expand All @@ -46,6 +46,7 @@
"@phosphor/signaling": "^1.2.0",
"@phosphor/widgets": "^1.6.0",
"react-virtualized": "^9.21.1",
"semver": "^6.3.0",
"typestyle": "^2.0.0"
},
"devDependencies": {
Expand All @@ -55,17 +56,21 @@
"@types/jest": "^24",
"@types/react": "~16.8.0",
"@types/react-virtualized": "^9.21.4",
"@types/semver": "^6.0.1",
"husky": "^0.14.0",
"jest": "^24",
"jest-fetch-mock": "^2.1.2",
"jupyterlab_toastify": "^2.3.0",
"jupyterlab_toastify": "^2.3.2",
"prettier": "~1.14.2",
"pretty-quick": "~1.6.0",
"react": "~16.8.0",
"rimraf": "~2.6.1",
"ts-jest": "^24",
"typescript": "~3.5.2"
},
"sideEffects": [
"style/*.css"
],
"jupyterlab": {
"extension": true,
"schemaDir": "schema",
Expand Down
17 changes: 17 additions & 0 deletions labextension/schema/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
"title": "Conda",
"description": "Environments and packages manager settings.",
"properties": {
"whitelist": {
"type": "boolean",
"title": "Only kernel whitelist",
"description": "Show only environment corresponding to whitelisted kernels",
"default": false
},
"types": {
"type": "object",
"title": "Environment types",
Expand All @@ -22,6 +28,17 @@
}
}
}
},
"companions": {
"type": "object",
"title": "Kernel companions",
"description": "{'package name': 'semver specification'} - pre and post releases not supported",
"default": {},
"properties": {
"^\\w[\\w\\-]*$": {
"type": "string"
}
}
}
},
"additionalProperties": false,
Expand Down
Loading

0 comments on commit ff92155

Please sign in to comment.