Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mypy type checking #167

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 8 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ jobs:
strategy:
matrix:
include:
- python-version: 2.7
toxenv: py27
# - python-version: 2.7
# toxenv: py27
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 2.7 bits either need fixing or removing elsewhere before this gets merged

- python-version: 3.5
toxenv: py35
- python-version: 3.6
Expand All @@ -26,10 +26,14 @@ jobs:
toxenv: py38
- python-version: 3.9
toxenv: py39
- python-version: pypy-2.7
toxenv: pypy
# - python-version: pypy-2.7
# toxenv: pypy
- python-version: pypy-3.7
toxenv: pypy3
- python-version: 3.9
toxenv: mypy
- python-version: 3.9
toxenv: mypy2

steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 2 additions & 0 deletions icecream/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@


def install(ic='ic'):
# type: (str) -> None
setattr(builtins, ic, icecream.ic)


def uninstall(ic='ic'):
# type: (str) -> None
delattr(builtins, ic)
83 changes: 61 additions & 22 deletions icecream/icecream.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@
from __future__ import print_function

import ast
import enum
import inspect
import pprint
import sys
from types import FrameType
from typing import TYPE_CHECKING, cast, Any, Callable, Generator, List, Sequence, Tuple, Type, Union, cast
if TYPE_CHECKING:
from typing import Literal
import warnings
from datetime import datetime
import functools
Expand All @@ -38,11 +43,14 @@

PYTHON2 = (sys.version_info[0] == 2)

_absent = object()
class Sentinel(enum.Enum):
absent = object()


def bindStaticVariable(name, value):
# type: (str, Any) -> Callable
def decorator(fn):
# type: (Callable) -> Callable
setattr(fn, name, value)
return fn
return decorator
Expand All @@ -52,12 +60,14 @@ def decorator(fn):
@bindStaticVariable(
'lexer', PyLexer(ensurenl=False) if PYTHON2 else Py3Lexer(ensurenl=False))
def colorize(s):
# type: (str) -> str
self = colorize
return highlight(s, self.lexer, self.formatter)


@contextmanager
def supportTerminalColorsInWindows():
# type: () -> Generator
# Filter and replace ANSI escape sequences on Windows with equivalent Win32
# API calls. This code does nothing on non-Windows systems.
colorama.init()
Expand All @@ -66,10 +76,12 @@ def supportTerminalColorsInWindows():


def stderrPrint(*args):
# type: (object) -> None
print(*args, file=sys.stderr)


def isLiteral(s):
# type: (str) -> bool
try:
ast.literal_eval(s)
except Exception:
Expand All @@ -78,6 +90,7 @@ def isLiteral(s):


def colorizedStderrPrint(s):
# type: (str) -> None
colored = colorize(s)
with supportTerminalColorsInWindows():
stderrPrint(colored)
Expand Down Expand Up @@ -112,20 +125,22 @@ def colorizedStderrPrint(s):


def callOrValue(obj):
# type: (Any) -> Any
return obj() if callable(obj) else obj


class Source(executing.Source):
def get_text_with_indentation(self, node):
# type: (Source, ast.expr) -> str
result = self.asttokens().get_text(node)
if '\n' in result:
result = ' ' * node.first_token.start[1] + result
result = ' ' * node.first_token.start[1] + result # type: ignore[attr-defined]
result = dedent(result)
result = result.strip()
return result


def prefixLines(prefix, s, startAtLine=0):
# type: (str, str, int) -> List[str]
lines = s.splitlines()

for i in range(startAtLine, len(lines)):
Expand All @@ -135,14 +150,16 @@ def prefixLines(prefix, s, startAtLine=0):


def prefixFirstLineIndentRemaining(prefix, s):
# type: (str, str) -> List[str]
indent = ' ' * len(prefix)
lines = prefixLines(indent, s, startAtLine=1)
lines[0] = prefix + lines[0]
return lines


def formatPair(prefix, arg, value):
if arg is _absent:
# type: (str, Union[str, Literal[Sentinel.absent]], str) -> str
if arg is Sentinel.absent:
argLines = []
valuePrefix = prefix
else:
Expand All @@ -160,30 +177,35 @@ def formatPair(prefix, arg, value):


