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

add type annotations #1412

Merged
merged 1 commit into from May 8, 2021
Merged
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
1 change: 1 addition & 0 deletions CHANGES.rst
Expand Up @@ -9,6 +9,7 @@ Unreleased
- Bump MarkupSafe dependency to >=1.1.
- Bump Babel optional dependency to >=2.1.
- Remove code that was marked deprecated.
- Add type hinting. :pr:`1412`
- Use :pep:`451` API to load templates with
:class:`~loaders.PackageLoader`. :issue:`1168`
- Fix a bug that caused imported macros to not have access to the
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Expand Up @@ -6,4 +6,5 @@ graft docs
prune docs/_build
graft examples
graft tests
include src/click/py.typed
global-exclude *.pyc
2 changes: 1 addition & 1 deletion docs/examples/inline_gettext_extension.py
Expand Up @@ -30,7 +30,7 @@ def filter_stream(self, stream):
pos = 0
lineno = token.lineno

while 1:
while True:
if not paren_stack:
match = _outside_re.search(token.value, pos)
else:
Expand Down
12 changes: 9 additions & 3 deletions setup.cfg
Expand Up @@ -86,15 +86,21 @@ per-file-ignores =
files = src/jinja2
python_version = 3.6
disallow_subclassing_any = True
# disallow_untyped_calls = True
# disallow_untyped_defs = True
disallow_untyped_calls = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
no_implicit_optional = True
local_partial_types = True
# no_implicit_reexport = True
no_implicit_reexport = True
strict_equality = True
warn_redundant_casts = True
warn_unused_configs = True
warn_unused_ignores = True
warn_return_any = True
warn_unreachable = True

[mypy-jinja2.defaults]
no_implicit_reexport = False

[mypy-markupsafe]
no_implicit_reexport = False
44 changes: 18 additions & 26 deletions src/jinja2/async_utils.py
Expand Up @@ -5,27 +5,26 @@
from .utils import _PassArg
from .utils import pass_eval_context

if t.TYPE_CHECKING:
V = t.TypeVar("V")
V = t.TypeVar("V")


def async_variant(normal_func):
def decorator(async_func):
def async_variant(normal_func): # type: ignore
def decorator(async_func): # type: ignore
pass_arg = _PassArg.from_obj(normal_func)
need_eval_context = pass_arg is None

if pass_arg is _PassArg.environment:

def is_async(args):
return args[0].is_async
def is_async(args: t.Any) -> bool:
return t.cast(bool, args[0].is_async)

else:

def is_async(args):
return args[0].environment.is_async
def is_async(args: t.Any) -> bool:
return t.cast(bool, args[0].environment.is_async)

@wraps(normal_func)
def wrapper(*args, **kwargs):
def wrapper(*args, **kwargs): # type: ignore
b = is_async(args)

if need_eval_context:
Expand All @@ -45,32 +44,25 @@ def wrapper(*args, **kwargs):
return decorator


async def auto_await(value):
async def auto_await(value: t.Union[t.Awaitable["V"], "V"]) -> "V":
if inspect.isawaitable(value):
return await value
return await t.cast("t.Awaitable[V]", value)

return value
return t.cast("V", value)


async def auto_aiter(iterable):
async def auto_aiter(
iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
) -> "t.AsyncIterator[V]":
if hasattr(iterable, "__aiter__"):
async for item in iterable:
async for item in t.cast("t.AsyncIterable[V]", iterable):
yield item
else:
for item in iterable:
for item in t.cast("t.Iterable[V]", iterable):
yield item


async def auto_to_list(
value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
) -> "t.List[V]":
seq = []

if hasattr(value, "__aiter__"):
async for item in t.cast(t.AsyncIterable, value):
seq.append(item)
else:
for item in t.cast(t.Iterable, value):
seq.append(item)

return seq
) -> t.List["V"]:
return [x async for x in auto_aiter(value)]
117 changes: 68 additions & 49 deletions src/jinja2/bccache.py
Expand Up @@ -13,10 +13,22 @@
import stat
import sys
import tempfile
import typing as t
from hashlib import sha1
from io import BytesIO
from types import CodeType

if t.TYPE_CHECKING:
import typing_extensions as te
from .environment import Environment

class _MemcachedClient(te.Protocol):
def get(self, key: str) -> bytes:
...

def set(self, key: str, value: bytes, timeout: t.Optional[int] = None) -> None:
...

from .utils import open_if_exists

bc_version = 5
# Magic bytes to identify Jinja bytecode cache files. Contains the
Expand All @@ -38,17 +50,17 @@ class Bucket:
cache subclasses don't have to care about cache invalidation.
"""

def __init__(self, environment, key, checksum):
def __init__(self, environment: "Environment", key: str, checksum: str) -> None:
self.environment = environment
self.key = key
self.checksum = checksum
self.reset()

def reset(self):
def reset(self) -> None:
"""Resets the bucket (unloads the bytecode)."""
self.code = None
self.code: t.Optional[CodeType] = None

def load_bytecode(self, f):
def load_bytecode(self, f: t.BinaryIO) -> None:
"""Loads bytecode from a file or file like object."""
# make sure the magic header is correct
magic = f.read(len(bc_magic))
Expand All @@ -67,20 +79,20 @@ def load_bytecode(self, f):
self.reset()
return

def write_bytecode(self, f):
def write_bytecode(self, f: t.BinaryIO) -> None:
"""Dump the bytecode into the file or file like object passed."""
if self.code is None:
raise TypeError("can't write empty bucket")
f.write(bc_magic)
pickle.dump(self.checksum, f, 2)
marshal.dump(self.code, f)

def bytecode_from_string(self, string):
"""Load bytecode from a string."""
def bytecode_from_string(self, string: bytes) -> None:
"""Load bytecode from bytes."""
self.load_bytecode(BytesIO(string))

def bytecode_to_string(self):
"""Return the bytecode as string."""
def bytecode_to_string(self) -> bytes:
"""Return the bytecode as bytes."""
out = BytesIO()
self.write_bytecode(out)
return out.getvalue()
Expand Down Expand Up @@ -115,41 +127,48 @@ def dump_bytecode(self, bucket):
Jinja.
"""

