Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jupyter/jupyter_client
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v7.4.5
Choose a base ref
...
head repository: jupyter/jupyter_client
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v7.4.6
Choose a head ref
  • 2 commits
  • 6 files changed
  • 3 contributors

Commits on Nov 15, 2022

  1. Backport PR #879 on branch 7.x (Reconcile connection information) (#881)

    Co-authored-by: Kevin Bates <kbates4@gmail.com>
    meeseeksmachine and kevin-bates authored Nov 15, 2022

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    3394591 View commit details
  2. Publish 7.4.6

    SHA256 hashes:
    
    jupyter_client-7.4.6-py3-none-any.whl: 540b6a5c9c2dc481c5dd54fd5acb260f03dfaaa7c5325b2ffb1f676710f8c7c4
    
    jupyter_client-7.4.6.tar.gz: f7f9a9dc3a0ecd223ed6a5a00cf4140a5c252ec72e52d6de370748ed0aa083dd
    blink1073 committed Nov 15, 2022

    Unverified

    This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
    Copy the full SHA
    f71daff View commit details
Showing with 151 additions and 30 deletions.
  1. +16 −2 CHANGELOG.md
  2. +1 −1 jupyter_client/_version.py
  3. +63 −23 jupyter_client/connect.py
  4. +3 −2 jupyter_client/manager.py
  5. +66 −0 jupyter_client/tests/test_connect.py
  6. +2 −2 pyproject.toml
18 changes: 16 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,22 @@

<!-- <START NEW CHANGELOG ENTRY> -->

## 7.4.6