def singledispatch(func):
if "singledispatch" not in dir(functools):
# type: (Callable) -> Callable
if sys.version_info < (3, 4): # Need version check not attribute check for mypy
def unsupport_py2(*args, **kwargs):
# type: (Any, Any) -> None
raise NotImplementedError(
"functools.singledispatch is missing in " + sys.version
)
func.register = func.unregister = unsupport_py2
func.register = func.unregister = unsupport_py2 # type: ignore[attr-defined]
return func

func = functools.singledispatch(func)

# add unregister based on https://stackoverflow.com/a/25951784
assert func.register.__closure__ is not None
closure = dict(zip(func.register.__code__.co_freevars,
func.register.__closure__))
registry = closure['registry'].cell_contents
dispatch_cache = closure['dispatch_cache'].cell_contents
def unregister(cls):
# type: (Type) -> None
del registry[cls]
dispatch_cache.clear()
func.unregister = unregister
func.unregister = unregister # type: ignore[attr-defined]
return func


@singledispatch
def argumentToString(obj):
# type: (Any) -> str
s = DEFAULT_ARG_TO_STRING_FUNCTION(obj)
s = s.replace('\\n', '\n') # Preserve string newlines in output.
return s
Expand All @@ -198,6 +220,7 @@ def __init__(self, prefix=DEFAULT_PREFIX,
outputFunction=DEFAULT_OUTPUT_FUNCTION,
argToStringFunction=argumentToString, includeContext=False,
contextAbsPath=False):
# type: (IceCreamDebugger, str, Callable[[str], None], Callable[[Any], str], bool,bool) -> None
self.enabled = True
self.prefix = prefix
self.includeContext = includeContext
Expand All @@ -206,8 +229,11 @@ def __init__(self, prefix=DEFAULT_PREFIX,
self.contextAbsPath = contextAbsPath

def __call__(self, *args):
# type: (IceCreamDebugger, Any) -> Any
if self.enabled:
callFrame = inspect.currentframe().f_back
currentFrame = inspect.currentframe()
assert currentFrame is not None and currentFrame.f_back is not None
callFrame = currentFrame.f_back
self.outputFunction(self._format(callFrame, *args))

if not args: # E.g. ic().
Expand All @@ -220,11 +246,15 @@ def __call__(self, *args):
return passthrough

def format(self, *args):
callFrame = inspect.currentframe().f_back
# type: (IceCreamDebugger, Any) -> str
currentFrame = inspect.currentframe()
assert currentFrame is not None and currentFrame.f_back is not None
callFrame = currentFrame.f_back
out = self._format(callFrame, *args)
return out

def _format(self, callFrame, *args):
# type: (IceCreamDebugger, FrameType, Any) -> str
prefix = callOrValue(self.prefix)

context = self._formatContext(callFrame)
Expand All @@ -240,25 +270,29 @@ def _format(self, callFrame, *args):
return out

def _formatArgs(self, callFrame, prefix, context, args):
# type: (IceCreamDebugger, FrameType, str, str, Sequence[Any]) -> str
callNode = Source.executing(callFrame).node
if callNode is not None:
source = Source.for_frame(callFrame)
assert isinstance(callNode, ast.Call)
source = cast(Source, Source.for_frame(callFrame))
sanitizedArgStrs = [
source.get_text_with_indentation(arg)
for arg in callNode.args]
else:
warnings.warn(
NO_SOURCE_AVAILABLE_WARNING_MESSAGE,
category=RuntimeWarning, stacklevel=4)
sanitizedArgStrs = [_absent] * len(args)
sanitizedArgStrs = [Sentinel.absent] * len(args)

pairs = list(zip(sanitizedArgStrs, args))

out = self._constructArgumentOutput(prefix, context, pairs)
return out

def _constructArgumentOutput(self, prefix, context, pairs):
# type: (IceCreamDebugger, str, str, Sequence[Tuple[Union[str, Literal[Sentinel.absent]], Any]]) -> str
def argPrefix(arg):
# type: (str) -> str
return '%s: ' % arg

pairs = [(arg, self.argToStringFunction(val)) for arg, val in pairs]
Expand All @@ -276,7 +310,7 @@ def argPrefix(arg):
# When the source for an arg is missing we also only print the value,
# since we can't know anything about the argument itself.
pairStrs = [
val if (isLiteral(arg) or arg is _absent)
val if (arg is Sentinel.absent or isLiteral(arg))
else (argPrefix(arg) + val)
for arg, val in pairs]

Expand Down Expand Up @@ -319,6 +353,7 @@ def argPrefix(arg):
return '\n'.join(lines)

def _formatContext(self, callFrame):
# type: (IceCreamDebugger, FrameType) -> str
filename, lineNumber, parentFunction = self._getContext(callFrame)

if parentFunction != '<module>':
Expand All @@ -328,45 +363,49 @@ def _formatContext(self, callFrame):
return context

def _formatTime(self):
# type: (IceCreamDebugger) -> str
now = datetime.now()
formatted = now.strftime('%H:%M:%S.%f')[:-3]
return ' at %s' % formatted

def _getContext(self, callFrame):
# type: (IceCreamDebugger, FrameType) -> Tuple[str, int, str]
frameInfo = inspect.getframeinfo(callFrame)
lineNumber = frameInfo.lineno
parentFunction = frameInfo.function

filepath = (realpath if self.contextAbsPath else basename)(frameInfo.filename)
filepath = (realpath if self.contextAbsPath else basename)(frameInfo.filename) # type: ignore[operator]
return filepath, lineNumber, parentFunction

def enable(self):
# type: (IceCreamDebugger) -> None
self.enabled = True

def disable(self):
# type: (IceCreamDebugger) -> None
self.enabled = False

def configureOutput(self, prefix=_absent, outputFunction=_absent,
argToStringFunction=_absent, includeContext=_absent,
contextAbsPath=_absent):
def configureOutput(self, prefix=Sentinel.absent, outputFunction=Sentinel.absent,
argToStringFunction=Sentinel.absent, includeContext=Sentinel.absent, contextAbsPath=Sentinel.absent):
# type: (IceCreamDebugger, Union[str, Literal[Sentinel.absent]], Union[Callable, Literal[Sentinel.absent]], Union[Callable, Literal[Sentinel.absent]], Union[bool, Literal[Sentinel.absent]], Union[bool, Literal[Sentinel.absent]]) -> None
noParameterProvided = all(
v is _absent for k,v in locals().items() if k != 'self')
v is Sentinel.absent for k,v in locals().items() if k != 'self')
if noParameterProvided:
raise TypeError('configureOutput() missing at least one argument')

