Skip to content

Commit

Permalink
Add mypy CI, fix errors
Browse files Browse the repository at this point in the history
Signed-off-by: Viicos <65306057+Viicos@users.noreply.github.com>
  • Loading branch information
Viicos committed Sep 8, 2023
1 parent 85ce456 commit 0eb9d94
Show file tree
Hide file tree
Showing 13 changed files with 222 additions and 50 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
173 changes: 157 additions & 16 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,22 +1,163 @@
build
dist
*.egg-info
*.egg/
*.pyc
*.swp

.tox
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# 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/

# Translations
*.mo
*.pot

# 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

# Compiled Documentation
_build/
README.rst
# 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

# setuptools_scm
_version.py
# 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 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
40 changes: 22 additions & 18 deletions docker/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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
Expand All @@ -25,11 +25,12 @@
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
Expand Down Expand Up @@ -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,
Expand All @@ -124,7 +127,6 @@ def __init__(self, base_url: Optional[str] = None, version: Optional[str] = None
'If using TLS, the base_url argument must be provided.'
)

self.base_url = base_url
self.timeout = timeout
self.headers['User-Agent'] = user_agent

Expand All @@ -143,9 +145,7 @@ def __init__(self, base_url: Optional[str] = None, version: Optional[str] = None
)
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
Expand Down Expand Up @@ -261,9 +261,9 @@ def _url(self, pathfmt: str, *args: str, **kwargs: Any) -> str:
)

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:
Expand All @@ -281,7 +281,7 @@ def _result(self, response: requests.Response, json: Literal[True], binary: Lite
...

@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
Expand Down Expand Up @@ -329,7 +329,7 @@ def _attach_params(self, override: Optional[Dict[str, Any]] = None) -> Dict[str,
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)
Expand Down Expand Up @@ -364,10 +364,10 @@ def _stream_helper(self, response: requests.Response, decode: Literal[True]) ->
...

@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:
Expand All @@ -386,7 +386,7 @@ def _stream_helper(self, response: requests.Response, decode: bool = False) -> I
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
Expand Down Expand Up @@ -426,14 +426,18 @@ def _multiplexed_response_stream_helper(self, response: requests.Response) -> It
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)

Expand Down Expand Up @@ -489,13 +493,13 @@ def _disable_socket_timeout(self, socket: ssl.SSLSocket) -> None:
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:
Expand All @@ -510,7 +514,7 @@ def _get_result(self, container: str, stream: Literal[True], res: requests.Respo
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
Expand Down

0 comments on commit 0eb9d94

Please sign in to comment.