Skip to content

Commit

Permalink
Add get_current() to return the current active element in a with cont…
Browse files Browse the repository at this point in the history
…ext. Closes #123
  • Loading branch information
Knio committed Oct 21, 2020
1 parent fdd4037 commit dce0093
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 41 deletions.
2 changes: 1 addition & 1 deletion dominate/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '2.5.2'
__version__ = '2.6.0'
90 changes: 63 additions & 27 deletions dominate/dom_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
except ImportError:
greenlet = None


def _get_thread_context():
context = [threading.current_thread()]
if greenlet:
Expand All @@ -57,11 +58,11 @@ class dom_tag(object):
# modified
is_inline = False

frame = namedtuple('frame', ['tag', 'items', 'used'])

def __new__(_cls, *args, **kwargs):
'''
Check if bare tag is being used a a decorator.
Check if bare tag is being used a a decorator
(called with a single function arg).
decorate the function and return
'''
if len(args) == 1 and isinstance(args[0], Callable) \
Expand All @@ -75,6 +76,7 @@ def f(*args, **kwargs):
return f
return object.__new__(_cls)


def __init__(self, *args, **kwargs):
'''
Creates a new tag. Child tags should be passed as arguments and attributes
Expand Down Expand Up @@ -105,28 +107,35 @@ def __init__(self, *args, **kwargs):
self._ctx = None
self._add_to_ctx()

def _add_to_ctx(self):
ctx = dom_tag._with_contexts[_get_thread_context()]
if ctx and ctx[-1]:
self._ctx = ctx[-1]
ctx[-1].items.append(self)

# stack of (root_tag, [new_tags], set(used_tags))
# context manager
frame = namedtuple('frame', ['tag', 'items', 'used'])
# stack of frames
_with_contexts = defaultdict(list)

def _add_to_ctx(self):
stack = dom_tag._with_contexts.get(_get_thread_context())
if stack:
self._ctx = stack[-1]
stack[-1].items.append(self)


def __enter__(self):
ctx = dom_tag._with_contexts[_get_thread_context()]
ctx.append(dom_tag.frame(self, [], set()))
stack = dom_tag._with_contexts[_get_thread_context()]
stack.append(dom_tag.frame(self, [], set()))
return self


def __exit__(self, type, value, traceback):
ctx = dom_tag._with_contexts[_get_thread_context()]
slf, items, used = ctx[-1]
ctx[-1] = None
for item in items:
if item in used: continue
thread_id = _get_thread_context()
stack = dom_tag._with_contexts[thread_id]
frame = stack.pop()
for item in frame.items:
if item in frame.used: continue
self.add(item)
ctx.pop()
if not stack:
del dom_tag._with_contexts[thread_id]


def __call__(self, func):
'''
Expand All @@ -146,6 +155,7 @@ def f(*args, **kwargs):
return func(*args, **kwargs) or tag
return f


def set_attribute(self, key, value):
'''
Add or update the value of an attribute.
Expand Down Expand Up @@ -178,6 +188,7 @@ def setdocument(self, doc):
if not isinstance(i, dom_tag): return
i.setdocument(doc)


def add(self, *args):
'''
Add new child tags.
Expand All @@ -192,9 +203,9 @@ def add(self, *args):
self.children.append(obj)

elif isinstance(obj, dom_tag):
ctx = dom_tag._with_contexts[_get_thread_context()]
if ctx and ctx[-1]:
ctx[-1].used.add(obj)
stack = dom_tag._with_contexts.get(_get_thread_context())
if stack:
stack[-1].used.add(obj)
self.children.append(obj)
obj.parent = self
obj.setdocument(self.document)
Expand All @@ -215,18 +226,22 @@ def add(self, *args):

return args


def add_raw_string(self, s):
self.children.append(s)


def remove(self, obj):
self.children.remove(obj)


def clear(self):
for i in self.children:
if isinstance(i, dom_tag) and i.parent is self:
i.parent = None
self.children = []


def get(self, tag=None, **kwargs):
'''
Recursively searches children for tags of a certain
Expand All @@ -253,6 +268,7 @@ def get(self, tag=None, **kwargs):
results.extend(child.get(tag, **kwargs))
return results


def __getitem__(self, key):
'''
Returns the stored value of the specified attribute or child
Expand All @@ -275,32 +291,37 @@ def __getitem__(self, key):
'child tags and attributes, respectively.')
__getattr__ = __getitem__


def __len__(self):
'''
Number of child elements.
'''
return len(self.children)


def __bool__(self):
'''
Hack for "if x" and __len__
'''
return True
__nonzero__ = __bool__


def __iter__(self):
'''
Iterates over child elements.
'''
return self.children.__iter__()


def __contains__(self, item):
'''
Checks recursively if item is in children tree.
Accepts both a string and a class.
'''
return bool(self.get(item))


