Skip to content

Commit

Permalink
Merge pull request #396 from malthe/test-windows
Browse files Browse the repository at this point in the history
Fix Windows regression
  • Loading branch information
malthe authored Dec 12, 2023
2 parents a075c59 + 616fe22 commit bedfd67
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 122 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ jobs:
fail-fast: false
matrix:
os:
- ["ubuntu", "ubuntu-20.04"]
- ["ubuntu-latest", "windows-latest"]
config:
# [Python version, tox env]
- ["3.9", "lint"]
- ["3.7", "py37"]
- ["3.8", "py38"]
- ["3.9", "py39"]
- ["3.10", "py310"]
Expand All @@ -36,6 +35,7 @@ jobs:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
name: ${{ matrix.config[1] }}
steps:
- run: git config --global core.autocrlf false
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
Expand Down
8 changes: 7 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
Changes
=======

In next release ...
- Add optional parameter ``package_name`` which allows loading a
template relative to a package.

- Drop support for Python 3.7.

- Fix regression where Chameleon would not load templates correctly on
Windows.

- Fix names of dependencies for ``importlib_resources`` and
``importlib_metadata``.
Expand Down
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ def run_tests(self):
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
Expand All @@ -70,7 +69,7 @@ def run_tests(self):
packages=find_packages('src'),
package_dir={'': 'src'},
include_package_data=True,
python_requires='>=3.7',
python_requires='>=3.8',
install_requires=install_requires,
extras_require={
'docs': {
Expand Down
6 changes: 3 additions & 3 deletions src/chameleon/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -977,7 +977,7 @@ class Compiler:

global_builtins = set(builtins.__dict__)

def __init__(self, engine_factory, node, spec, source,
def __init__(self, engine_factory, node, filename, source,
builtins={}, strict=True):
self._scopes = [set()]
self._expression_cache = {}
Expand Down Expand Up @@ -1052,7 +1052,7 @@ def visit_Scope(self, node):
self.lock.release()

self.code = "\n".join((
"__spec = %r\n" % spec,
"__filename = %r\n" % filename,
token_map_def,
generator.code
))
Expand Down Expand Up @@ -1181,7 +1181,7 @@ def visit_Macro(self, node):

exc_handler = template(
"if pos is not None: rcontext.setdefault('__error__', [])."
"append(token + (__spec, exc, ))",
"append(token + (__filename, exc, ))",
exc=exc,
token=template("__tokens[pos]", pos="__token", mode="eval"),
pos="__token"
Expand Down
53 changes: 44 additions & 9 deletions src/chameleon/loader.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import contextlib
import functools
import logging
import os
import posixpath
import py_compile
import shutil
import sys
Expand All @@ -9,6 +11,13 @@
from importlib.machinery import SourceFileLoader
from threading import RLock


try:
# we need to try the backport first, as we rely on ``files`` added in 3.9
import importlib_resources
except ImportError:
import importlib.resources as importlib_resources

from .utils import encode_string


Expand All @@ -29,6 +38,14 @@ def load(self, *args, **kwargs):
return load


@contextlib.contextmanager
def import_package_resource(name):
path = importlib_resources.files(name)
yield path
if hasattr(path.root, "close"):
path.root.close()


class TemplateLoader:
"""Template loader class.
Expand Down Expand Up @@ -66,16 +83,34 @@ def load(self, spec, cls=None):
if self.default_extension is not None and '.' not in spec:
spec += self.default_extension

if ':' not in spec and not os.path.isabs(spec):
for path in self.search_path:
path = os.path.join(path, spec)
if os.path.exists(path):
spec = path
break
else:
raise ValueError("Template not found: %s." % spec)
package_name = None

return cls(spec, search_path=self.search_path, **self.kwargs)
if not os.path.isabs(spec):
if ':' in spec:
package_name, spec = spec.split(':', 1)
else:
for path in self.search_path:
if not os.path.isabs(path) and ':' in path:
package_name, path = path.split(':', 1)
with import_package_resource(package_name) as files:
if files.joinpath(path).joinpath(spec).exists():
spec = posixpath.join(path, spec)
break
else:
path = os.path.join(path, spec)
if os.path.exists(path):
package_name = None
spec = path
break
else:
raise ValueError("Template not found: %s." % spec)

return cls(
spec,
search_path=self.search_path,
package_name=package_name,
**self.kwargs
)

def bind(self, cls):
return functools.partial(self.load, cls=cls)
Expand Down
4 changes: 2 additions & 2 deletions src/chameleon/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ class ElementProgram:

restricted_namespace = True

def __init__(self, source, mode="xml", spec=None, tokenizer=None):
def __init__(self, source, mode="xml", filename=None, tokenizer=None):
if tokenizer is None:
tokenizer = self.tokenizers[mode]
tokens = tokenizer(source, spec)
tokens = tokenizer(source, filename)
parser = ElementParser(
tokens,
self.DEFAULT_NAMESPACES,
Expand Down
109 changes: 39 additions & 70 deletions src/chameleon/template.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import datetime
import hashlib
import inspect
import logging
import os
import sys
import tempfile
import warnings


try:
# we need to try the backport first, as we rely on ``files`` added in 3.9
import importlib_resources
except ImportError:
import importlib.resources as importlib_resources

from .compiler import Compiler
from .config import AUTO_RELOAD
Expand All @@ -23,6 +16,7 @@
from .exc import TemplateError
from .loader import MemoryLoader
from .loader import ModuleLoader
from .loader import import_package_resource
from .nodes import Module
from .utils import DebuggingOutputStream
from .utils import Scope
Expand Down Expand Up @@ -95,16 +89,8 @@ class BaseTemplate:

# This attribute is strictly informational in this template class
# and is used in exception formatting. It may be set on
# initialization using the optional ``spec`` keyword argument.
spec = '<string>'

@property
def filename(self):
warnings.warn(
"The filename attribute is deprecated, use spec instead.",
DeprecationWarning,
stacklevel=2)
return self.spec
# initialization using the optional ``filename`` keyword argument.
filename = '<string>'

_cooked = False

Expand Down Expand Up @@ -156,7 +142,7 @@ def __call__(self, **kwargs):
return self.render(**kwargs)

def __repr__(self):
return "<{} {}>".format(self.__class__.__name__, self.spec)
return "<{} {}>".format(self.__class__.__name__, self.filename)

@property
def keep_body(self):
Expand Down Expand Up @@ -267,12 +253,12 @@ def _cook(self, body, name, builtins):
source = self._compile(body, builtins)
if self.debug:
source = "# template: {}\n#\n{}".format(
self.spec, source)
self.filename, source)
if self.keep_source:
self.source = source
cooked = self.loader.build(source, filename)
except TemplateError as exc:
exc.token.filename = self.spec
exc.token.filename = self.filename
raise
elif self.keep_source:
module = sys.modules.get(cooked.get('__name__'))
Expand All @@ -289,36 +275,22 @@ def digest(self, body, names):
sha.update(class_name)
digest = sha.hexdigest()

if self.spec and self.spec is not BaseTemplate.spec:
digest = os.path.splitext(self.spec.filename)[0] + '-' + digest
filename = str(self.filename)
if filename and filename != BaseTemplate.filename:
digest = os.path.splitext(filename)[0] + '-' + digest

return digest

def _compile(self, body, builtins):
program = self.parse(body)
module = Module("initialize", program)
compiler = Compiler(
self.engine, module, self.spec, body,
self.engine, module, str(self.filename), body,
builtins, strict=self.strict
)
return compiler.code


class Spec(object):
__slots__ = ('filename', 'pname', 'spec')

def __init__(self, spec):
self.spec = spec
if ':' in spec:
(self.pname, self.filename) = spec.split(':', 1)
else:
self.pname = None
self.filename = spec

def __repr__(self):
return repr(self.spec)


class BaseTemplateFile(BaseTemplate):
"""File-based template base class.
Expand All @@ -332,17 +304,18 @@ class BaseTemplateFile(BaseTemplate):

def __init__(
self,
spec,
filename,
auto_reload=None,
package_name=None,
post_init_hook=None,
**config):
if ':' not in spec:
if package_name is None:
# Normalize filename
spec = os.path.abspath(
os.path.normpath(os.path.expanduser(spec))
filename = os.path.abspath(
os.path.normpath(os.path.expanduser(filename))
)

self.spec = Spec(spec)
self.package_name = package_name
self.filename = filename

# Override reload setting only if value is provided explicitly
if auto_reload is not None:
Expand All @@ -356,14 +329,6 @@ def __init__(
if EAGER_PARSING:
self.cook_check()

@property
def filename(self):
warnings.warn(
"The filename attribute is deprecated, use spec.filename instead.",
DeprecationWarning,
stacklevel=2)
return self.spec.filename

def cook_check(self):
if self.auto_reload:
mtime = self.mtime()
Expand All @@ -374,24 +339,28 @@ def cook_check(self):

if self._cooked is False:
body = self.read()
log.debug("cooking %r (%d bytes)..." % (self.spec, len(body)))
log.debug("cooking %r (%d bytes)..." % (self.filename, len(body)))
self.cook(body)

def mtime(self):
if self.spec.pname is not None:
filename = self.filename
if self.package_name is not None:
with import_package_resource(self.package_name) as path:
filename = path.joinpath(self.filename).at
timetuple = path.root.getinfo(filename).date_time
return datetime.datetime(*timetuple).timestamp()
try:
return os.path.getmtime(filename)
except OSError:
return 0
else:
try:
return os.path.getmtime(self.spec.filename)
except OSError:
return 0

def read(self):
if self.spec.pname is not None:
files = importlib_resources.files(self.spec.pname)
data = files.joinpath(self.spec.filename).read_bytes()
if self.package_name is not None:
with import_package_resource(self.package_name) as files:
path = files.joinpath(self.filename)
data = path.read_bytes()
else:
with open(self.spec.filename, "rb") as f:
with open(self.filename, "rb") as f:
data = f.read()

body, encoding, content_type = read_bytes(
Expand All @@ -404,16 +373,16 @@ def read(self):
return body

def _get_module_name(self, name):
filename = os.path.basename(self.spec.filename)
filename = os.path.basename(str(self.filename))
mangled = mangle(filename)
return "{}_{}.py".format(mangled, name)

def _get_spec(self):
return self.__dict__.get('spec')
def _get_filename(self):
return self.__dict__.get('filename')

def _set_spec(self, spec):
self.__dict__['spec'] = spec
def _set_filename(self, filename):
self.__dict__['filename'] = filename
self._v_last_read = None
self._cooked = False

spec = property(_get_spec, _set_spec)
filename = property(_get_filename, _set_filename)
2 changes: 1 addition & 1 deletion src/chameleon/tests/inputs/081-load-spec.pt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<html metal:use-macro="load: chameleon:tests/inputs/hello_world.pt" />
<html metal:use-macro="load: chameleon:tests/inputs/hello_world.pt" />
Loading

0 comments on commit bedfd67

Please sign in to comment.