Fix all typing issues

This commit is contained in:
Ayaz Salikhov
2022-01-23 12:44:16 +03:00
parent 013a42fff3
commit 37c510fc8e
25 changed files with 184 additions and 129 deletions

View File

@@ -17,6 +17,13 @@ repos:
- id: black - id: black
args: [--target-version=py39] args: [--target-version=py39]
# Check python code static typing
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.931
hooks:
- id: mypy
additional_dependencies: ["pytest", "types-requests", "types-tabulate"]
# Autoformat: YAML, JSON, Markdown, etc. # Autoformat: YAML, JSON, Markdown, etc.
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.5.1 rev: v2.5.1

View File

@@ -3,7 +3,7 @@
import logging import logging
import pytest import pytest # type: ignore
from pathlib import Path from pathlib import Path
from conftest import TrackedContainer from conftest import TrackedContainer

View File

@@ -1,12 +1,12 @@
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
# mypy: ignore-errors
from jupyter_core.paths import jupyter_data_dir from jupyter_core.paths import jupyter_data_dir
import subprocess import subprocess
import os import os
import errno
import stat import stat
c = get_config() # noqa: F821 c = get_config() # noqa: F821
c.NotebookApp.ip = "0.0.0.0" c.NotebookApp.ip = "0.0.0.0"
c.NotebookApp.port = 8888 c.NotebookApp.port = 8888
@@ -16,28 +16,21 @@ c.NotebookApp.open_browser = False
c.FileContentsManager.delete_to_trash = False c.FileContentsManager.delete_to_trash = False
# Generate a self-signed certificate # Generate a self-signed certificate
OPENSSL_CONFIG = """\
[req]
distinguished_name = req_distinguished_name
[req_distinguished_name]
"""
if "GEN_CERT" in os.environ: if "GEN_CERT" in os.environ:
dir_name = jupyter_data_dir() dir_name = jupyter_data_dir()
pem_file = os.path.join(dir_name, "notebook.pem") pem_file = os.path.join(dir_name, "notebook.pem")
try: os.makedirs(dir_name, exist_ok=True)
os.makedirs(dir_name)
except OSError as exc: # Python >2.5
if exc.errno == errno.EEXIST and os.path.isdir(dir_name):
pass
else:
raise
# Generate an openssl.cnf file to set the distinguished name # Generate an openssl.cnf file to set the distinguished name
cnf_file = os.path.join(os.getenv("CONDA_DIR", "/usr/lib"), "ssl", "openssl.cnf") cnf_file = os.path.join(os.getenv("CONDA_DIR", "/usr/lib"), "ssl", "openssl.cnf")
if not os.path.isfile(cnf_file): if not os.path.isfile(cnf_file):
with open(cnf_file, "w") as fh: with open(cnf_file, "w") as fh:
fh.write( fh.write(OPENSSL_CONFIG)
"""\
[req]
distinguished_name = req_distinguished_name
[req_distinguished_name]
"""
)
# Generate a certificate if one doesn't exist on disk # Generate a certificate if one doesn't exist on disk
subprocess.check_call( subprocess.check_call(

View File

@@ -4,7 +4,7 @@ import pathlib
import time import time
import logging import logging
import pytest import pytest # type: ignore
import requests import requests
from conftest import TrackedContainer from conftest import TrackedContainer
@@ -303,6 +303,6 @@ def test_jupyter_env_vars_to_unset_as_root(
"-c", "-c",
"echo I like $FRUIT and ${SECRET_FRUIT:-stuff}, and love ${SECRET_ANIMAL:-to keep secrets}!", "echo I like $FRUIT and ${SECRET_FRUIT:-stuff}, and love ${SECRET_ANIMAL:-to keep secrets}!",
], ],
**root_args, **root_args, # type: ignore
) )
assert "I like bananas and stuff, and love to keep secrets!" in logs assert "I like bananas and stuff, and love to keep secrets!" in logs

View File

@@ -2,7 +2,7 @@
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
import logging import logging
import pytest import pytest # type: ignore
from conftest import TrackedContainer from conftest import TrackedContainer

View File

@@ -2,7 +2,7 @@
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
import logging import logging
from packaging import version from packaging import version # type: ignore
from conftest import TrackedContainer from conftest import TrackedContainer

View File

@@ -3,7 +3,7 @@
import logging import logging
from typing import Optional from typing import Optional
import pytest import pytest # type: ignore
import requests import requests
import time import time

View File

