Skip to content

Commit

Permalink
Merge pull request #1 from shosca/master
Browse files Browse the repository at this point in the history
Add attribute support
  • Loading branch information
miki725 authored May 11, 2017
2 parents 5a72ca9 + dadb0b8 commit b734145
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 35 deletions.
2 changes: 1 addition & 1 deletion AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ Development Lead
Contributors
~~~~~~~~~~~~

None yet. Why not be the first?
* Serkan Hosca - https://github.com/shosca
7 changes: 7 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
History
-------

0.2.0 (2017-05-11)
~~~~~~~~~~~~~~~~~~

* Add attribute support

* Add make watch target

0.1.0 (2017-03-22)
~~~~~~~~~~~~~~~~~~

Expand Down
66 changes: 40 additions & 26 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,65 +1,79 @@
.PHONY: clean-pyc clean-build docs clean
.PHONY: watch clean-pyc clean-build docs clean

NOSE_FLAGS=-sv --with-doctest --rednose
COVER_CONFIG_FLAGS=--with-coverage --cover-package=pycontext,tests --cover-tests --cover-erase
COVER_REPORT_FLAGS=--cover-html --cover-html-dir=htmlcov
COVER_FLAGS=${COVER_CONFIG_FLAGS} ${COVER_REPORT_FLAGS}

# automatic help generator
help:
@echo "install - install all requirements including for testing"
@echo "clean - remove all artifacts"
@echo "clean-build - remove build artifacts"
@echo "clean-pyc - remove Python file artifacts"
@echo "clean-test - remove test and coverage artifacts"
@echo "lint - check style with flake8"
@echo "test - run tests quickly with the default Python"
@echo "test-coverage - run tests with coverage report"
@echo "test-all - run tests on every Python version with tox"
@echo "check - run all necessary steps to check validity of project"
@echo "release - package and upload a release"
@echo "dist - package"

install:
@for f in $(MAKEFILE_LIST) ; do \
echo "$$f:" ; \
grep -E '^[a-zA-Z_-%]+:.*?## .*$$' $$f | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' ; \
done ; \

install: ## install all requirements including for testing
pip install -U -r requirements-dev.txt

clean: clean-build clean-pyc clean-test-all
clean: clean-build clean-pyc clean-test-all ## remove all artifacts

clean-build:
clean-build: ## remove build artifacts
@rm -rf build/
@rm -rf dist/
@rm -rf *.egg-info

clean-pyc:
clean-pyc: ## remove Python file artifacts
-@find . -name '*.pyc' -follow -print0 | xargs -0 rm -f
-@find . -name '*.pyo' -follow -print0 | xargs -0 rm -f
-@find . -name '__pycache__' -type d -follow -print0 | xargs -0 rm -rf

clean-test:
clean-test: ## remove test and coverage artifacts
rm -rf .coverage coverage*
rm -rf htmlcov/

clean-test-all: clean-test
rm -rf .tox/

lint:
lint: ## check style with flake8
flake8 pycontext tests

test:
test: ## run tests quickly with the default Python
nosetests ${NOSE_FLAGS} tests/ pycontext/

test-coverage:
test-coverage: ## run tests with coverage report
nosetests ${NOSE_FLAGS} ${COVER_FLAGS} tests/ pycontext/

test-all:
test-all: ## run tests on every Python version with tox
tox

check: lint clean-build clean-pyc clean-test test-coverage
check: lint clean-build clean-pyc clean-test test-coverage ## run all necessary steps to check validity of project

release: clean
release: clean ## release - package and upload a release
python setup.py sdist upload
python setup.py bdist_wheel upload

dist: clean
dist: clean ## package
python setup.py sdist
python setup.py bdist_wheel
ls -l dist

.NOTPARALLEL: watch
WATCH_EVENTS=modify,close_write,moved_to,create
watch: ## watch file changes to run a command, e.g. make watch test
@if ! type "inotifywait" > /dev/null; then \
echo "Please install inotify-tools" ; \
fi; \
echo "Watching $(pwd) to run: $(WATCH_ARGS)" ; \
while true; do \
$(MAKE) $(WATCH_ARGS) ; \
inotifywait -e $(WATCH_EVENTS) -r --exclude '.*(git|~)' . ; \
done \

# This needs to be at the bottom as it'll convert things to do-nothing targets
# If the first argument is "watch"...
ifeq (watch,$(firstword $(MAKECMDGOALS)))
# use the rest as arguments for "watch"
WATCH_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
# ...and turn them into do-nothing targets
$(eval $(WATCH_ARGS):;@:)
endif
2 changes: 1 addition & 1 deletion pycontext/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@

__author__ = 'Miroslav Shubernetskiy'
__author_email__ = '[email protected]'
__version__ = '0.1.0'
__version__ = '0.2'
__description__ = 'Python dict with stacked context data'
47 changes: 40 additions & 7 deletions pycontext/context.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals

import copy
from collections import Mapping, deque
from copy import deepcopy
from itertools import chain


Expand Down Expand Up @@ -50,6 +51,8 @@ class Context(Mapping):
>>> assert ctxt['user'] == 'Fred'
"""

__slots__ = ('frames',)

def __init__(self, context_data=None, **kwargs):
"""
Initialize the template context with the dictionary
Expand Down Expand Up @@ -109,6 +112,21 @@ def __getitem__(self, key):
raise KeyError(key)
return value

def __getattr__(self, key):
"""
Get attribute's value, starting at the current scope and going upward.
:param key: the name of the attribute
:param value: the variable value
"""
try:
super(Context, self).__getattr__(key)
except AttributeError:
value, frame = self._find(key)
if frame is not None:
return value
raise

def __len__(self):
"""
Return the number of keys in the context.
Expand All @@ -130,6 +148,18 @@ def __setitem__(self, key, value):
"""
self.frames[0][key] = value

def __setattr__(self, key, value):
"""
Set an attribute in the current scope.
:param key: the name of the variable
:param value: the variable value
"""
try:
super(Context, self).__setattr__(key, value)
except AttributeError:
self.__setitem__(key, value)

def __eq__(self, other):
"""
Compare this context with other objects
Expand Down Expand Up @@ -189,12 +219,15 @@ def __deepcopy__(self, memo=None):
# that ensures that copy of correct subclass is created
new_context = self.__class__()

# need to loop over the vars to copy all
# class attributes including possibly added by subclasses
for attr, value in vars(self).items():
if callable(getattr(self, attr)):
continue
setattr(new_context, attr, deepcopy(getattr(self, attr), memo))
# pop as we we insert a frame by default
new_context.frames.popleft()

# need to loop over frame by frame in reverse order and
# copy all attributes in a frame to the new context
for frame in reversed(self.frames):
new_context.frames.appendleft({
attr: copy.deepcopy(value) for attr, value in frame.items() if not callable(value)
})

return new_context

Expand Down
14 changes: 14 additions & 0 deletions tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,20 @@ def test_keys(self):
sorted(['qwe', 'foo', 'hello']),
)

def test_attributes(self):
"""
Test attribute functionality
"""
self.context.hello = 'world'
self.context.push({'foo': 'bar'})
self.context.push({'qwe': 'asd'})
self.assertEqual(self.context.foo, 'bar')
self.assertEqual(self.context.hello, 'world')
self.assertEqual(self.context.qwe, 'asd')

with self.assertRaises(AttributeError):
self.context.bar

def test_values(self):
"""
Test values functionality
Expand Down

0 comments on commit b734145

Please sign in to comment.