def load_bytecode(self, bucket):
def load_bytecode(self, bucket: Bucket) -> None:
"""Subclasses have to override this method to load bytecode into a
bucket. If they are not able to find code in the cache for the
bucket, it must not do anything.
"""
raise NotImplementedError()

def dump_bytecode(self, bucket):
def dump_bytecode(self, bucket: Bucket) -> None:
"""Subclasses have to override this method to write the bytecode
from a bucket back to the cache. If it unable to do so it must not
fail silently but raise an exception.
"""
raise NotImplementedError()

def clear(self):
def clear(self) -> None:
"""Clears the cache. This method is not used by Jinja but should be
implemented to allow applications to clear the bytecode cache used
by a particular environment.
"""

def get_cache_key(self, name, filename=None):
def get_cache_key(
self, name: str, filename: t.Optional[t.Union[str]] = None
) -> str:
"""Returns the unique hash key for this template name."""
hash = sha1(name.encode("utf-8"))

if filename is not None:
filename = "|" + filename
if isinstance(filename, str):
filename = filename.encode("utf-8")
hash.update(filename)
hash.update(f"|{filename}".encode("utf-8"))

return hash.hexdigest()

def get_source_checksum(self, source):
def get_source_checksum(self, source: str) -> str:
"""Returns a checksum for the source."""
return sha1(source.encode("utf-8")).hexdigest()

def get_bucket(self, environment, name, filename, source):
def get_bucket(
self,
environment: "Environment",
name: str,
filename: t.Optional[str],
source: str,
) -> Bucket:
"""Return a cache bucket for the given template. All arguments are
mandatory but filename may be `None`.
"""
Expand All @@ -159,7 +178,7 @@ def get_bucket(self, environment, name, filename, source):
self.load_bytecode(bucket)
return bucket

def set_bucket(self, bucket):
def set_bucket(self, bucket: Bucket) -> None:
"""Put the bucket into the cache."""
self.dump_bytecode(bucket)

Expand All @@ -182,14 +201,16 @@ class FileSystemBytecodeCache(BytecodeCache):
This bytecode cache supports clearing of the cache using the clear method.
"""

def __init__(self, directory=None, pattern="__jinja2_%s.cache"):
def __init__(
self, directory: t.Optional[str] = None, pattern: str = "__jinja2_%s.cache"
) -> None:
if directory is None:
directory = self._get_default_cache_dir()
self.directory = directory
self.pattern = pattern

def _get_default_cache_dir(self):
def _unsafe_dir():
def _get_default_cache_dir(self) -> str:
def _unsafe_dir() -> t.NoReturn:
raise RuntimeError(
"Cannot determine safe temp directory. You "
"need to explicitly provide one."
Expand Down Expand Up @@ -235,25 +256,21 @@ def _unsafe_dir():

return actual_dir

def _get_cache_filename(self, bucket):
def _get_cache_filename(self, bucket: Bucket) -> str:
return os.path.join(self.directory, self.pattern % (bucket.key,))

def load_bytecode(self, bucket):
f = open_if_exists(self._get_cache_filename(bucket), "rb")
if f is not None:
try:
def load_bytecode(self, bucket: Bucket) -> None:
filename = self._get_cache_filename(bucket)

if os.path.exists(filename):
with open(filename, "rb") as f:
bucket.load_bytecode(f)
finally:
f.close()

def dump_bytecode(self, bucket):
f = open(self._get_cache_filename(bucket), "wb")
try:
def dump_bytecode(self, bucket: Bucket) -> None:
with open(self._get_cache_filename(bucket), "wb") as f:
bucket.write_bytecode(f)
finally:
f.close()

def clear(self):
def clear(self) -> None:
# imported lazily here because google app-engine doesn't support
# write access on the file system and the function does not exist
# normally.
Expand Down Expand Up @@ -314,32 +331,34 @@ class MemcachedBytecodeCache(BytecodeCache):

def __init__(
self,
client,
prefix="jinja2/bytecode/",
timeout=None,
ignore_memcache_errors=True,
client: "_MemcachedClient",
prefix: str = "jinja2/bytecode/",
timeout: t.Optional[int] = None,
ignore_memcache_errors: bool = True,
):
self.client = client
self.prefix = prefix
self.timeout = timeout
self.ignore_memcache_errors = ignore_memcache_errors

def load_bytecode(self, bucket):
def load_bytecode(self, bucket: Bucket) -> None:
try:
code = self.client.get(self.prefix + bucket.key)
except Exception:
if not self.ignore_memcache_errors:
raise
code = None
if code is not None:
else:
bucket.bytecode_from_string(code)

def dump_bytecode(self, bucket):
args = (self.prefix + bucket.key, bucket.bytecode_to_string())
if self.timeout is not None:
args += (self.timeout,)
def dump_bytecode(self, bucket: Bucket) -> None:
key = self.prefix + bucket.key
value = bucket.bytecode_to_string()

try:
self.client.set(*args)
if self.timeout is not None:
self.client.set(key, value, self.timeout)
else:
self.client.set(key, value)
except Exception:
if not self.ignore_memcache_errors:
raise