@@ -2,11 +2,11 @@
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
import os import os
import logging import logging
import typing from typing import Any, Optional
import docker import docker
from docker.models.containers import Container from docker.models.containers import Container
import pytest import pytest # type: ignore
import requests import requests
from requests.packages.urllib3.util.retry import Retry from requests.packages.urllib3.util.retry import Retry
@@ -35,7 +35,7 @@ def docker_client() -> docker.DockerClient:
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def image_name() -> str: def image_name() -> str:
"""Image name to test""" """Image name to test"""
return os.getenv("TEST_IMAGE") return os.environ["TEST_IMAGE"]
class TrackedContainer: class TrackedContainer:
@@ -56,14 +56,14 @@ class TrackedContainer:
self, self,
docker_client: docker.DockerClient, docker_client: docker.DockerClient,
image_name: str, image_name: str,
**kwargs: typing.Any, **kwargs: Any,
): ):
self.container = None self.container: Optional[Container] = None
self.docker_client = docker_client self.docker_client: docker.DockerClient = docker_client
self.image_name = image_name self.image_name: str = image_name
self.kwargs = kwargs self.kwargs: Any = kwargs
def run_detached(self, **kwargs: typing.Any) -> Container: def run_detached(self, **kwargs: Any) -> Container:
"""Runs a docker container using the preconfigured image name """Runs a docker container using the preconfigured image name
and a mix of the preconfigured container options and those passed and a mix of the preconfigured container options and those passed
to this method. to this method.
@@ -94,11 +94,12 @@ class TrackedContainer:
timeout: int, timeout: int,
no_warnings: bool = True, no_warnings: bool = True,
no_errors: bool = True, no_errors: bool = True,
**kwargs: typing.Any, **kwargs: Any,
) -> str: ) -> str:
running_container = self.run_detached(**kwargs) running_container = self.run_detached(**kwargs)
rv = running_container.wait(timeout=timeout) rv = running_container.wait(timeout=timeout)
logs = running_container.logs().decode("utf-8") logs = running_container.logs().decode("utf-8")
assert isinstance(logs, str)
LOGGER.debug(logs) LOGGER.debug(logs)
if no_warnings: if no_warnings:
assert not self.get_warnings(logs) assert not self.get_warnings(logs)
@@ -119,14 +120,14 @@ class TrackedContainer:
def _lines_starting_with(logs: str, pattern: str) -> list[str]: def _lines_starting_with(logs: str, pattern: str) -> list[str]:
return [line for line in logs.splitlines() if line.startswith(pattern)] return [line for line in logs.splitlines() if line.startswith(pattern)]
def remove(self): def remove(self) -> None:
"""Kills and removes the tracked docker container.""" """Kills and removes the tracked docker container."""
if self.container: if self.container:
self.container.remove(force=True) self.container.remove(force=True)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def container(docker_client: docker.DockerClient, image_name: str): def container(docker_client: docker.DockerClient, image_name: str) -> Container:
"""Notebook container with initial configuration appropriate for testing """Notebook container with initial configuration appropriate for testing
(e.g., HTTP port exposed to the host for HTTP calls). (e.g., HTTP port exposed to the host for HTTP calls).

View File

@@ -3,7 +3,7 @@
import logging import logging
import pytest import pytest # type: ignore
from pathlib import Path from pathlib import Path
from conftest import TrackedContainer from conftest import TrackedContainer

26
mypy.ini Normal file
View File

@@ -0,0 +1,26 @@
[mypy]
python_version = 3.9
follow_imports = normal
strict = False
no_incremental = True
[mypy-docker.*]
ignore_missing_imports = True
[mypy-matplotlib.*]
ignore_missing_imports = True
[mypy-packaging.*]
ignore_missing_imports = True
[mypy-pandas.*]
ignore_missing_imports = True
[mypy-plumbum.*]
ignore_missing_imports = True
[mypy-pyspark.*]
ignore_missing_imports = True
[mypy-tensorflow.*]
ignore_missing_imports = True

View File

@@ -10,4 +10,5 @@
# Attempt to capture and forward low-level output, e.g. produced by Extension # Attempt to capture and forward low-level output, e.g. produced by Extension
# libraries. # libraries.
# Default: True # Default: True
# type:ignore
c.IPKernelApp.capture_fd_output = False # noqa: F821 c.IPKernelApp.capture_fd_output = False # noqa: F821

View File

@@ -2,7 +2,7 @@
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
import logging import logging
import pytest import pytest # type: ignore
from conftest import TrackedContainer from conftest import TrackedContainer

View File

