diff --git a/AUTHORS.rst b/AUTHORS.rst index f984277..25d1f1e 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -12,4 +12,4 @@ Development Lead Contributors ~~~~~~~~~~~~ -None yet. Why not be the first? +* Serkan Hosca - https://github.com/shosca diff --git a/HISTORY.rst b/HISTORY.rst index e81ea90..3255f32 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,13 @@ History ------- +0.2.0 (2017-05-11) +~~~~~~~~~~~~~~~~~~ + +* Add attribute support + +* Add make watch target + 0.1.0 (2017-03-22) ~~~~~~~~~~~~~~~~~~ diff --git a/Makefile b/Makefile index 1b7d9c9..1f50378 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/pycontext/__init__.py b/pycontext/__init__.py index 646f383..813e621 100644 --- a/pycontext/__init__.py +++ b/pycontext/__init__.py @@ -4,5 +4,5 @@ __author__ = 'Miroslav Shubernetskiy' __author_email__ = 'miroslav.shubernetskiy@coxautoinc.com' -__version__ = '0.1.0' +__version__ = '0.2' __description__ = 'Python dict with stacked context data' diff --git a/pycontext/context.py b/pycontext/context.py index 08d5a6c..c94f9d3 100644 --- a/pycontext/context.py +++ b/pycontext/context.py @@ -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 @@ -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 @@ -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. @@ -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 @@ -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 diff --git a/tests/test_context.py b/tests/test_context.py index 612008f..838f8d3 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -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