if prefix is not _absent:
if prefix is not Sentinel.absent:
self.prefix = prefix

if outputFunction is not _absent:
if outputFunction is not Sentinel.absent:
self.outputFunction = outputFunction

if argToStringFunction is not _absent:
if argToStringFunction is not Sentinel.absent:
self.argToStringFunction = argToStringFunction

if includeContext is not _absent:
if includeContext is not Sentinel.absent:
self.includeContext = includeContext

if contextAbsPath is not _absent:
if contextAbsPath is not Sentinel.absent:
self.contextAbsPath = contextAbsPath


Expand Down
Empty file added icecream/py.typed
Empty file.
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[tool.mypy]
show_error_codes=true
disallow_untyped_defs=true
disallow_untyped_calls=true
warn_redundant_casts=true
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def run_tests(self):
platforms=['any'],
packages=find_packages(),
include_package_data=True,
package_data={'icecream': ['py.typed']},
classifiers=[
'License :: OSI Approved :: MIT License',
'Natural Language :: English',
Expand Down
21 changes: 20 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
[tox]
envlist = py27, py35, py36, py37, py38, py39, pypy, pypy3
envlist = py27, py35, py36, py37, py38, py39, pypy, pypy3, mypy, mypy2

[testenv]
deps =
nose
commands =
nosetests --exe []

[testenv:mypy]
basepython = python3.9
deps =
mypy==1.7.1
types-pygments
types-colorama
commands =
mypy icecream

[testenv:mypy2]
basepython = python3.9
deps =
mypy[python2]==0.971
types-pygments
types-colorama
types-enum34
commands =
mypy --py2 icecream --ignore-missing-imports