@@ -3,7 +3,7 @@
import logging import logging
import pytest import pytest # type: ignore
from pathlib import Path from pathlib import Path
from conftest import TrackedContainer from conftest import TrackedContainer
@@ -29,7 +29,7 @@ THIS_DIR = Path(__file__).parent.resolve()
) )
def test_matplotlib( def test_matplotlib(
container: TrackedContainer, test_file: str, expected_file: str, description: str container: TrackedContainer, test_file: str, expected_file: str, description: str
): ) -> None:
"""Various tests performed on matplotlib """Various tests performed on matplotlib
- Test that matplotlib is able to plot a graph and write it as an image - Test that matplotlib is able to plot a graph and write it as an image

View File

@@ -5,6 +5,7 @@ import argparse
import datetime import datetime
import logging import logging
import os import os
from docker.models.containers import Container
from .docker_runner import DockerRunner from .docker_runner import DockerRunner
from .get_taggers_and_manifests import get_taggers_and_manifests from .get_taggers_and_manifests import get_taggers_and_manifests
from .git_helper import GitHelper from .git_helper import GitHelper
@@ -55,9 +56,9 @@ def create_manifest_file(
owner: str, owner: str,
wiki_path: str, wiki_path: str,
manifests: list[ManifestInterface], manifests: list[ManifestInterface],
container, container: Container,
) -> None: ) -> None:
manifest_names = [manifest.__name__ for manifest in manifests] manifest_names = [manifest.__class__.__name__ for manifest in manifests]
LOGGER.info(f"Using manifests: {manifest_names}") LOGGER.info(f"Using manifests: {manifest_names}")
commit_hash_tag = GitHelper.commit_hash_tag() commit_hash_tag = GitHelper.commit_hash_tag()

View File

