mirror of https://github.com/docker/docker-py.git
Add mypy CI, fix errors
Signed-off-by: Viicos <65306057+Viicos@users.noreply.github.com>
This commit is contained in:
parent
85ce456a2b
commit
0eb9d94e9c
|
|
@ -13,9 +13,11 @@ jobs:
|
|||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- run: pip install -U ruff==0.0.284
|
||||
- run: pip install -U ruff==0.0.284 mypy==1.5.1
|
||||
- name: Run ruff
|
||||
run: ruff docker tests
|
||||
- name: Run mypy
|
||||
run: mypy docker
|
||||
|
||||
unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
|
|
@ -1,22 +1,163 @@
|
|||
build
|
||||
dist
|
||||
*.egg-info
|
||||
*.egg/
|
||||
*.pyc
|
||||
*.swp
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
.tox
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
html/*
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Compiled Documentation
|
||||
_build/
|
||||
README.rst
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# setuptools_scm
|
||||
_version.py
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
.idea/
|
||||
*.iml
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Ruff linter
|
||||
.ruff_cache/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
|
|
|||
6
Makefile
6
Makefile
|
|
@ -46,7 +46,7 @@ build-dind-certs:
|
|||
docker build -t dpy-dind-certs -f tests/Dockerfile-dind-certs .
|
||||
|
||||
.PHONY: test
|
||||
test: ruff unit-test-py3 integration-dind integration-dind-ssl
|
||||
test: ruff mypy unit-test-py3 integration-dind integration-dind-ssl
|
||||
|
||||
.PHONY: unit-test-py3
|
||||
unit-test-py3: build-py3
|
||||
|
|
@ -167,6 +167,10 @@ integration-dind-ssl: build-dind-certs build-py3 setup-network
|
|||
ruff: build-py3
|
||||
docker run -t --rm docker-sdk-python3 ruff docker tests
|
||||
|
||||
.PHONY: mypy
|
||||
mypy: build-py3
|
||||
docker run -t --rm docker-sdk-python3 mypy docker
|
||||
|
||||
.PHONY: docs
|
||||
docs: build-docs
|
||||
docker run --rm -t -v `pwd`:/src docker-sdk-python-docs sphinx-build docs docs/_build
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import urllib
|
|||
import ssl
|
||||
import sys
|
||||
from functools import partial
|
||||
from typing import Any, AnyStr, Optional, Union, Dict, overload, NoReturn, Iterator
|
||||
from typing import Any, AnyStr, Optional, Union, Dict, cast, overload, NoReturn, Iterator
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Literal
|
||||
|
|
@ -25,11 +25,12 @@ from ..errors import (DockerException, InvalidVersion, TLSParameterError,
|
|||
create_api_error_from_http_exception)
|
||||
from ..tls import TLSConfig
|
||||
from ..transport import SSLHTTPAdapter, UnixHTTPAdapter
|
||||
from ..transport.basehttpadapter import BaseHTTPAdapter
|
||||
from ..utils import check_resource, config, update_headers, utils
|
||||
from ..utils.json_stream import json_stream
|
||||
from ..utils.proxy import ProxyConfig
|
||||
from ..utils.socket import consume_socket_output, demux_adaptor, frames_iter
|
||||
from ..utils.typing import BytesOrDict
|
||||
from ..utils.typing import StrOrDict
|
||||
from .build import BuildApiMixin
|
||||
from .config import ConfigApiMixin
|
||||
from .container import ContainerApiMixin
|
||||
|
|
@ -112,6 +113,8 @@ class APIClient(
|
|||
'base_url',
|
||||
'timeout']
|
||||
|
||||
_custom_adapter: BaseHTTPAdapter
|
||||
|
||||
def __init__(self, base_url: Optional[str] = None, version: Optional[str] = None,
|
||||
timeout: int = DEFAULT_TIMEOUT_SECONDS, tls: Optional[Union[bool, TLSConfig]] = False,
|
||||
user_agent: str = DEFAULT_USER_AGENT, num_pools: Optional[int] = None,
|
||||
|
|
@ -124,7 +127,6 @@ class APIClient(
|
|||
'If using TLS, the base_url argument must be provided.'
|
||||
)
|
||||
|
||||
self.base_url = base_url
|
||||
self.timeout = timeout
|
||||
self.headers['User-Agent'] = user_agent
|
||||
|
||||
|
|
@ -143,9 +145,7 @@ class APIClient(
|
|||
)
|
||||
self.credstore_env = credstore_env
|
||||
|
||||
base_url = utils.parse_host(
|
||||
base_url, IS_WINDOWS_PLATFORM, tls=bool(tls)
|
||||
)
|
||||
base_url = cast(str, utils.parse_host(base_url, IS_WINDOWS_PLATFORM, tls=bool(tls)))
|
||||
# SSH has a different default for num_pools to all other adapters
|
||||
num_pools = num_pools or DEFAULT_NUM_POOLS_SSH if \
|
||||
base_url.startswith('ssh://') else DEFAULT_NUM_POOLS
|
||||
|
|
@ -261,9 +261,9 @@ class APIClient(
|
|||
)
|
||||
|
||||
quote_f = partial(urllib.parse.quote, safe="/:")
|
||||
args = map(quote_f, args)
|
||||
args_quoted = map(quote_f, args)
|
||||
|
||||
formatted_path = pathfmt.format(*args)
|
||||
formatted_path = pathfmt.format(*args_quoted)
|
||||
if kwargs.get('versioned_api', True):
|
||||
return f'{self.base_url}/v{self._version}{formatted_path}'
|
||||
else:
|
||||
|
|
@ -281,7 +281,7 @@ class APIClient(
|
|||
...
|
||||
|
||||
@overload
|
||||
def _result(self, response: requests.Response, json: Literal[False] = ..., binary: Literal[False] = ...) -> str:
|
||||
def _result(self, response: requests.Response, json: Literal[False] = ..., binary: Literal[False] = ...) -> str: # type: ignore[misc]
|
||||
...
|
||||
|
||||
@overload
|
||||
|
|
@ -329,7 +329,7 @@ class APIClient(
|
|||
def _attach_websocket(self, container: str, params: Optional[Dict[str, Any]] = None) -> websocket.WebSocket:
|
||||
url = self._url("/containers/{0}/attach/ws", container)
|
||||
req = requests.Request("POST", url, params=self._attach_params(params))
|
||||
full_url = req.prepare().url
|
||||
full_url = cast(str, req.prepare().url)
|
||||
full_url = full_url.replace("http://", "ws://", 1)
|
||||
full_url = full_url.replace("https://", "wss://", 1)
|
||||
return self._create_websocket_connection(full_url)
|
||||
|
|
@ -364,10 +364,10 @@ class APIClient(
|
|||
...
|
||||
|
||||
@overload
|
||||
def _stream_helper(self, response: requests.Response, decode: Literal[False] = ...) -> Iterator[bytes]:
|
||||
def _stream_helper(self, response: requests.Response, decode: Literal[False] = ...) -> Iterator[str]:
|
||||
...
|
||||
|
||||
def _stream_helper(self, response: requests.Response, decode: bool = False) -> Iterator[BytesOrDict]:
|
||||
def _stream_helper(self, response: requests.Response, decode: bool = False) -> Iterator[StrOrDict]:
|
||||
"""Generator for data coming from a chunked-encoded HTTP response."""
|
||||
|
||||
if response.raw._fp.chunked:
|
||||
|
|
@ -386,7 +386,7 @@ class APIClient(
|
|||
else:
|
||||
# Response isn't chunked, meaning we probably
|
||||
# encountered an error immediately
|
||||
yield self._result(response, json=decode)
|
||||
yield self._result(response, json=decode) # type: ignore[misc]
|
||||
|
||||
def _multiplexed_buffer_helper(self, response: requests.Response) -> Iterator[bytes]:
|
||||
"""A generator of multiplexed data blocks read from a buffered
|
||||
|
|
@ -426,14 +426,18 @@ class APIClient(
|
|||
yield data
|
||||
|
||||
@overload
|
||||
def _stream_raw_result(self, response: requests.Response, chunk_size: int = ..., decode: Literal[False] = ...) -> Iterator[bytes]:
|
||||
def _stream_raw_result(self, response: requests.Response, chunk_size: int, decode: Literal[False]) -> Iterator[bytes]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def _stream_raw_result(self, response: requests.Response, decode: Literal[False]) -> Iterator[bytes]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def _stream_raw_result(self, response: requests.Response, chunk_size: int = ..., decode: Literal[True] = ...) -> Iterator[str]:
|
||||
...
|
||||
|
||||
def _stream_raw_result(self, response: requests.Response, chunk_size: int = 1, decode: bool = True) -> Iterator[AnyStr]:
|
||||
def _stream_raw_result(self, response: requests.Response, chunk_size: int = 1, decode: bool = True) -> Iterator[AnyStr]: # type: ignore[misc]
|
||||
''' Stream result for TTY-enabled container and raw binary data'''
|
||||
self._raise_for_status(response)
|
||||
|
||||
|
|
@ -489,13 +493,13 @@ class APIClient(
|
|||
timeout = -1
|
||||
|
||||
if hasattr(s, 'gettimeout'):
|
||||
timeout = s.gettimeout()
|
||||
timeout = cast(int, s.gettimeout()) # type: ignore[union-attr]
|
||||
|
||||
# Don't change the timeout if it is already disabled.
|
||||
if timeout is None or timeout == 0.0:
|
||||
continue
|
||||
|
||||
s.settimeout(None)
|
||||
s.settimeout(None) # type: ignore[union-attr]
|
||||
|
||||
@check_resource('container')
|
||||
def _check_is_tty(self, container: str) -> bool:
|
||||
|
|
@ -510,7 +514,7 @@ class APIClient(
|
|||
def _get_result(self, container: str, stream: Literal[False], res: requests.Response) -> bytes:
|
||||
...
|
||||
|
||||
def _get_result(self, container: str, stream: bool, res: requests.Response):
|
||||
def _get_result(self, container, stream, res):
|
||||
return self._get_result_tty(stream, res, self._check_is_tty(container))
|
||||
|
||||
@overload
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
import base64
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, AnyStr, Tuple, Dict, Optional, Union
|
||||
from typing import Any, AnyStr, Tuple, Dict, Optional, Union, cast
|
||||
|
||||
from . import credentials
|
||||
from . import errors
|
||||
|
|
@ -67,7 +67,7 @@ def split_repo_name(repo_name: str) -> Tuple[str, str]:
|
|||
):
|
||||
# This is a docker index repo (ex: username/foobar or ubuntu)
|
||||
return INDEX_NAME, repo_name
|
||||
return tuple(parts)
|
||||
return tuple(parts) # type: ignore[return-value]
|
||||
|
||||
|
||||
def get_credential_store(authconfig, registry):
|
||||
|
|
@ -82,7 +82,7 @@ class AuthConfig(dict):
|
|||
dct['auths'] = {}
|
||||
self.update(dct)
|
||||
self._credstore_env = credstore_env
|
||||
self._stores = {}
|
||||
self._stores: Dict[str, credentials.Store] = {}
|
||||
|
||||
@classmethod
|
||||
def parse_auth(cls, entries: Dict[str, Any], raise_on_error: bool = False) -> Dict[str, Any]:
|
||||
|
|
@ -145,7 +145,7 @@ class AuthConfig(dict):
|
|||
return conf
|
||||
|
||||
@classmethod
|
||||
def load_config(cls, config_path: str, config_dict: Dict[str, Any], credstore_env: Optional[Dict[str, Any]] = None) -> AuthConfig:
|
||||
def load_config(cls, config_path: Optional[str], config_dict: Optional[Dict[str, Any]], credstore_env: Optional[Dict[str, Any]] = None) -> AuthConfig:
|
||||
"""
|
||||
Loads authentication data from a Docker configuration file in the given
|
||||
root directory or if config_path is passed use given path.
|
||||
|
|
@ -161,7 +161,7 @@ class AuthConfig(dict):
|
|||
return cls({}, credstore_env)
|
||||
try:
|
||||
with open(config_file) as f:
|
||||
config_dict = json.load(f)
|
||||
config_dict = cast(Dict[str, Any], json.load(f))
|
||||
except (OSError, KeyError, ValueError) as e:
|
||||
# Likely missing new Docker config file or it's in an
|
||||
# unknown format, continue to attempt to read old location
|
||||
|
|
@ -245,7 +245,7 @@ class AuthConfig(dict):
|
|||
log.debug("No entry found")
|
||||
return None
|
||||
|
||||
def _resolve_authconfig_credstore(self, registry: str, credstore_name: str) -> Optional[Dict[str, Any]]:
|
||||
def _resolve_authconfig_credstore(self, registry: Optional[str], credstore_name: str) -> Optional[Dict[str, Any]]:
|
||||
if not registry or registry == INDEX_NAME:
|
||||
# The ecosystem is a little schizophrenic with index.docker.io VS
|
||||
# docker.io - in that case, it seems the full URL is necessary.
|
||||
|
|
@ -273,14 +273,14 @@ class AuthConfig(dict):
|
|||
f'Credentials store error: {repr(e)}'
|
||||
) from e
|
||||
|
||||
def _get_store_instance(self, name: str) -> Dict[str, Any]:
|
||||
def _get_store_instance(self, name: str) -> credentials.Store:
|
||||
if name not in self._stores:
|
||||
self._stores[name] = credentials.Store(
|
||||
name, environment=self._credstore_env
|
||||
)
|
||||
return self._stores[name]
|
||||
|
||||
def get_credential_store(self, registry: str) -> str:
|
||||
def get_credential_store(self, registry: Optional[str]) -> Optional[str]:
|
||||
if not registry or registry == INDEX_NAME:
|
||||
registry = INDEX_URL
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class APIError(requests.exceptions.HTTPError, DockerException):
|
|||
"""
|
||||
An HTTP error from the API.
|
||||
"""
|
||||
def __init__(self, message: str, response: Optional[requests.Response] = None, explanation: Optional[str] = None) -> None:
|
||||
def __init__(self, message: Any, response: Optional[requests.Response] = None, explanation: Optional[str] = None) -> None:
|
||||
# requests 1.2 supports response as a keyword argument, but
|
||||
# requests 1.1 doesn't
|
||||
super().__init__(message)
|
||||
|
|
@ -78,6 +78,7 @@ class APIError(requests.exceptions.HTTPError, DockerException):
|
|||
def status_code(self) -> Optional[int]:
|
||||
if self.response is not None:
|
||||
return self.response.status_code
|
||||
return None
|
||||
|
||||
def is_error(self) -> bool:
|
||||
return self.is_client_error() or self.is_server_error()
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ class TLSConfig:
|
|||
"""
|
||||
Configure a client with these TLS options.
|
||||
"""
|
||||
client.ssl_version = self.ssl_version
|
||||
client.ssl_version = self.ssl_version # type: ignore[attr-defined]
|
||||
|
||||
if self.verify and self.ca_cert:
|
||||
client.verify = self.ca_cert
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
from typing import Any, Dict, TypeVar
|
||||
|
||||
BytesOrDict = TypeVar("BytesOrDict", bytes, Dict[str, Any])
|
||||
StrOrDict = TypeVar("StrOrDict", str, Dict[str, Any])
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
try:
|
||||
from ._version import __version__
|
||||
from ._version import __version__ # type: ignore[import]
|
||||
except ImportError:
|
||||
try:
|
||||
# importlib.metadata available in Python 3.8+, the fallback (0.0.0)
|
||||
|
|
|
|||
|
|
@ -18,3 +18,22 @@ ignore = [
|
|||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"**/__init__.py" = ["F401"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.8"
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"docker.api.*",
|
||||
"docker.context.*",
|
||||
"docker.credentials.*",
|
||||
"docker.models.*",
|
||||
"docker.transport.*",
|
||||
"docker.types.*",
|
||||
"docker.utils.*",
|
||||
]
|
||||
ignore_errors = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "docker.api.client"
|
||||
ignore_errors = false
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@ pywin32==304; sys_platform == 'win32'
|
|||
requests==2.31.0
|
||||
urllib3==1.26.11
|
||||
websocket-client==1.3.3
|
||||
typing_extensions>=3.10.0.0; python_version < '3.8'
|
||||
typing_extensions>=3.10.0.0; python_version < '3.11'
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -14,7 +14,7 @@ requirements = [
|
|||
'requests >= 2.26.0',
|
||||
'urllib3 >= 1.26.0',
|
||||
'websocket-client >= 0.32.0',
|
||||
'typing_extensions>=3.10.0.0; python_version < "3.8"',
|
||||
'typing_extensions>=3.10.0.0; python_version < "3.11"',
|
||||
]
|
||||
|
||||
extras_require = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
setuptools==65.5.1
|
||||
coverage==6.4.2
|
||||
ruff==0.0.284
|
||||
mypy==1.5.1
|
||||
pytest==7.1.2
|
||||
pytest-cov==3.0.0
|
||||
pytest-timeout==2.1.0
|
||||
|
|
|
|||
Loading…
Reference in New Issue