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

Fix Windows regression #396

Merged
merged 14 commits into from
Dec 12, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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