@@ -1,6 +1,7 @@
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
from typing import Optional from typing import Optional
from types import TracebackType
import docker import docker
from docker.models.containers import Container from docker.models.containers import Container
import logging import logging
@@ -13,7 +14,7 @@ class DockerRunner:
def __init__( def __init__(
self, self,
image_name: str, image_name: str,
docker_client=docker.from_env(), docker_client: docker.DockerClient = docker.from_env(),
command: str = "sleep infinity", command: str = "sleep infinity",
): ):
self.container: Optional[Container] = None self.container: Optional[Container] = None
@@ -31,7 +32,13 @@ class DockerRunner:
LOGGER.info(f"Container {self.container.name} created") LOGGER.info(f"Container {self.container.name} created")
return self.container return self.container
def __exit__(self, exc_type, exc_value, traceback) -> None: def __exit__(
self,
exc_type: Optional[type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
assert self.container is not None
LOGGER.info(f"Removing container {self.container.name} ...") LOGGER.info(f"Removing container {self.container.name} ...")
if self.container: if self.container:
self.container.remove(force=True) self.container.remove(force=True)
@@ -44,6 +51,7 @@ class DockerRunner:
LOGGER.info(f"Running cmd: '{cmd}' on container: {container}") LOGGER.info(f"Running cmd: '{cmd}' on container: {container}")
out = container.exec_run(cmd) out = container.exec_run(cmd)
result = out.output.decode("utf-8").rstrip() result = out.output.decode("utf-8").rstrip()
assert isinstance(result, str)
if print_result: if print_result:
LOGGER.info(f"Command result: {result}") LOGGER.info(f"Command result: {result}")
assert out.exit_code == 0, f"Command: {cmd} failed" assert out.exit_code == 0, f"Command: {cmd} failed"

View File

@@ -1,20 +1,22 @@
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
from typing import Optional
from .images_hierarchy import ALL_IMAGES from .images_hierarchy import ALL_IMAGES
from .manifests import ManifestInterface from .manifests import ManifestInterface
from .taggers import TaggerInterface from .taggers import TaggerInterface
def get_taggers_and_manifests( def get_taggers_and_manifests(
short_image_name: str, short_image_name: Optional[str],
) -> tuple[list[TaggerInterface], list[ManifestInterface]]: ) -> tuple[list[TaggerInterface], list[ManifestInterface]]:
taggers: list[TaggerInterface] = [] if short_image_name is None:
manifests: list[ManifestInterface] = [] return [[], []] # type: ignore
while short_image_name is not None:
image_description = ALL_IMAGES[short_image_name]
taggers = image_description.taggers + taggers image_description = ALL_IMAGES[short_image_name]
manifests = image_description.manifests + manifests parent_taggers, parent_manifests = get_taggers_and_manifests(
image_description.parent_image
short_image_name = image_description.parent_image )
return taggers, manifests return (
parent_taggers + image_description.taggers,
parent_manifests + image_description.manifests,
)

View File

@@ -7,7 +7,7 @@ from plumbum.cmd import git
class GitHelper: class GitHelper:
@staticmethod @staticmethod
def commit_hash() -> str: def commit_hash() -> str:
return git["rev-parse", "HEAD"]().strip() return git["rev-parse", "HEAD"]().strip() # type: ignore
@staticmethod @staticmethod
def commit_hash_tag() -> str: def commit_hash_tag() -> str:
@@ -15,7 +15,7 @@ class GitHelper:
@staticmethod @staticmethod
def commit_message() -> str: def commit_message() -> str:
return git["log", -1, "--pretty=%B"]().strip() return git["log", -1, "--pretty=%B"]().strip() # type: ignore
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -3,7 +3,7 @@
import os import os
def github_set_env(env_name, env_value): def github_set_env(env_name: str, env_value: str) -> None:
if not os.environ.get("GITHUB_ACTIONS") or not os.environ.get("GITHUB_ENV"): if not os.environ.get("GITHUB_ACTIONS") or not os.environ.get("GITHUB_ENV"):
return return

View File

@@ -39,39 +39,39 @@ ALL_IMAGES = {
"base-notebook": ImageDescription( "base-notebook": ImageDescription(
parent_image=None, parent_image=None,
taggers=[ taggers=[
SHATagger, SHATagger(),
DateTagger, DateTagger(),
UbuntuVersionTagger, UbuntuVersionTagger(),
PythonVersionTagger, PythonVersionTagger(),
JupyterNotebookVersionTagger, JupyterNotebookVersionTagger(),
JupyterLabVersionTagger, JupyterLabVersionTagger(),
JupyterHubVersionTagger, JupyterHubVersionTagger(),
], ],
manifests=[CondaEnvironmentManifest, AptPackagesManifest], manifests=[CondaEnvironmentManifest(), AptPackagesManifest()],
), ),
"minimal-notebook": ImageDescription(parent_image="base-notebook"), "minimal-notebook": ImageDescription(parent_image="base-notebook"),
"scipy-notebook": ImageDescription(parent_image="minimal-notebook"), "scipy-notebook": ImageDescription(parent_image="minimal-notebook"),
"r-notebook": ImageDescription( "r-notebook": ImageDescription(
parent_image="minimal-notebook", parent_image="minimal-notebook",
taggers=[RVersionTagger], taggers=[RVersionTagger()],
manifests=[RPackagesManifest], manifests=[RPackagesManifest()],
), ),
"tensorflow-notebook": ImageDescription( "tensorflow-notebook": ImageDescription(
parent_image="scipy-notebook", taggers=[TensorflowVersionTagger] parent_image="scipy-notebook", taggers=[TensorflowVersionTagger()]
), ),
"datascience-notebook": ImageDescription( "datascience-notebook": ImageDescription(
parent_image="scipy-notebook", parent_image="scipy-notebook",
taggers=[RVersionTagger, JuliaVersionTagger], taggers=[RVersionTagger(), JuliaVersionTagger()],
manifests=[RPackagesManifest, JuliaPackagesManifest], manifests=[RPackagesManifest(), JuliaPackagesManifest()],
), ),
"pyspark-notebook": ImageDescription( "pyspark-notebook": ImageDescription(
parent_image="scipy-notebook", parent_image="scipy-notebook",
taggers=[SparkVersionTagger, HadoopVersionTagger, JavaVersionTagger], taggers=[SparkVersionTagger(), HadoopVersionTagger(), JavaVersionTagger()],
manifests=[SparkInfoManifest], manifests=[SparkInfoManifest()],
), ),
"all-spark-notebook": ImageDescription( "all-spark-notebook": ImageDescription(
parent_image="pyspark-notebook", parent_image="pyspark-notebook",
taggers=[RVersionTagger], taggers=[RVersionTagger()],
manifests=[RPackagesManifest], manifests=[RPackagesManifest()],
), ),
} }

View File

@@ -1,11 +1,12 @@
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
from plumbum.cmd import docker from plumbum.cmd import docker
from docker.models.containers import Container
from .docker_runner import DockerRunner from .docker_runner import DockerRunner
from .git_helper import GitHelper from .git_helper import GitHelper
def quoted_output(container, cmd: str) -> str: def quoted_output(container: Container, cmd: str) -> str:
return "\n".join( return "\n".join(
[ [
"```", "```",
@@ -50,13 +51,13 @@ class ManifestInterface:
"""Common interface for all manifests""" """Common interface for all manifests"""
@staticmethod @staticmethod
def markdown_piece(container) -> str: def markdown_piece(container: Container) -> str:
raise NotImplementedError raise NotImplementedError
class CondaEnvironmentManifest(ManifestInterface): class CondaEnvironmentManifest(ManifestInterface):
@staticmethod @staticmethod
def markdown_piece(container) -> str: def markdown_piece(container: Container) -> str:
return "\n".join( return "\n".join(
[ [
"## Python Packages", "## Python Packages",
@@ -72,7 +73,7 @@ class CondaEnvironmentManifest(ManifestInterface):
class AptPackagesManifest(ManifestInterface): class AptPackagesManifest(ManifestInterface):
@staticmethod @staticmethod
def markdown_piece(container) -> str: def markdown_piece(container: Container) -> str:
return "\n".join( return "\n".join(
[ [
"## Apt Packages", "## Apt Packages",
@@ -84,7 +85,7 @@ class AptPackagesManifest(ManifestInterface):
class RPackagesManifest(ManifestInterface): class RPackagesManifest(ManifestInterface):
@staticmethod @staticmethod
def markdown_piece(container) -> str: def markdown_piece(container: Container) -> str:
return "\n".join( return "\n".join(
[ [
"## R Packages", "## R Packages",
@@ -101,7 +102,7 @@ class RPackagesManifest(ManifestInterface):
class JuliaPackagesManifest(ManifestInterface): class JuliaPackagesManifest(ManifestInterface):
@staticmethod @staticmethod
def markdown_piece(container) -> str: def markdown_piece(container: Container) -> str:
return "\n".join( return "\n".join(
[ [
"## Julia Packages", "## Julia Packages",
@@ -118,7 +119,7 @@ class JuliaPackagesManifest(ManifestInterface):
class SparkInfoManifest(ManifestInterface): class SparkInfoManifest(ManifestInterface):
@staticmethod @staticmethod
def markdown_piece(container) -> str: def markdown_piece(container: Container) -> str:
return "\n".join( return "\n".join(
[ [
"## Apache Spark", "## Apache Spark",

View File

@@ -28,11 +28,11 @@ def tag_image(short_image_name: str, owner: str) -> None:
with DockerRunner(image) as container: with DockerRunner(image) as container:
tags = [] tags = []
for tagger in taggers: for tagger in taggers:
tagger_name = tagger.__name__ tagger_name = tagger.__class__.__name__
tag_value = tagger.tag_value(container) tag_value = tagger.tag_value(container)
tags.append(tag_value) tags.append(tag_value)
LOGGER.info( LOGGER.info(
f"Applying tag tagger_name: {tagger_name} tag_value: {tag_value}" f"Applying tag, tagger_name: {tagger_name} tag_value: {tag_value}"
) )
docker["tag", image, f"{owner}/{short_image_name}:{tag_value}"]() docker["tag", image, f"{owner}/{short_image_name}:{tag_value}"]()

View File

@@ -1,15 +1,16 @@
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
from datetime import datetime from datetime import datetime
from docker.models.containers import Container
from .git_helper import GitHelper from .git_helper import GitHelper
from .docker_runner import DockerRunner from .docker_runner import DockerRunner
def _get_program_version(container, program: str) -> str: def _get_program_version(container: Container, program: str) -> str:
return DockerRunner.run_simple_command(container, cmd=f"{program} --version") return DockerRunner.run_simple_command(container, cmd=f"{program} --version")
def _get_env_variable(container, variable: str) -> str: def _get_env_variable(container: Container, variable: str) -> str:
env = DockerRunner.run_simple_command( env = DockerRunner.run_simple_command(
container, container,
cmd="env", cmd="env",
@@ -21,7 +22,7 @@ def _get_env_variable(container, variable: str) -> str:
raise KeyError(variable) raise KeyError(variable)
def _get_pip_package_version(container, package: str) -> str: def _get_pip_package_version(container: Container, package: str) -> str:
VERSION_PREFIX = "Version: " VERSION_PREFIX = "Version: "
package_info = DockerRunner.run_simple_command( package_info = DockerRunner.run_simple_command(
container, container,
@@ -37,25 +38,25 @@ class TaggerInterface:
"""Common interface for all taggers""" """Common interface for all taggers"""
@staticmethod @staticmethod
def tag_value(container) -> str: def tag_value(container: Container) -> str:
raise NotImplementedError raise NotImplementedError
class SHATagger(TaggerInterface): class SHATagger(TaggerInterface):
@staticmethod @staticmethod
def tag_value(container) -> str: def tag_value(container: Container) -> str:
return GitHelper.commit_hash_tag() return GitHelper.commit_hash_tag()
class DateTagger(TaggerInterface): class DateTagger(TaggerInterface):
@staticmethod @staticmethod
def tag_value(container) -> str: def tag_value(container: Container) -> str:
return datetime.utcnow().strftime("%Y-%m-%d") return datetime.utcnow().strftime("%Y-%m-%d")
class UbuntuVersionTagger(TaggerInterface): class UbuntuVersionTagger(TaggerInterface):
@staticmethod @staticmethod
def tag_value(container) -> str: def tag_value(container: Container) -> str:
os_release = DockerRunner.run_simple_command( os_release = DockerRunner.run_simple_command(
container, container,
"cat /etc/os-release", "cat /etc/os-release",
@@ -63,63 +64,64 @@ class UbuntuVersionTagger(TaggerInterface):
for line in os_release: for line in os_release:
if line.startswith("VERSION_ID"): if line.startswith("VERSION_ID"):
return "ubuntu-" + line.split("=")[1].strip('"') return "ubuntu-" + line.split("=")[1].strip('"')
raise RuntimeError(f"did not find ubuntu version in: {os_release}")
class PythonVersionTagger(TaggerInterface): class PythonVersionTagger(TaggerInterface):
@staticmethod @staticmethod
def tag_value(container) -> str: def tag_value(container: Container) -> str:
return "python-" + _get_program_version(container, "python").split()[1] return "python-" + _get_program_version(container, "python").split()[1]
class JupyterNotebookVersionTagger(TaggerInterface): class JupyterNotebookVersionTagger(TaggerInterface):
@staticmethod @staticmethod
def tag_value(container) -> str: def tag_value(container: Container) -> str:
return "notebook-" + _get_program_version(container, "jupyter-notebook") return "notebook-" + _get_program_version(container, "jupyter-notebook")
class JupyterLabVersionTagger(TaggerInterface): class JupyterLabVersionTagger(TaggerInterface):
@staticmethod @staticmethod
def tag_value(container) -> str: def tag_value(container: Container) -> str:
return "lab-" + _get_program_version(container, "jupyter-lab") return "lab-" + _get_program_version(container, "jupyter-lab")
class JupyterHubVersionTagger(TaggerInterface): class JupyterHubVersionTagger(TaggerInterface):
@staticmethod @staticmethod
def tag_value(container) -> str: def tag_value(container: Container) -> str:
return "hub-" + _get_program_version(container, "jupyterhub") return "hub-" + _get_program_version(container, "jupyterhub")
class RVersionTagger(TaggerInterface): class RVersionTagger(TaggerInterface):
@staticmethod @staticmethod
def tag_value(container) -> str: def tag_value(container: Container) -> str:
return "r-" + _get_program_version(container, "R").split()[2] return "r-" + _get_program_version(container, "R").split()[2]
class TensorflowVersionTagger(TaggerInterface): class TensorflowVersionTagger(TaggerInterface):
@staticmethod @staticmethod
def tag_value(container) -> str: def tag_value(container: Container) -> str:
return "tensorflow-" + _get_pip_package_version(container, "tensorflow") return "tensorflow-" + _get_pip_package_version(container, "tensorflow")
class JuliaVersionTagger(TaggerInterface): class JuliaVersionTagger(TaggerInterface):
@staticmethod @staticmethod
def tag_value(container) -> str: def tag_value(container: Container) -> str:
return "julia-" + _get_program_version(container, "julia").split()[2] return "julia-" + _get_program_version(container, "julia").split()[2]
class SparkVersionTagger(TaggerInterface): class SparkVersionTagger(TaggerInterface):
@staticmethod @staticmethod
def tag_value(container) -> str: def tag_value(container: Container) -> str:
return "spark-" + _get_env_variable(container, "APACHE_SPARK_VERSION") return "spark-" + _get_env_variable(container, "APACHE_SPARK_VERSION")
class HadoopVersionTagger(TaggerInterface): class HadoopVersionTagger(TaggerInterface):
@staticmethod @staticmethod
def tag_value(container) -> str: def tag_value(container: Container) -> str:
return "hadoop-" + _get_env_variable(container, "HADOOP_VERSION") return "hadoop-" + _get_env_variable(container, "HADOOP_VERSION")
class JavaVersionTagger(TaggerInterface): class JavaVersionTagger(TaggerInterface):
@staticmethod @staticmethod
def tag_value(container) -> str: def tag_value(container: Container) -> str:
return "java-" + _get_program_version(container, "java").split()[1] return "java-" + _get_program_version(container, "java").split()[1]

View File

@@ -27,7 +27,8 @@ from collections import defaultdict
from itertools import chain from itertools import chain
import logging import logging
import json import json
from typing import Optional from typing import Any, Optional
from docker.models.containers import Container
from tabulate import tabulate from tabulate import tabulate
@@ -40,14 +41,16 @@ class CondaPackageHelper:
"""Conda package helper permitting to get information about packages""" """Conda package helper permitting to get information about packages"""
def __init__(self, container: TrackedContainer): def __init__(self, container: TrackedContainer):
self.running_container = CondaPackageHelper.start_container(container) self.running_container: Container = CondaPackageHelper.start_container(
container
)
self.requested: Optional[dict[str, set[str]]] = None self.requested: Optional[dict[str, set[str]]] = None
self.installed: Optional[dict[str, set[str]]] = None self.installed: Optional[dict[str, set[str]]] = None
self.available: Optional[dict[str, set[str]]] = None self.available: Optional[dict[str, set[str]]] = None
self.comparison: list[dict[str, str]] = [] self.comparison: list[dict[str, str]] = []
@staticmethod @staticmethod
def start_container(container: TrackedContainer): def start_container(container: TrackedContainer) -> Container:
"""Start the TrackedContainer and return an instance of a running container""" """Start the TrackedContainer and return an instance of a running container"""
LOGGER.info(f"Starting container {container.image_name} ...") LOGGER.info(f"Starting container {container.image_name} ...")
return container.run_detached( return container.run_detached(
@@ -85,13 +88,13 @@ class CondaPackageHelper:
) )
return self.requested return self.requested
def _execute_command(self, command): def _execute_command(self, command: list[str]) -> str:
"""Execute a command on a running container""" """Execute a command on a running container"""
rc = self.running_container.exec_run(command) rc = self.running_container.exec_run(command)
return rc.output.decode("utf-8") return rc.output.decode("utf-8") # type: ignore
@staticmethod @staticmethod
def _packages_from_json(env_export) -> dict[str, set[str]]: def _packages_from_json(env_export: str) -> dict[str, set[str]]:
"""Extract packages and versions from the lines returned by the list of specifications""" """Extract packages and versions from the lines returned by the list of specifications"""
# dependencies = filter(lambda x: isinstance(x, str), json.loads(env_export).get("dependencies")) # dependencies = filter(lambda x: isinstance(x, str), json.loads(env_export).get("dependencies"))
dependencies = json.loads(env_export).get("dependencies") dependencies = json.loads(env_export).get("dependencies")
@@ -114,7 +117,7 @@ class CondaPackageHelper:
packages_dict[package] = version packages_dict[package] = version
return packages_dict return packages_dict
def available_packages(self): def available_packages(self) -> dict[str, set[str]]:
"""Return the available packages""" """Return the available packages"""
if self.available is None: if self.available is None:
LOGGER.info("Grabing the list of available packages (can take a while) ...") LOGGER.info("Grabing the list of available packages (can take a while) ...")
@@ -125,11 +128,13 @@ class CondaPackageHelper:
return self.available return self.available
@staticmethod @staticmethod
def _extract_available(lines): def _extract_available(lines: str) -> dict[str, set[str]]:
"""Extract packages and versions from the lines returned by the list of packages""" """Extract packages and versions from the lines returned by the list of packages"""
ddict = defaultdict(set) ddict = defaultdict(set)
for line in lines.splitlines()[2:]: for line in lines.splitlines()[2:]:
pkg, version = re.match(r"^(\S+)\s+(\S+)", line, re.MULTILINE).groups() match = re.match(r"^(\S+)\s+(\S+)", line, re.MULTILINE)
assert match is not None
pkg, version = match.groups()
ddict[pkg].add(version) ddict[pkg].add(version)
return ddict return ddict
@@ -162,11 +167,11 @@ class CondaPackageHelper:
return self.comparison return self.comparison
@staticmethod @staticmethod
def semantic_cmp(version_string: str): def semantic_cmp(version_string: str) -> Any:
"""Manage semantic versioning for comparison""" """Manage semantic versioning for comparison"""
def mysplit(string): def mysplit(string: str) -> list[Any]:
def version_substrs(x): def version_substrs(x: str) -> list[str]:
return re.findall(r"([A-z]+|\d+)", x) return re.findall(r"([A-z]+|\d+)", x)
return list(chain(map(version_substrs, string.split(".")))) return list(chain(map(version_substrs, string.split("."))))
@@ -189,7 +194,9 @@ class CondaPackageHelper:
def get_outdated_summary(self, requested_only: bool = True) -> str: def get_outdated_summary(self, requested_only: bool = True) -> str:
"""Return a summary of outdated packages""" """Return a summary of outdated packages"""
nb_packages = len(self.requested if requested_only else self.installed) packages = self.requested if requested_only else self.installed
assert packages is not None
nb_packages = len(packages)
nb_updatable = len(self.comparison) nb_updatable = len(self.comparison)
updatable_ratio = nb_updatable / nb_packages updatable_ratio = nb_updatable / nb_packages
return f"{nb_updatable}/{nb_packages} ({updatable_ratio:.0%}) packages could be updated" return f"{nb_updatable}/{nb_packages} ({updatable_ratio:.0%}) packages could be updated"

View File

@@ -3,7 +3,7 @@
import logging import logging
import pytest import pytest # type: ignore
from conftest import TrackedContainer from conftest import TrackedContainer
from package_helper import CondaPackageHelper from package_helper import CondaPackageHelper
@@ -12,7 +12,9 @@ LOGGER = logging.getLogger(__name__)
@pytest.mark.info @pytest.mark.info
def test_outdated_packages(container: TrackedContainer, requested_only: bool = True): def test_outdated_packages(
container: TrackedContainer, requested_only: bool = True
) -> None:
"""Getting the list of updatable packages""" """Getting the list of updatable packages"""
LOGGER.info(f"Checking outdated packages in {container.image_name} ...") LOGGER.info(f"Checking outdated packages in {container.image_name} ...")
pkg_helper = CondaPackageHelper(container) pkg_helper = CondaPackageHelper(container)

View File

@@ -37,8 +37,9 @@ Example:
import logging import logging
import pytest import pytest # type: ignore
from conftest import TrackedContainer from conftest import TrackedContainer
from typing import Callable, Iterable
from package_helper import CondaPackageHelper from package_helper import CondaPackageHelper
@@ -87,7 +88,7 @@ def packages(package_helper: CondaPackageHelper) -> dict[str, set[str]]:
return package_helper.requested_packages() return package_helper.requested_packages()
def package_map(package: str) -> str: def get_package_import_name(package: str) -> str:
"""Perform a mapping between the python package name and the name used for the import""" """Perform a mapping between the python package name and the name used for the import"""
return PACKAGE_MAPPING.get(package, package) return PACKAGE_MAPPING.get(package, package)
@@ -113,7 +114,7 @@ def _check_import_package(
"""Generic function executing a command""" """Generic function executing a command"""
LOGGER.debug(f"Trying to import a package with [{command}] ...") LOGGER.debug(f"Trying to import a package with [{command}] ...")
rc = package_helper.running_container.exec_run(command) rc = package_helper.running_container.exec_run(command)
return rc.exit_code return rc.exit_code # type: ignore
def check_import_python_package( def check_import_python_package(
@@ -130,10 +131,10 @@ def check_import_r_package(package_helper: CondaPackageHelper, package: str) ->
) )
def _import_packages( def _check_import_packages(
package_helper: CondaPackageHelper, package_helper: CondaPackageHelper,
filtered_packages: dict[str, set[str]], filtered_packages: Iterable[str],
check_function, check_function: Callable[[CondaPackageHelper, str], int],
max_failures: int, max_failures: int,
) -> None: ) -> None:
"""Test if packages can be imported """Test if packages can be imported
@@ -157,33 +158,36 @@ def _import_packages(
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def r_packages(packages: dict[str, set[str]]): def r_packages(packages: dict[str, set[str]]) -> Iterable[str]:
"""Return an iterable of R packages""" """Return an iterable of R packages"""
# package[2:] is to remove the leading "r-" appended on R packages # package[2:] is to remove the leading "r-" appended on R packages
return map( return map(
lambda package: package_map(package[2:]), filter(r_package_predicate, packages) lambda package: get_package_import_name(package[2:]),
filter(r_package_predicate, packages),
) )
def test_r_packages( def test_r_packages(
package_helper: CondaPackageHelper, r_packages, max_failures: int = 0 package_helper: CondaPackageHelper, r_packages: Iterable[str], max_failures: int = 0
): ) -> None:
"""Test the import of specified R packages""" """Test the import of specified R packages"""
return _import_packages( _check_import_packages(
package_helper, r_packages, check_import_r_package, max_failures package_helper, r_packages, check_import_r_package, max_failures
) )
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def python_packages(packages: dict[str, set[str]]): def python_packages(packages: dict[str, set[str]]) -> Iterable[str]:
"""Return an iterable of Python packages""" """Return an iterable of Python packages"""
return map(package_map, filter(python_package_predicate, packages)) return map(get_package_import_name, filter(python_package_predicate, packages))
def test_python_packages( def test_python_packages(
package_helper: CondaPackageHelper, python_packages, max_failures: int = 0 package_helper: CondaPackageHelper,
): python_packages: Iterable[str],
max_failures: int = 0,
) -> None:
"""Test the import of specified python packages""" """Test the import of specified python packages"""
return _import_packages( _check_import_packages(
package_helper, python_packages, check_import_python_package, max_failures package_helper, python_packages, check_import_python_package, max_failures
) )