def __iadd__(self, obj):
'''
Reflexive binary addition simply adds tag as a child.
Expand All @@ -313,10 +334,12 @@ def __unicode__(self):
return self.render()
__str__ = __unicode__


def render(self, indent=' ', pretty=True, xhtml=False):
data = self._render([], 0, indent, pretty, xhtml)
return u''.join(data)


def _render(self, sb, indent_level, indent_str, pretty, xhtml):
pretty = pretty and self.is_pretty

Expand Down Expand Up @@ -365,6 +388,7 @@ def _render_children(self, sb, indent_level, indent_str, pretty, xhtml):

return inline


def __repr__(self):
name = '%s.%s' % (self.__module__, type(self).__name__)

Expand Down Expand Up @@ -432,18 +456,30 @@ def clean_pair(cls, attribute, value):
return (attribute, value)


_get_current_none = object()
def get_current(default=_get_current_none):
'''
get the current tag being used as a with context or decorated function.
if no context is active, raises ValueError, or returns the default, if provided
'''
h = _get_thread_context()
ctx = dom_tag._with_contexts.get(h, None)
if ctx:
return ctx[-1].tag
if default is _get_current_none:
raise ValueError('no current context')
return default


def attr(*args, **kwargs):
'''
Set attributes on the current active tag context
'''
ctx = dom_tag._with_contexts[_get_thread_context()]
if ctx and ctx[-1]:
dicts = args + (kwargs,)
for d in dicts:
for attr, value in d.items():
ctx[-1].tag.set_attribute(*dom_tag.clean_pair(attr, value))
else:
raise ValueError('not in a tag context')
c = get_current()
dicts = args + (kwargs,)
for d in dicts:
for attr, value in d.items():
c.set_attribute(*dom_tag.clean_pair(attr, value))


# escape() is used in render
Expand Down
2 changes: 1 addition & 1 deletion dominate/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
Public License along with Dominate. If not, see
<http://www.gnu.org/licenses/>.
'''
from .dom_tag import dom_tag, attr
from .dom_tag import dom_tag, attr, get_current
from .dom1core import dom1core

try:
Expand Down
2 changes: 1 addition & 1 deletion dominate/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
'''

import re
from .dom_tag import dom_tag

from .dom_tag import dom_tag

try:
basestring = basestring
Expand Down
18 changes: 18 additions & 0 deletions tests/test_dominate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import pytest

import dominate
from dominate import tags
from dominate import util

def test_version():
import dominate
version = '2.6.0'
assert dominate.version == version
assert dominate.__version__ == version


def test_context():
id1 = dominate.dom_tag._get_thread_context()
id2 = dominate.dom_tag._get_thread_context()
assert id1 == id2

39 changes: 29 additions & 10 deletions tests/test_html.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dominate
from dominate.tags import *
import pytest

Expand All @@ -6,12 +7,6 @@
except NameError:
xrange = range

def test_version():
import dominate
version = '2.5.2'
assert dominate.version == version
assert dominate.__version__ == version


def test_arguments():
assert html(body(h1('Hello, pyy!'))).render() == \
Expand All @@ -29,7 +24,7 @@ def test_kwargs():
cls="mydiv",
data_name='foo',
onclick='alert(1);').render() == \
'''<div checked="checked" class="mydiv" data-name="foo" id="4" onclick="alert(1);"></div>'''
'''<div checked="checked" class="mydiv" data-name="foo" id="4" onclick="alert(1);"></div>'''


def test_repr():
Expand Down Expand Up @@ -77,15 +72,15 @@ def test_iadd():
</ul>'''


# copy rest of examples here


def test_context_manager():
other = div()
h = ul()
with h:
li('One')
li('Two')
li('Three')
# added to other, so not added to h
other += li('Four')

assert h.render() == \
'''<ul>
Expand Down Expand Up @@ -186,6 +181,22 @@ def test_escape():
<pre>&lt;&gt;</pre>'''


def test_get_context():
with pytest.raises(ValueError):
d = get_current()

d = get_current(None)
assert d is None

with div() as d1:
d2 = span()
with d2:
d2p = get_current()
d1p = get_current()
assert d1 is d1p
assert d2 is d2p


def test_attributes():
d = div()
d['id'] = 'foo'
Expand Down Expand Up @@ -248,6 +259,14 @@ def test_comment():
assert d.render() == '<!--Hi there-->'
assert div(d).render() == '<div>\n <!--Hi there-->\n</div>'

d = comment('Hi ie user', condition='IE 6')
assert d.render() == '<!--[if IE 6]>Hi ie user<![endif]-->'

d = comment(div('Hi non-ie user'), condition='!IE', downlevel='revealed')
assert d.render() == '''<![if !IE]>
<div>Hi non-ie user</div>
<![endif]>'''


def test_boolean_attributes():
assert input_(type="checkbox", checked=True).render() == \
Expand Down
7 changes: 6 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import pytest

from dominate.tags import *
from dominate import util


def test_include():
import os
try:
Expand All @@ -18,6 +21,7 @@ def test_include():
except:
pass


def test_system():
d = div()
d += util.system('echo Hello World')
Expand All @@ -27,6 +31,7 @@ def test_system():
def test_unescape():
assert util.unescape('&amp;&lt;&gt;&#32;') == '&<> '


def test_url():
assert util.url_escape('hi there?') == 'hi%20there%3F'
assert util.url_unescape('hi%20there%3f') == 'hi there?'
assert util.url_unescape('hi%20there%3f') == 'hi there?'

0 comments on commit dce0093

Please sign in to comment.