([Full Changelog](https://github.com/jupyter/jupyter_client/compare/v7.4.5...3394591f161be4a19f9e61c66ba510d7e29afd59))

### Bugs fixed

- Reconcile connection information [#879](https://github.com/jupyter/jupyter_client/pull/879) ([@kevin-bates](https://github.com/kevin-bates))

### Contributors to this release

([GitHub contributors page for this release](https://github.com/jupyter/jupyter_client/graphs/contributors?from=2022-11-10&to=2022-11-15&type=c))

[@meeseeksmachine](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_client+involves%3Ameeseeksmachine+updated%3A2022-11-10..2022-11-15&type=Issues)

<!-- <END NEW CHANGELOG ENTRY> -->

## 7.4.5

([Full Changelog](https://github.com/jupyter/jupyter_client/compare/v7.4.4...d27c8a497c6cbb1a232fbbe75cb1fd0f53faa9b0))
@@ -17,8 +33,6 @@

[@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_client+involves%3Ablink1073+updated%3A2022-10-25..2022-11-10&type=Issues)

<!-- <END NEW CHANGELOG ENTRY> -->

## 7.4.4

([Full Changelog](https://github.com/jupyter/jupyter_client/compare/v7.4.3...4029f6cad9223b1287980a1f0e966ff66557386e))
2 changes: 1 addition & 1 deletion jupyter_client/_version.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
from typing import List
from typing import Union

__version__ = "7.4.5"
__version__ = "7.4.6"

# Build up version_info tuple for backwards compatibility
pattern = r'(?P<major>\d+).(?P<minor>\d+).(?P<patch>\d+)(?P<rest>.*)'
86 changes: 63 additions & 23 deletions jupyter_client/connect.py
Original file line number Diff line number Diff line change
@@ -158,21 +158,9 @@ def write_connection_file(
cfg["signature_scheme"] = signature_scheme
cfg["kernel_name"] = kernel_name

# Prevent over-writing a file that has already been written with the same
# info. This is to prevent a race condition where the process has
# already been launched but has not yet read the connection file.
if os.path.exists(fname):
with open(fname) as f:
try:
data = json.load(f)
if data == cfg:
return fname, cfg
except Exception:
pass

# Only ever write this file as user read/writeable
# This would otherwise introduce a vulnerability as a file has secrets
# which would let others execute arbitrarily code as you
# which would let others execute arbitrary code as you
with secure_write(fname) as f:
f.write(json.dumps(cfg, indent=2))

@@ -579,18 +567,70 @@ def load_connection_info(self, info: KernelConnectionInfo) -> None:
if "signature_scheme" in info:
self.session.signature_scheme = info["signature_scheme"]

def _force_connection_info(self, info: KernelConnectionInfo) -> None:
"""Unconditionally loads connection info from a dict containing connection info.
def _reconcile_connection_info(self, info: KernelConnectionInfo) -> None:
"""Reconciles the connection information returned from the Provisioner.
Overwrites connection info-based attributes, regardless of their current values
and writes this information to the connection file.
Because some provisioners (like derivations of LocalProvisioner) may have already
written the connection file, this method needs to ensure that, if the connection
file exists, its contents match that of what was returned by the provisioner. If
the file does exist and its contents do not match, a ValueError is raised.
If the file does not exist, the connection information in 'info' is loaded into the
KernelManager and written to the file.
"""
# Reset current ports to 0 and indicate file has not been written to enable override
self._connection_file_written = False
for name in port_names:
setattr(self, name, 0)
self.load_connection_info(info)
self.write_connection_file()
# Prevent over-writing a file that has already been written with the same
# info. This is to prevent a race condition where the process has
# already been launched but has not yet read the connection file - as is
# the case with LocalProvisioners.
file_exists: bool = False
if os.path.exists(self.connection_file):
with open(self.connection_file) as f:
file_info = json.load(f)
# Prior to the following comparison, we need to adjust the value of "key" to
# be bytes, otherwise the comparison below will fail.
file_info["key"] = file_info["key"].encode()
if not self._equal_connections(info, file_info):
raise ValueError(
"Connection file already exists and does not match "
"the expected values returned from provisioner!"
)
file_exists = True

if not file_exists:
# Load the connection info and write out file. Note, this does not necessarily
# overwrite non-zero port values, so we'll validate afterward.
self.load_connection_info(info)
self.write_connection_file()

# Ensure what is in KernelManager is what we expect. This will catch issues if the file
# already existed, yet it's contents differed from the KernelManager's (and provisioner).
km_info = self.get_connection_info()
if not self._equal_connections(info, km_info):
raise ValueError(
"KernelManager's connection information already exists and does not match "
"the expected values returned from provisioner!"
)

@staticmethod
def _equal_connections(conn1: KernelConnectionInfo, conn2: KernelConnectionInfo) -> bool:
"""Compares pertinent keys of connection info data. Returns True if equivalent, False otherwise."""

pertinent_keys = [
"key",
"ip",
"stdin_port",
"iopub_port",
"shell_port",
"control_port",
"hb_port",
"transport",
"signature_scheme",
]

for key in pertinent_keys:
if conn1.get(key) != conn2.get(key):
return False
return True

# --------------------------------------------------------------------------
# Creating connected sockets
5 changes: 3 additions & 2 deletions jupyter_client/manager.py
Original file line number Diff line number Diff line change
@@ -310,8 +310,9 @@ async def _async_launch_kernel(self, kernel_cmd: t.List[str], **kw: t.Any) -> No
assert self.provisioner is not None
connection_info = await self.provisioner.launch_kernel(kernel_cmd, **kw)
assert self.provisioner.has_process
# Provisioner provides the connection information. Load into kernel manager and write file.
self._force_connection_info(connection_info)
# Provisioner provides the connection information. Load into kernel manager
# and write the connection file, if not already done.
self._reconcile_connection_info(connection_info)

_launch_kernel = run_sync(_async_launch_kernel)

66 changes: 66 additions & 0 deletions jupyter_client/tests/test_connect.py
Original file line number Diff line number Diff line change
@@ -5,11 +5,13 @@
import os
from tempfile import TemporaryDirectory

import pytest
from jupyter_core.application import JupyterApp
from jupyter_core.paths import jupyter_runtime_dir

from jupyter_client import connect
from jupyter_client import KernelClient
from jupyter_client import KernelManager
from jupyter_client.consoleapp import JupyterConsoleApp
from jupyter_client.session import Session

@@ -235,3 +237,67 @@ def test_mixin_cleanup_random_ports():
assert not os.path.exists(filename)
for name in dc._random_port_names:
assert getattr(dc, name) == 0


param_values = [
(True, True, None),
(True, False, ValueError),
(False, True, None),
(False, False, ValueError),
]


@pytest.mark.parametrize("file_exists, km_matches, expected_exception", param_values)
def test_reconcile_connection_info(file_exists, km_matches, expected_exception):

expected_info = sample_info
mismatched_info = sample_info.copy()
mismatched_info["key"] = b"def456"
mismatched_info["shell_port"] = expected_info["shell_port"] + 42
mismatched_info["control_port"] = expected_info["control_port"] + 42

with TemporaryDirectory() as connection_dir:

cf = os.path.join(connection_dir, "kernel.json")
km = KernelManager()
km.connection_file = cf

if file_exists:
_, info = connect.write_connection_file(cf, **expected_info)
info["key"] = info["key"].encode() # set 'key' back to bytes

if km_matches:
# Let this be the case where the connection file exists, and the KM has matching
# values prior to reconciliation. This is the LocalProvisioner case.
provisioner_info = info
km.load_connection_info(provisioner_info)
else:
# Let this be the case where the connection file exists, the KM has no values
# prior to reconciliation, but the provisioner has returned different values
# and a ValueError is expected.
provisioner_info = mismatched_info
else: # connection file does not exist
if km_matches:
# Let this be the case where the connection file does not exist, NOR does the KM
# have any values of its own and reconciliation sets those values. This is the
# non-LocalProvisioner case.
provisioner_info = expected_info
else:
# Let this be the case where the connection file does not exist, yet the KM
# has values that do not match those returned from the provisioner and a
# ValueError is expected.
km.load_connection_info(expected_info)
provisioner_info = mismatched_info

if expected_exception is None:
km._reconcile_connection_info(provisioner_info)
km_info = km.get_connection_info()
assert km._equal_connections(km_info, provisioner_info)
else:
with pytest.raises(expected_exception) as ex:
km._reconcile_connection_info(provisioner_info)
if file_exists:
assert "Connection file already exists" in str(ex.value)
else:
assert "KernelManager's connection information already exists" in str(ex.value)
assert km._equal_connections(km.get_connection_info(), provisioner_info) is False
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "jupyter_client"
version = "7.4.5"
version = "7.4.6"
description = "Jupyter protocol implementation and client libraries"
keywords = [ "Interactive", "Interpreter", "Shell", "Web",]
classifiers = [
@@ -93,7 +93,7 @@ skip = ["check-links"]
ignore = [".mailmap", "*.yml", "*.yaml"]

[tool.tbump.version]
current = "7.4.5"
current = "7.4.6"
regex = '''
(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
((?P<channel>a|b|rc|.dev)(?P<release>\d+))?