├── tests ├── __init__.py ├── fixtures │ ├── two │ │ ├── __init__.py │ │ ├── mod2.py │ │ └── mod1.py │ ├── pyc │ │ ├── __init__.py │ │ ├── subpackage │ │ │ └── __init__.py │ │ └── module.py │ ├── one │ │ ├── __init__.py │ │ ├── module.py │ │ └── module2.py │ ├── importonly │ │ ├── __init__.py │ │ ├── two.py │ │ └── one.py │ ├── subpackages │ │ ├── childpackage │ │ │ ├── __init__.py │ │ │ └── will_cause_import_error.py │ │ ├── mod2.py │ │ └── __init__.py │ ├── import_and_scan │ │ ├── __init__.py │ │ ├── two.py │ │ ├── one.py │ │ └── mock.py │ ├── attrerror │ │ ├── will_cause_import_error.py │ │ └── __init__.py │ ├── importerror │ │ ├── will_cause_import_error.py │ │ └── __init__.py │ ├── attrerror_package │ │ ├── will_cause_import_error │ │ │ └── __init__.py │ │ └── __init__.py │ ├── importerror_package │ │ ├── will_cause_import_error │ │ │ └── __init__.py │ │ └── __init__.py │ ├── zipped.zip │ ├── nested │ │ ├── __init__.py │ │ ├── sub1 │ │ │ ├── __init__.py │ │ │ └── subsub1 │ │ │ │ └── __init__.py │ │ └── sub2 │ │ │ ├── __init__.py │ │ │ └── subsub2 │ │ │ └── __init__.py │ ├── inheritance.py │ ├── class_and_method.py │ ├── classdecorator.py │ ├── category.py │ ├── subclassing.py │ ├── lifting4.py │ ├── lifting1.py │ ├── lifting2.py │ ├── lifting5.py │ ├── __init__.py │ └── lifting3.py ├── test_advice.py └── test_venusian.py ├── COPYRIGHT.txt ├── setup.py ├── .coveragerc ├── .github ├── dependabot.yml └── workflows │ └── ci-tests.yml ├── src └── venusian │ ├── compat.py │ ├── advice.py │ └── __init__.py ├── docs ├── api.rst ├── glossary.rst ├── Makefile ├── conf.py └── index.rst ├── MANIFEST.in ├── .gitignore ├── pyproject.toml ├── .readthedocs.yaml ├── README.rst ├── setup.cfg ├── tox.ini ├── LICENSE.txt ├── CONTRIBUTING.rst ├── CONTRIBUTORS.txt └── CHANGES.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # package 2 | -------------------------------------------------------------------------------- /tests/fixtures/two/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/pyc/__init__.py: -------------------------------------------------------------------------------- 1 | # pkg 2 | -------------------------------------------------------------------------------- /tests/fixtures/one/__init__.py: -------------------------------------------------------------------------------- 1 | # package 2 | -------------------------------------------------------------------------------- /tests/fixtures/importonly/__init__.py: -------------------------------------------------------------------------------- 1 | # package 2 | -------------------------------------------------------------------------------- /tests/fixtures/subpackages/childpackage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/import_and_scan/__init__.py: -------------------------------------------------------------------------------- 1 | # package 2 | -------------------------------------------------------------------------------- /tests/fixtures/two/mod2.py: -------------------------------------------------------------------------------- 1 | from .mod1 import Class 2 | -------------------------------------------------------------------------------- /tests/fixtures/subpackages/mod2.py: -------------------------------------------------------------------------------- 1 | raise AttributeError 2 | -------------------------------------------------------------------------------- /tests/fixtures/attrerror/will_cause_import_error.py: -------------------------------------------------------------------------------- 1 | raise AttributeError 2 | -------------------------------------------------------------------------------- /tests/fixtures/importerror/will_cause_import_error.py: -------------------------------------------------------------------------------- 1 | import doesnt.exist 2 | -------------------------------------------------------------------------------- /tests/fixtures/attrerror_package/will_cause_import_error/__init__.py: -------------------------------------------------------------------------------- 1 | raise AttributeError 2 | -------------------------------------------------------------------------------- /tests/fixtures/subpackages/childpackage/will_cause_import_error.py: -------------------------------------------------------------------------------- 1 | import doesnt.exist 2 | -------------------------------------------------------------------------------- /tests/fixtures/importerror_package/will_cause_import_error/__init__.py: -------------------------------------------------------------------------------- 1 | import doesnt.exist 2 | -------------------------------------------------------------------------------- /tests/fixtures/zipped.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pylons/venusian/HEAD/tests/fixtures/zipped.zip -------------------------------------------------------------------------------- /tests/fixtures/two/mod1.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator() 5 | class Class(object): 6 | pass 7 | -------------------------------------------------------------------------------- /COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Agendaless Consulting and Contributors. 2 | (http://www.agendaless.com), All Rights Reserved 3 | 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Necessary for pip install -e, and python setup.py check 3 | """ 4 | 5 | from setuptools import setup 6 | 7 | setup() 8 | -------------------------------------------------------------------------------- /tests/fixtures/attrerror/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def function(request): # pragma: no cover 6 | return request 7 | -------------------------------------------------------------------------------- /tests/fixtures/importonly/two.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def twofunction(request): # pragma: no cover 6 | return request 7 | -------------------------------------------------------------------------------- /tests/fixtures/nested/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def function(request): # pragma: no cover 6 | return request 7 | -------------------------------------------------------------------------------- /tests/fixtures/import_and_scan/two.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def twofunction(request): # pragma: no cover 6 | return request 7 | -------------------------------------------------------------------------------- /tests/fixtures/importerror/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def function(request): # pragma: no cover 6 | return request 7 | -------------------------------------------------------------------------------- /tests/fixtures/nested/sub1/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def function(request): # pragma: no cover 6 | return request 7 | -------------------------------------------------------------------------------- /tests/fixtures/nested/sub2/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def function(request): # pragma: no cover 6 | return request 7 | -------------------------------------------------------------------------------- /tests/fixtures/subpackages/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def function(request): # pragma: no cover 6 | return request 7 | -------------------------------------------------------------------------------- /tests/fixtures/attrerror_package/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def function(request): # pragma: no cover 6 | return request 7 | -------------------------------------------------------------------------------- /tests/fixtures/importerror_package/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def function(request): # pragma: no cover 6 | return request 7 | -------------------------------------------------------------------------------- /tests/fixtures/inheritance.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator() 5 | class Parent(object): 6 | pass 7 | 8 | 9 | class Child(Parent): 10 | pass 11 | -------------------------------------------------------------------------------- /tests/fixtures/nested/sub1/subsub1/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def function(request): # pragma: no cover 6 | return request 7 | -------------------------------------------------------------------------------- /tests/fixtures/nested/sub2/subsub2/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def function(request): # pragma: no cover 6 | return request 7 | -------------------------------------------------------------------------------- /tests/fixtures/pyc/subpackage/__init__.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def pkgfunction(request): # pragma: no cover 6 | return request 7 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | parallel = true 3 | source = 4 | venusian 5 | 6 | [paths] 7 | source = 8 | src/venusian 9 | */src/venusian 10 | */site-packages/venusian 11 | 12 | [report] 13 | show_missing = true 14 | precision = 2 15 | -------------------------------------------------------------------------------- /tests/fixtures/class_and_method.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(class_=True) 5 | class ClassWithMethod(object): 6 | @decorator(method=True) 7 | def method_on_class(self): # pragma: no cover 8 | pass 9 | -------------------------------------------------------------------------------- /tests/fixtures/classdecorator.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(superclass=True) 5 | class SuperClass(object): 6 | pass 7 | 8 | 9 | @decorator(subclass=True) 10 | class SubClass(SuperClass): 11 | pass 12 | -------------------------------------------------------------------------------- /tests/fixtures/importonly/one.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | from tests.fixtures.importonly.two import twofunction # should not be scanned 3 | 4 | 5 | @decorator(function=True) 6 | def onefunction(request): # pragma: no cover 7 | return request 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | 3 | version: 2 4 | updates: 5 | 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | # Check for updates to GitHub Actions every weekday 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /tests/fixtures/import_and_scan/one.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | from tests.fixtures.import_and_scan.two import twofunction # should not be scanned 3 | 4 | 5 | @decorator(function=True) 6 | def onefunction(request): # pragma: no cover 7 | twofunction(request) 8 | return request 9 | -------------------------------------------------------------------------------- /tests/fixtures/category.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import categorydecorator, categorydecorator2 2 | 3 | 4 | @categorydecorator(function=True) 5 | def function(request): # pragma: no cover 6 | return request 7 | 8 | 9 | @categorydecorator2(function=True) 10 | def function2(request): # pragma: no cover 11 | return request 12 | -------------------------------------------------------------------------------- /src/venusian/compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info[0] == 3 and sys.version_info[1] < 10: 4 | 5 | def compat_find_loader(importer, modname): 6 | return importer.find_module(modname) 7 | 8 | else: 9 | 10 | def compat_find_loader(importer, modname): 11 | spec = importer.find_spec(modname) 12 | return spec.loader 13 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation for Venusian 2 | ============================== 3 | 4 | .. automodule:: venusian 5 | 6 | .. autoclass:: Scanner 7 | 8 | .. automethod:: scan 9 | 10 | .. autoclass:: AttachInfo 11 | 12 | .. autofunction:: attach(wrapped, callback, category=None, name=None) 13 | 14 | .. autoclass:: lift 15 | 16 | .. autoclass:: onlyliftedfrom 17 | -------------------------------------------------------------------------------- /tests/fixtures/subclassing.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | class Super(object): # pragma: no cover 5 | @decorator() 6 | def classname(self): 7 | pass 8 | 9 | @decorator() 10 | def boo(self): 11 | pass 12 | 13 | 14 | # the Sub class must not inherit the decorations of its superclass when scanned 15 | 16 | 17 | class Sub(Super): # pragma: no cover 18 | pass 19 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft src/venusian 2 | graft docs 3 | prune docs/_build 4 | graft tests 5 | 6 | include README.rst 7 | include CHANGES.rst 8 | include CONTRIBUTING.rst 9 | include CONTRIBUTORS.txt 10 | include LICENSE.txt 11 | include COPYRIGHT.txt 12 | 13 | include .coveragerc pyproject.toml setup.cfg 14 | include tox.ini .readthedocs.yaml 15 | graft .github 16 | 17 | global-exclude __pycache__ *.py[cod] 18 | global-exclude .DS_Store 19 | -------------------------------------------------------------------------------- /docs/glossary.rst: -------------------------------------------------------------------------------- 1 | .. _glossary: 2 | 3 | Glossary 4 | ======== 5 | 6 | .. glossary:: 7 | :sorted: 8 | 9 | scan 10 | Walk a module or package executing callbacks defined by 11 | venusian-aware decorators along the way. 12 | 13 | Martian 14 | The package venusian was inspired by, part of the :term:`Grok` 15 | project. 16 | 17 | Grok 18 | A Zope-based `web framework `. 19 | 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *$py.class 2 | *.egg 3 | *.egg-info 4 | *.pt.py 5 | *.pyc 6 | *.pyo 7 | *.swp 8 | *.txt.py 9 | *~ 10 | .*.swp 11 | .cache/ 12 | .coverage 13 | .coverage.* 14 | .tox/ 15 | __pycache__/ 16 | build/ 17 | coverage*.xml 18 | coverage-py* 19 | coverage.xml 20 | dist/ 21 | docs/_build/ 22 | docs/_themes/ 23 | env*/ 24 | jyenv/ 25 | nosetests-py* 26 | nosetests.xml 27 | pypyenv/ 28 | pytest*.xml 29 | venusian.egg-info/ 30 | venusian/coverage.xml 31 | -------------------------------------------------------------------------------- /tests/fixtures/one/module.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def function(request): # pragma: no cover 6 | return request 7 | 8 | 9 | class Class(object): 10 | @decorator(method=True) 11 | def method(self, request): # pragma: no cover 12 | return request 13 | 14 | 15 | class Instance(object): 16 | def __call__(self, request): # pragma: no cover 17 | return request 18 | 19 | 20 | inst = Instance() 21 | inst = decorator(instance=True)(inst) 22 | -------------------------------------------------------------------------------- /tests/fixtures/one/module2.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def function(request): # pragma: no cover 6 | return request 7 | 8 | 9 | class Class(object): 10 | @decorator(method=True) 11 | def method(self, request): # pragma: no cover 12 | return request 13 | 14 | 15 | class Instance(object): 16 | def __call__(self, request): # pragma: no cover 17 | return request 18 | 19 | 20 | inst = Instance() 21 | inst = decorator(instance=True)(inst) 22 | -------------------------------------------------------------------------------- /tests/fixtures/pyc/module.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | 3 | 4 | @decorator(function=True) 5 | def function(request): # pragma: no cover 6 | return request 7 | 8 | 9 | class Class(object): 10 | @decorator(method=True) 11 | def method(self, request): # pragma: no cover 12 | return request 13 | 14 | 15 | class Instance(object): 16 | def __call__(self, request): # pragma: no cover 17 | return request 18 | 19 | 20 | inst = Instance() 21 | inst = decorator(instance=True)(inst) 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 40.9.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | target-version = ['py37', 'py38', 'py39', 'py310'] 7 | exclude = ''' 8 | /( 9 | \.git 10 | | .tox 11 | | build 12 | )/ 13 | ''' 14 | 15 | # This next section only exists for people that have their editors 16 | # automatically call isort, black already sorts entries on its own when run. 17 | [tool.isort] 18 | profile = "black" 19 | src_paths = ["src", "tests"] 20 | known_first_party = "venusian" 21 | -------------------------------------------------------------------------------- /tests/fixtures/lifting4.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import categorydecorator, categorydecorator2 2 | from venusian import lift, onlyliftedfrom 3 | 4 | 5 | @onlyliftedfrom() 6 | class Super(object): # pragma: no cover 7 | @categorydecorator() 8 | def hiss(self): 9 | pass 10 | 11 | @categorydecorator2() 12 | def jump(self): 13 | pass 14 | 15 | 16 | @lift(("mycategory",)) 17 | class Sub(Super): # pragma: no cover 18 | def hiss(self): 19 | pass 20 | 21 | def jump(self): 22 | pass 23 | 24 | @categorydecorator2() 25 | def smack(self): 26 | pass 27 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-lts-latest 11 | tools: 12 | python: "latest" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # If using Sphinx, optionally build your docs in additional formats such as PDF 19 | formats: 20 | - pdf 21 | 22 | # Optionally declare the Python requirements required to build your docs 23 | python: 24 | install: 25 | - method: pip 26 | path: . 27 | extra_requirements: 28 | - docs 29 | - method: setuptools 30 | path: . 31 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | venusian 2 | ======== 3 | 4 | .. image:: https://github.com/Pylons/venusian/workflows/Build%20and%20test/badge.svg 5 | :target: https://github.com/Pylons/venusian/actions?query=workflow%3A%22Build+and+test%22 6 | 7 | .. image:: https://readthedocs.org/projects/venusian/badge/?version=latest 8 | :target: https://docs.pylonsproject.org/projects/venusian/en/latest/ 9 | :alt: Documentation Status 10 | 11 | Venusian is a library which allows framework authors to defer 12 | decorator actions. Instead of taking actions when a function (or 13 | class) decorator is executed at import time, you can defer the action 14 | usually taken by the decorator until a separate "scan" phase. 15 | 16 | See the "docs" directory of the package or the online documentation at 17 | https://docs.pylonsproject.org/projects/venusian/en/latest/ 18 | -------------------------------------------------------------------------------- /tests/fixtures/lifting1.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | from venusian import lift 3 | 4 | 5 | class Super1(object): # pragma: no cover 6 | @decorator() 7 | def classname(self): 8 | pass 9 | 10 | @decorator() 11 | def boo(self): 12 | pass 13 | 14 | @decorator() 15 | def ram(self): 16 | pass 17 | 18 | def jump(self): 19 | pass 20 | 21 | 22 | class Super2(object): # pragma: no cover 23 | def boo(self): 24 | pass 25 | 26 | @decorator() 27 | def hiss(self): 28 | pass 29 | 30 | @decorator() 31 | def jump(self): 32 | pass 33 | 34 | 35 | @lift() 36 | class Sub(Super1, Super2): # pragma: no cover 37 | def boo(self): 38 | pass 39 | 40 | def hiss(self): 41 | pass 42 | 43 | @decorator() 44 | def smack(self): 45 | pass 46 | -------------------------------------------------------------------------------- /tests/fixtures/import_and_scan/mock.py: -------------------------------------------------------------------------------- 1 | class _Call(tuple): # pragma: no cover 2 | def __new__(cls, value=(), name=None): 3 | name = "" 4 | args = () 5 | kwargs = {} 6 | _len = len(value) 7 | if _len == 3: 8 | name, args, kwargs = value 9 | return tuple.__new__(cls, (name, args, kwargs)) 10 | 11 | def __init__(self, value=(), name=None): 12 | self.name = name 13 | 14 | def __call__(self, *args, **kwargs): 15 | if self.name is None: 16 | return _Call(("", args, kwargs), name="()") 17 | else: 18 | return _Call((self.name, args, kwargs), name=self.name + "()") 19 | 20 | def __getattr__(self, attr): 21 | if self.name is None: 22 | return _Call(name=attr) 23 | else: 24 | return _Call(name="%s.%s" % (self.name, attr)) 25 | 26 | 27 | call = _Call() 28 | -------------------------------------------------------------------------------- /tests/fixtures/lifting2.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | from venusian import lift, onlyliftedfrom 3 | 4 | 5 | @onlyliftedfrom() 6 | class Super1(object): # pragma: no cover 7 | @decorator() 8 | def classname(self): 9 | pass 10 | 11 | @decorator() 12 | def boo(self): 13 | pass 14 | 15 | @decorator() 16 | def ram(self): 17 | pass 18 | 19 | def jump(self): 20 | pass 21 | 22 | 23 | @onlyliftedfrom() 24 | class Super2(object): # pragma: no cover 25 | def boo(self): 26 | pass 27 | 28 | @decorator() 29 | def hiss(self): 30 | pass 31 | 32 | @decorator() 33 | def jump(self): 34 | pass 35 | 36 | 37 | @lift() 38 | class Sub(Super1, Super2): # pragma: no cover 39 | def boo(self): 40 | pass 41 | 42 | def hiss(self): 43 | pass 44 | 45 | @decorator() 46 | def smack(self): 47 | pass 48 | -------------------------------------------------------------------------------- /tests/fixtures/lifting5.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | from venusian import lift 3 | 4 | 5 | class Super1(object): # pragma: no cover 6 | @decorator() 7 | def classname(self): 8 | pass 9 | 10 | @decorator() 11 | def boo(self): 12 | pass 13 | 14 | @decorator() 15 | def ram(self): 16 | pass 17 | 18 | @decorator() 19 | def jump(self): 20 | pass 21 | 22 | 23 | @lift() 24 | class Super2(Super1): # pragma: no cover 25 | @decorator() 26 | def boo(self): 27 | pass 28 | 29 | @decorator() 30 | def hiss(self): 31 | pass 32 | 33 | @decorator() 34 | def jump(self): 35 | pass 36 | 37 | 38 | @lift() 39 | class Sub(Super2): # pragma: no cover 40 | @decorator() 41 | def boo(self): 42 | pass 43 | 44 | @decorator() 45 | def hiss(self): 46 | pass 47 | 48 | @decorator() 49 | def smack(self): 50 | pass 51 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | import venusian 2 | 3 | 4 | class decorator(object): 5 | category = None 6 | call_count = 0 7 | 8 | def __init__(self, **kw): 9 | self.__dict__.update(kw) 10 | 11 | def __call__(self, wrapped): 12 | view_config = self.__dict__.copy() 13 | 14 | def callback(context, name, ob): 15 | if hasattr(context, "test"): 16 | context.test(ob=ob, name=name, **view_config) 17 | self.__class__.call_count += 1 18 | 19 | info = venusian.attach(wrapped, callback, category=self.category) 20 | if info.scope == "class": 21 | # we're in the midst of a class statement 22 | if view_config.get("attr") is None: 23 | view_config["attr"] = wrapped.__name__ 24 | return wrapped 25 | 26 | 27 | class categorydecorator(decorator): 28 | category = "mycategory" 29 | 30 | 31 | class categorydecorator2(decorator): 32 | category = "mycategory2" 33 | -------------------------------------------------------------------------------- /tests/fixtures/lifting3.py: -------------------------------------------------------------------------------- 1 | from tests.fixtures import decorator 2 | from venusian import lift, onlyliftedfrom 3 | 4 | 5 | @onlyliftedfrom() 6 | class NoDefinitions(object): 7 | pass 8 | 9 | 10 | @onlyliftedfrom() 11 | class Super1(object): # pragma: no cover 12 | @decorator() 13 | def classname(self): 14 | pass 15 | 16 | @decorator() 17 | def boo(self): 18 | pass 19 | 20 | @decorator() 21 | def ram(self): 22 | pass 23 | 24 | def jump(self): 25 | pass 26 | 27 | 28 | class Super2(object): # pragma: no cover 29 | def boo(self): 30 | pass 31 | 32 | @decorator() 33 | def hiss(self): 34 | pass 35 | 36 | @decorator() 37 | def jump(self): 38 | pass 39 | 40 | 41 | @lift() 42 | class Sub(Super1, Super2): # pragma: no cover 43 | def boo(self): 44 | pass 45 | 46 | def hiss(self): 47 | pass 48 | 49 | @decorator() 50 | def smack(self): 51 | pass 52 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = venusian 3 | version = 3.2.0dev0 4 | description = A library for deferring decorator actions 5 | long_description = file: README.rst, CHANGES.rst 6 | long_description_content_type = text/x-rst 7 | keywords = web wsgi zope 8 | license_files = LICENSE.txt 9 | license = BSD-derived (Repoze) 10 | classifiers = 11 | Development Status :: 6 - Mature 12 | Intended Audience :: Developers 13 | Programming Language :: Python :: 3 14 | Programming Language :: Python :: 3.9 15 | Programming Language :: Python :: 3.10 16 | Programming Language :: Python :: 3.11 17 | Programming Language :: Python :: 3.12 18 | Programming Language :: Python :: 3.13 19 | Programming Language :: Python :: Implementation :: CPython 20 | Programming Language :: Python :: Implementation :: PyPy 21 | License :: Repoze Public License 22 | url = https://pylonsproject.org/ 23 | author = Chris McDonough, Agendaless Consulting 24 | author_email = pylons-devel@googlegroups.com 25 | 26 | [options] 27 | package_dir= 28 | =src 29 | packages=find: 30 | python_requires = >=3.9 31 | 32 | [options.packages.find] 33 | where=src 34 | 35 | [options.extras_require] 36 | testing = 37 | pytest 38 | pytest-cov 39 | coverage 40 | docs = 41 | Sphinx>=4.3.2 42 | repoze.sphinx.autointerface 43 | pylons-sphinx-themes 44 | sphinx-copybutton 45 | 46 | [bdist_wheel] 47 | universal=0 48 | 49 | [tool:pytest] 50 | python_files = test_*.py 51 | testpaths = 52 | tests 53 | addopts = -W always --cov --cov-report=term-missing --ignore=tests/fixtures/ 54 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | lint, 4 | py39,py310,py311,py312,py313,pypy3 5 | docs, 6 | coverage 7 | isolated_build = True 8 | 9 | [testenv] 10 | commands = 11 | python --version 12 | python -mpytest {posargs:} 13 | extras = 14 | testing 15 | setenv = 16 | COVERAGE_FILE=.coverage.{envname} 17 | 18 | [testenv:coverage] 19 | commands = 20 | coverage combine 21 | coverage xml 22 | coverage report --show-missing 23 | deps = 24 | coverage 25 | setenv = 26 | COVERAGE_FILE=.coverage 27 | 28 | [testenv:lint] 29 | skip_install = True 30 | commands = 31 | isort --check-only --df . 32 | black --check --diff . 33 | check-manifest 34 | # build sdist/wheel 35 | python -m build -o {envdir}/dist . 36 | twine check {envdir}/dist/* 37 | deps = 38 | black 39 | build 40 | check-manifest 41 | isort 42 | readme_renderer 43 | twine 44 | 45 | [testenv:docs] 46 | allowlist_externals = 47 | make 48 | commands = 49 | make -C docs html BUILDDIR={envdir} SPHINXOPTS="-W -E" 50 | extras = 51 | docs 52 | setenv = 53 | LC_ALL=C.utf8 54 | 55 | [testenv:format] 56 | skip_install = true 57 | commands = 58 | isort . 59 | black . 60 | deps = 61 | black 62 | isort 63 | 64 | [testenv:build] 65 | skip_install = true 66 | commands = 67 | # clean up build/ and dist/ folders 68 | python -c 'import shutil; shutil.rmtree("build", ignore_errors=True)' 69 | # Make sure we aren't forgetting anything 70 | check-manifest 71 | # build sdist/wheel 72 | python -m build . 73 | # Verify all is well 74 | twine check dist/* 75 | 76 | deps = 77 | build 78 | check-manifest 79 | readme_renderer 80 | twine 81 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | License 2 | 3 | A copyright notice accompanies this license document that identifies 4 | the copyright holders. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | 1. Redistributions in source code must retain the accompanying 11 | copyright notice, this list of conditions, and the following 12 | disclaimer. 13 | 14 | 2. Redistributions in binary form must reproduce the accompanying 15 | copyright notice, this list of conditions, and the following 16 | disclaimer in the documentation and/or other materials provided 17 | with the distribution. 18 | 19 | 3. Names of the copyright holders must not be used to endorse or 20 | promote products derived from this software without prior 21 | written permission from the copyright holders. 22 | 23 | 4. If any files are modified, you must cause the modified files to 24 | carry prominent notices stating that you changed the files and 25 | the date of any change. 26 | 27 | Disclaimer 28 | 29 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND 30 | ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 31 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 32 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 33 | HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 34 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 35 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 36 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 37 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 38 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 39 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 40 | SUCH DAMAGE. 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/ci-tests.yml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | # Only on pushes to master or one of the release branches we build on push 5 | push: 6 | branches: 7 | - main 8 | - "[0-9].[0-9]+-branch" 9 | tags: 10 | - '**' 11 | # Build pull requests 12 | pull_request: 13 | 14 | jobs: 15 | test: 16 | strategy: 17 | matrix: 18 | py: 19 | - "3.9" 20 | - "3.10" 21 | - "3.11" 22 | - "3.12" 23 | - "3.13" 24 | - "pypy-3.10" 25 | os: 26 | - "ubuntu-latest" 27 | - "windows-latest" 28 | - "macos-13" 29 | architecture: 30 | - x64 31 | - x86 32 | 33 | exclude: 34 | # Linux and macOS don't have x86 python 35 | - os: "ubuntu-latest" 36 | architecture: x86 37 | - os: "macos-13" 38 | architecture: x86 39 | 40 | name: "Python: ${{ matrix.py }}-${{ matrix.architecture }} on ${{ matrix.os }}" 41 | runs-on: ${{ matrix.os }} 42 | steps: 43 | - uses: actions/checkout@v4 44 | - name: Setup python 45 | uses: actions/setup-python@v5 46 | with: 47 | python-version: ${{ matrix.py }} 48 | architecture: ${{ matrix.architecture }} 49 | - run: pip install tox 50 | - name: Running tox 51 | run: tox -e py 52 | coverage: 53 | runs-on: ubuntu-latest 54 | name: Validate coverage 55 | steps: 56 | - uses: actions/checkout@v4 57 | - name: Setup python 3.9 58 | uses: actions/setup-python@v5 59 | with: 60 | python-version: 3.9 61 | architecture: x64 62 | - run: pip install tox 63 | - run: tox -e py39,coverage 64 | docs: 65 | runs-on: ubuntu-latest 66 | name: Build the documentation 67 | steps: 68 | - uses: actions/checkout@v4 69 | - name: Setup python 70 | uses: actions/setup-python@v5 71 | with: 72 | python-version: 3.9 73 | architecture: x64 74 | - run: pip install tox 75 | - run: tox -e docs 76 | lint: 77 | runs-on: ubuntu-latest 78 | name: Lint the package 79 | steps: 80 | - uses: actions/checkout@v4 81 | - name: Setup python 82 | uses: actions/setup-python@v5 83 | with: 84 | python-version: 3.9 85 | architecture: x64 86 | - run: pip install tox 87 | - run: tox -e lint 88 | -------------------------------------------------------------------------------- /src/venusian/advice.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2003 Zope Corporation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """Class advice. 15 | 16 | This module was adapted from 'protocols.advice', part of the Python 17 | Enterprise Application Kit (PEAK). Please notify the PEAK authors 18 | (pje@telecommunity.com and tsarna@sarna.org) if bugs are found or 19 | Zope-specific changes are required, so that the PEAK version of this module 20 | can be kept in sync. 21 | 22 | PEAK is a Python application framework that interoperates with (but does 23 | not require) Zope 3 and Twisted. It provides tools for manipulating UML 24 | models, object-relational persistence, aspect-oriented programming, and more. 25 | Visit the PEAK home page at http://peak.telecommunity.com for more information. 26 | 27 | $Id: advice.py 25177 2004-06-02 13:17:31Z jim $ 28 | """ 29 | 30 | import inspect 31 | import sys 32 | 33 | 34 | def getFrameInfo(frame): 35 | """Return (kind,module,locals,globals) for a frame 36 | 37 | 'kind' is one of "exec", "module", "class", "function call", or "unknown". 38 | """ 39 | 40 | f_locals = frame.f_locals 41 | f_globals = frame.f_globals 42 | 43 | sameNamespace = f_locals is f_globals 44 | hasModule = "__module__" in f_locals 45 | hasName = "__name__" in f_globals 46 | 47 | sameName = hasModule and hasName 48 | sameName = sameName and f_globals["__name__"] == f_locals["__module__"] 49 | 50 | module = hasName and sys.modules.get(f_globals["__name__"]) or None 51 | 52 | namespaceIsModule = module and module.__dict__ is f_globals 53 | 54 | frameinfo = inspect.getframeinfo(frame) 55 | try: 56 | sourceline = frameinfo[3][0].strip() 57 | except: # pragma NO COVER 58 | # dont understand circumstance here, 3rdparty code without comment 59 | sourceline = frameinfo[3] 60 | 61 | codeinfo = frameinfo[0], frameinfo[1], frameinfo[2], sourceline 62 | 63 | if not namespaceIsModule: # pragma no COVER 64 | # some kind of funky exec 65 | kind = "exec" # don't know how to repeat this scenario 66 | elif sameNamespace and not hasModule: 67 | kind = "module" 68 | elif sameName and not sameNamespace: 69 | kind = "class" 70 | elif not sameNamespace: 71 | kind = "function call" 72 | else: # pragma NO COVER 73 | # How can you have f_locals is f_globals, and have '__module__' set? 74 | # This is probably module-level code, but with a '__module__' variable. 75 | kind = "unknown" 76 | return kind, module, f_locals, f_globals, codeinfo 77 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= -W 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Internal variables. 12 | PAPEROPT_a4 = -D latex_paper_size=a4 13 | PAPEROPT_letter = -D latex_paper_size=letter 14 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 15 | 16 | .PHONY: help clean html web pickle htmlhelp latex changes linkcheck 17 | 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " pickle to make pickle files (usable by e.g. sphinx-web)" 22 | @echo " htmlhelp to make HTML files and a HTML help project" 23 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 24 | @echo " changes to make an overview over all changed/added/deprecated items" 25 | @echo " linkcheck to check all external links for integrity" 26 | 27 | clean: 28 | -rm -rf $(BUILDDIR)/* 29 | 30 | html: 31 | mkdir -p $(BUILDDIR)/html $(BUILDDIR)/doctrees 32 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 33 | @echo 34 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 35 | 36 | text: 37 | mkdir -p $(BUILDDIR)/text $(BUILDDIR)/doctrees 38 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 39 | @echo 40 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/text." 41 | 42 | pickle: 43 | mkdir -p $(BUILDDIR)/pickle $(BUILDDIR)/doctrees 44 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 45 | @echo 46 | @echo "Build finished; now you can process the pickle files or run" 47 | @echo " sphinx-web $(BUILDDIR)/pickle" 48 | @echo "to start the sphinx-web server." 49 | 50 | web: pickle 51 | 52 | htmlhelp: 53 | mkdir -p $(BUILDDIR)/htmlhelp $(BUILDDIR)/doctrees 54 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 55 | @echo 56 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 57 | ".hhp project file in $(BUILDDIR)/htmlhelp." 58 | 59 | latex: 60 | mkdir -p $(BUILDDIR)/latex $(BUILDDIR)/doctrees 61 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 62 | cp _static/*.png $(BUILDDIR)/latex 63 | ./convert_images.sh 64 | cp _static/latex-warning.png $(BUILDDIR)/latex 65 | cp _static/latex-note.png $(BUILDDIR)/latex 66 | @echo 67 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 68 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 69 | "run these through (pdf)latex." 70 | 71 | changes: 72 | mkdir -p $(BUILDDIR)/changes $(BUILDDIR)/doctrees 73 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 74 | @echo 75 | @echo "The overview file is in $(BUILDDIR)/changes." 76 | 77 | linkcheck: 78 | mkdir -p $(BUILDDIR)/linkcheck $(BUILDDIR)/doctrees 79 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 80 | @echo 81 | @echo "Link check complete; look for any errors in the above output " \ 82 | "or in $(BUILDDIR)/linkcheck/output.txt." 83 | 84 | epub: 85 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 86 | @echo 87 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 88 | 89 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath(".")) 16 | import datetime 17 | 18 | import pkg_resources 19 | import pylons_sphinx_themes 20 | 21 | # -- General configuration --------------------------------------------------- 22 | 23 | # Add any Sphinx extension module names here, as strings. They can be 24 | # extensions coming with Sphinx (named "sphinx.ext.*") or your custom ones. 25 | extensions = [ 26 | "sphinx.ext.autodoc", 27 | "sphinx_copybutton", 28 | ] 29 | 30 | # General substitutions. 31 | author = "Pylons Project" 32 | year = datetime.datetime.now().year 33 | copyright = "2012-%s Pylons Project " % year 34 | 35 | # The default replacements for |version| and |release|, also used in various 36 | # other places throughout the built documents. 37 | # 38 | # The short X.Y version. 39 | version = pkg_resources.get_distribution("venusian").version 40 | # The full version, including alpha/beta/rc tags. 41 | release = version 42 | 43 | # The name of the Pygments (syntax highlighting) style to use. 44 | pygments_style = "sphinx" 45 | 46 | 47 | # -- Options for HTML output ------------------------------------------------- 48 | 49 | # The theme to use for HTML and HTML Help pages. See the documentation for 50 | # a list of builtin themes. 51 | # 52 | html_theme = "pylons" 53 | html_theme_path = pylons_sphinx_themes.get_html_themes_path() 54 | html_theme_options = dict( 55 | github_url="https://github.com/Pylons/venusian", 56 | canonical_url="https://docs.pylonsproject.org/projects/venusian/en/latest/", 57 | ) 58 | 59 | # Add any paths that contain custom static files (such as style sheets) here, 60 | # relative to this directory. They are copied after the builtin static files, 61 | # so a file named "default.css" will overwrite the builtin "default.css". 62 | # html_static_path = ['_static'] 63 | 64 | # Control display of sidebars 65 | html_sidebars = { 66 | "**": [ 67 | "localtoc.html", 68 | "ethicalads.html", 69 | "relations.html", 70 | "sourcelink.html", 71 | "searchbox.html", 72 | ] 73 | } 74 | 75 | # If not "", a "Last updated on:" timestamp is inserted at every page 76 | # bottom, using the given strftime format. 77 | html_last_updated_fmt = "%b %d, %Y" 78 | 79 | # Do not use smart quotes. 80 | smartquotes = False 81 | 82 | # Output file base name for HTML help builder. 83 | htmlhelp_basename = "atemplatedoc" 84 | 85 | 86 | # -- Options for LaTeX output ------------------------------------------------- 87 | 88 | # Grouping the document tree into LaTeX files. List of tuples 89 | # (source start file, target name, title, 90 | # author, document class [howto/manual]). 91 | latex_documents = [ 92 | ("index", "atemplate.tex", "venusian Documentation", "Pylons Project", "manual"), 93 | ] 94 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every 8 | little bit helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/Pylons/venusian/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" 30 | is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "feature" 36 | is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | venusian could always use more documentation, whether as part of the 42 | official venusian docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at 49 | https://github.com/Pylons/venusian/issues. 50 | 51 | If you are proposing a feature: 52 | 53 | * Explain in detail how it would work. 54 | * Keep the scope as narrow as possible, to make it easier to implement. 55 | * Remember that this is a volunteer-driven project, and that contributions 56 | are welcome :) 57 | 58 | Get Started! 59 | ------------ 60 | 61 | Ready to contribute? Here's how to set up `venusian` for local development. 62 | 63 | 1. Fork the `venusian` repo on GitHub. 64 | 2. Clone your fork locally:: 65 | 66 | $ git clone git@github.com:your_name_here/venusian.git 67 | 68 | 3. Install your local copy into a virtualenv:: 69 | 70 | $ python3 -m venv env 71 | $ env/bin/pip install -e .[docs,testing] 72 | $ env/bin/pip install tox 73 | 74 | 4. Create a branch for local development:: 75 | 76 | $ git checkout -b name-of-your-bugfix-or-feature 77 | 78 | Now you can make your changes locally. 79 | 80 | 5. When you're done making changes, check that your changes pass flake8 and 81 | the tests, including testing other Python versions with tox:: 82 | 83 | $ env/bin/tox 84 | 85 | 6. Commit your changes and push your branch to GitHub:: 86 | 87 | $ git add . 88 | $ git commit -m "Your detailed description of your changes." 89 | $ git push origin name-of-your-bugfix-or-feature 90 | 91 | 7. Submit a pull request through the GitHub website. 92 | 93 | Pull Request Guidelines 94 | ----------------------- 95 | 96 | Before you submit a pull request, check that it meets these guidelines: 97 | 98 | 1. The pull request should include tests. 99 | 2. If the pull request adds functionality, the docs should be updated. Put 100 | your new functionality into a function with a docstring, and add the 101 | feature to the list in README.rst. 102 | 3. The pull request should work for all supported versions of Python 103 | (see the `classifiers` section in 104 | https://github.com/Pylons/venusian/blob/main/setup.cfg). Verify 105 | that the "All checks have passed" flag is green on the "Conversation" 106 | page of the PR before merging. 107 | 108 | Tips 109 | ---- 110 | 111 | To run a subset of tests:: 112 | 113 | $ env/bin/py.test tests.test_venusian 114 | -------------------------------------------------------------------------------- /tests/test_advice.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2003 Zope Corporation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """Tests for advice 15 | 16 | This module was adapted from 'protocols.tests.advice', part of the Python 17 | Enterprise Application Kit (PEAK). Please notify the PEAK authors 18 | (pje@telecommunity.com and tsarna@sarna.org) if bugs are found or 19 | Zope-specific changes are required, so that the PEAK version of this module 20 | can be kept in sync. 21 | 22 | PEAK is a Python application framework that interoperates with (but does 23 | not require) Zope 3 and Twisted. It provides tools for manipulating UML 24 | models, object-relational persistence, aspect-oriented programming, and more. 25 | Visit the PEAK home page at http://peak.telecommunity.com for more information. 26 | 27 | $Id: test_advice.py 40836 2005-12-16 22:40:51Z benji_york $ 28 | """ 29 | 30 | import sys 31 | import unittest 32 | 33 | from venusian import advice 34 | 35 | PY3 = sys.version_info[0] >= 3 36 | 37 | if not PY3: 38 | 39 | class ClassicClass: 40 | classLevelFrameInfo = advice.getFrameInfo(sys._getframe()) 41 | 42 | 43 | class NewStyleClass(object): 44 | classLevelFrameInfo = advice.getFrameInfo(sys._getframe()) 45 | 46 | 47 | moduleLevelFrameInfo = advice.getFrameInfo(sys._getframe()) 48 | 49 | 50 | class FrameInfoTest(unittest.TestCase): 51 | classLevelFrameInfo = advice.getFrameInfo(sys._getframe()) 52 | 53 | def testModuleInfo(self): 54 | kind, module, f_locals, f_globals, codeinfo = moduleLevelFrameInfo 55 | self.assertEqual(kind, "module") 56 | for d in module.__dict__, f_locals, f_globals: 57 | self.assertTrue(d is globals()) 58 | self.assertEqual(len(codeinfo), 4) 59 | 60 | if not PY3: 61 | 62 | def testClassicClassInfo(self): 63 | ( 64 | kind, 65 | module, 66 | f_locals, 67 | f_globals, 68 | codeinfo, 69 | ) = ClassicClass.classLevelFrameInfo 70 | self.assertEqual(kind, "class") 71 | 72 | self.assertTrue(f_locals is ClassicClass.__dict__) # ??? 73 | for d in module.__dict__, f_globals: 74 | self.assertTrue(d is globals()) 75 | self.assertEqual(len(codeinfo), 4) 76 | 77 | def testNewStyleClassInfo(self): 78 | ( 79 | kind, 80 | module, 81 | f_locals, 82 | f_globals, 83 | codeinfo, 84 | ) = NewStyleClass.classLevelFrameInfo 85 | self.assertEqual(kind, "class") 86 | 87 | for d in module.__dict__, f_globals: 88 | self.assertTrue(d is globals()) 89 | self.assertEqual(len(codeinfo), 4) 90 | 91 | def testCallInfo(self): 92 | (kind, module, f_locals, f_globals, codeinfo) = advice.getFrameInfo( 93 | sys._getframe() 94 | ) 95 | self.assertEqual(kind, "function call") 96 | frame = sys._getframe() 97 | self.assertEqual(f_locals, frame.f_locals) 98 | self.assertEqual(f_locals, locals()) 99 | for d in module.__dict__, f_globals: 100 | self.assertTrue(d is globals()) 101 | self.assertEqual(len(codeinfo), 4) 102 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Pylons Project Contributor Agreement 2 | ==================================== 3 | 4 | The submitter agrees by adding his or her name within the section below named 5 | "Contributors" and submitting the resulting modified document to the 6 | canonical shared repository location for this software project (whether 7 | directly, as a user with "direct commit access", or via a "pull request"), he 8 | or she is signing a contract electronically. The submitter becomes a 9 | Contributor after a) he or she signs this document by adding their name 10 | beneath the "Contributors" section below, and b) the resulting document is 11 | accepted into the canonical version control repository. 12 | 13 | Treatment of Account 14 | --------------------- 15 | 16 | Contributor will not allow anyone other than the Contributor to use his or 17 | her username or source repository login to submit code to a Pylons Project 18 | source repository. Should Contributor become aware of any such use, 19 | Contributor will immediately by notifying Agendaless Consulting. 20 | Notification must be performed by sending an email to 21 | webmaster@agendaless.com. Until such notice is received, Contributor will be 22 | presumed to have taken all actions made through Contributor's account. If the 23 | Contributor has direct commit access, Agendaless Consulting will have 24 | complete control and discretion over capabilities assigned to Contributor's 25 | account, and may disable Contributor's account for any reason at any time. 26 | 27 | Legal Effect of Contribution 28 | ---------------------------- 29 | 30 | Upon submitting a change or new work to a Pylons Project source Repository (a 31 | "Contribution"), you agree to assign, and hereby do assign, a one-half 32 | interest of all right, title and interest in and to copyright and other 33 | intellectual property rights with respect to your new and original portions 34 | of the Contribution to Agendaless Consulting. You and Agendaless Consulting 35 | each agree that the other shall be free to exercise any and all exclusive 36 | rights in and to the Contribution, without accounting to one another, 37 | including without limitation, the right to license the Contribution to others 38 | under the Repoze Public License. This agreement shall run with title to the 39 | Contribution. Agendaless Consulting does not convey to you any right, title 40 | or interest in or to the Program or such portions of the Contribution that 41 | were taken from the Program. Your transmission of a submission to the Pylons 42 | Project source Repository and marks of identification concerning the 43 | Contribution itself constitute your intent to contribute and your assignment 44 | of the work in accordance with the provisions of this Agreement. 45 | 46 | License Terms 47 | ------------- 48 | 49 | Code committed to the Pylons Project source repository (Committed Code) must 50 | be governed by the Repoze Public License (http://repoze.org/LICENSE.txt, aka 51 | "the RPL") or another license acceptable to Agendaless Consulting. Until 52 | Agendaless Consulting declares in writing an acceptable license other than 53 | the RPL, only the RPL shall be used. A list of exceptions is detailed within 54 | the "Licensing Exceptions" section of this document, if one exists. 55 | 56 | Representations, Warranty, and Indemnification 57 | ---------------------------------------------- 58 | 59 | Contributor represents and warrants that the Committed Code does not violate 60 | the rights of any person or entity, and that the Contributor has legal 61 | authority to enter into this Agreement and legal authority over Contributed 62 | Code. Further, Contributor indemnifies Agendaless Consulting against 63 | violations. 64 | 65 | Cryptography 66 | ------------ 67 | 68 | Contributor understands that cryptographic code may be subject to government 69 | regulations with which Agendaless Consulting and/or entities using Committed 70 | Code must comply. Any code which contains any of the items listed below must 71 | not be checked-in until Agendaless Consulting staff has been notified and has 72 | approved such contribution in writing. 73 | 74 | - Cryptographic capabilities or features 75 | 76 | - Calls to cryptographic features 77 | 78 | - User interface elements which provide context relating to cryptography 79 | 80 | - Code which may, under casual inspection, appear to be cryptographic. 81 | 82 | Notices 83 | ------- 84 | 85 | Contributor confirms that any notices required will be included in any 86 | Committed Code. 87 | 88 | Licensing Exceptions 89 | ==================== 90 | 91 | None. 92 | 93 | List of Contributors 94 | ==================== 95 | 96 | The below-signed are contributors to a code repository that is part of the 97 | project named "Venusian". Each below-signed contributor has read, understand 98 | and agrees to the terms above in the section within this document entitled 99 | "Pylons Project Contributor Agreement" as of the date beside his or her name. 100 | 101 | Contributors 102 | ------------ 103 | 104 | - Chris McDonough, 2011/02/16 105 | - Chris Withers, 2011/03/14 106 | - Joel Bohman, 2011/07/28 107 | - Olaf Conradi, 2013/09/16 108 | - Wichert Akkerman, 2015/02/23 109 | - Marc Abramowitz, 2015/02/24 110 | - Bert JW Regeer, 2017-04-24 111 | - Steve Piercy, 2017-08-31 112 | - Gouji Ochiai, 2022-12-23 113 | - Florian Schulze, 2023-10-31 114 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 3.2.0 (unreleased) 2 | ------------------ 3 | 4 | - Remove support for Python 3.7 and 3.8. 5 | 6 | 3.1.1 (2024-12-01) 7 | ------------------ 8 | 9 | - Add support for Python 3.13 (thanks to musicinmybrain). 10 | 11 | - Fix GitHub test actions. 12 | 13 | 3.1.0 (2023-11-06) 14 | ------------------ 15 | 16 | - Remove support for Python 3.5 and 3.6 17 | 18 | - Add support for Python 3.9, 3.10, 3.11 and 3.12. 19 | 20 | - Use GitHub Actions instead of Travis. 21 | 22 | 3.0.0 (2019-10-04) 23 | ------------------ 24 | 25 | - This release matches 2.0.0 other than in the version number. This fixes an 26 | issue with Requires-Python metadata not being uploaded correctly to PyPi. 27 | 28 | This version is only compatible with Python 3.5+ 29 | 30 | 2.0.0 (2019-10-04) 31 | ------------------ 32 | 33 | - Drop support for Python 2.7, 3.3, and 3.4 34 | 35 | - Removed the usage of the ``imp`` module to squelch the warnings regarding a 36 | deprecated modules. See https://github.com/Pylons/venusian/pull/63 and 37 | https://github.com/Pylons/venusian/issues/57 38 | 39 | 1.2.0 (2019-01-08) 40 | ------------------ 41 | 42 | - Add support for Python 3.7. 43 | 44 | - Drop support for Python 3.3. 45 | 46 | 1.1.0 (2017-04-24) 47 | ------------------ 48 | 49 | - Updated to using py.test instead of nosetest, and added support for Python 50 | 3.4 -> 3.6 51 | 52 | - Make scanning more resilient of metaclasses that return proxies for any 53 | attribute access. 54 | 55 | - Fix bug where using the same venusian decorator on both a class and its 56 | methods would cause the method decorations to be ignored. See 57 | https://github.com/Pylons/venusian/issues/40 58 | 59 | - Drop support for Python 2.6. 60 | 61 | - Drop support for Python 3.2: it is no longer supported by current 62 | packaging / CI tools. 63 | 64 | - Support loaders that require the module name as argument to their 65 | ``get_filename()`` method. This fixes problems with zipped packages 66 | on Python 3. 67 | 68 | - Micro-optimization when ignores are used (see 69 | https://github.com/Pylons/venusian/pull/20). 70 | 71 | - A tox run now combines coverage between Py2 and Py3. 72 | 73 | 1.0 (2014-06-30) 74 | ---------------- 75 | 76 | - Fix an issue under PyPy > 2.0 where attached decorators may not be found. 77 | 78 | - Drop support of Python 2.4 / 2.5 / Jython. 79 | 80 | - Add ``lift`` and ``onlyliftedfrom`` class decorators to allow for inheritance 81 | of venusian decorators attached to superclass methods. See the API 82 | documentation for more information. 83 | 84 | - Fix bug where otherwise undecorated subclass of a superclass that had 85 | venusian decorators on it would inherit its superclass' decorations. 86 | Venusian decorators should have never been inherited implicitly. See 87 | https://github.com/Pylons/venusian/issues/11#issuecomment-4977352 88 | 89 | 1.0a8 (2013-04-15) 90 | ------------------ 91 | 92 | - Pass ``ignore`` argument along recursively to ``walk_packages`` so custom 93 | ignore functions will ignore things recursively. See 94 | https://github.com/Pylons/venusian/pull/16 95 | 96 | - Don't run tox tests under Python 2.4 anymore (tox no longer supports 2.4). 97 | 98 | 1.0a7 (2012-08-25) 99 | ------------------ 100 | 101 | - Venusian now works on Python 3.3b2+ (importlib-based). 102 | 103 | - Use nose-exclude instead of relying on fragile module-scope code to ensure 104 | we don't get errors resulting from import of fixture code during 105 | "nosetests". 106 | 107 | - Bug fix: no longer suppress ``ImportError`` while scanning by default. If 108 | you want to suppress ``ImportError`` while scanning, you'll now need use an 109 | ``onerror`` callback as described in the documentation. 110 | 111 | 1.0a6 (2012-04-23) 112 | ------------------ 113 | 114 | - Don't ignore decorated objects within their original locations if they 115 | happen to be imported into another module (remove ``seen`` set from invoke 116 | in venusian scanning). See https://github.com/Pylons/venusian/pull/13 . 117 | 118 | 1.0a5 (2012-04-21) 119 | ------------------ 120 | 121 | - Slightly less sucky way to ignore objects during scanning that are only 122 | imported into a module but not actually defined there. See 1.0a4 change 123 | notes for rationale. Now instead of checking whether the module of the 124 | *scanned object* matches the module being scanned, we check whether the 125 | module of the *Venusian attachment* matches the module being scanned. This 126 | allows some genuine uses of imported objects as Venusian scan targets while 127 | preventing inappropriate double-scanning of objects that have a venusian 128 | attachment which just happen to be imported into other scanned modules. 129 | 130 | - Add ``dev`` and ``docs`` setup.py commands (ala Pyramid). 131 | 132 | 1.0a4 (2012-04-16) 133 | ------------------ 134 | 135 | - Attempt to ignore objects during scanning that are only imported into a 136 | module but not actually defined there. This is a semantics change, but 137 | it's the right thing to do, because I found myself facing a situation like 138 | this:: 139 | 140 | # in a module named "one" 141 | 142 | from two import anotheradecoratedthing 143 | @adecorator 144 | def adecoratedthing(): pass 145 | 146 | # and scanning both modules 147 | scan('one') 148 | scan('two') 149 | 150 | In this case you'd wind up with two repeated registrations for 151 | "anotherdecoratedthing", which isn't what anyone expects. 152 | 153 | 1.0a3 (2012-02-08) 154 | ------------------ 155 | 156 | - Add an ``ignore`` argument to the ``scan`` method of a ``Scanner``. This 157 | argument allows a user to ignore packages, modules, and global objects by 158 | name during a ``scan``. See the "ignore Scan Argument" in the narrative 159 | documentation for more details. 160 | 161 | 1.0a2 (2011-09-02) 162 | ------------------ 163 | 164 | - Close ImpLoader file handle to avoid resource warnings on Python 3. 165 | 166 | 1.0a1 (2011-08-27) 167 | ------------------ 168 | 169 | - Python 3 compatibility. 170 | 171 | - Allow an ``onerror`` callback to be passed to ``Scanner.scan()``. 172 | 173 | 0.9 (2011-06-18) 174 | ---------------- 175 | 176 | - Prevent corner case scan-time exception when trying to introspect insane 177 | module-scope objects. See https://github.com/Pylons/venusian/issues/5 . 178 | 179 | 0.8 (2011-04-30) 180 | ---------------- 181 | 182 | - Normal "setup.py test" can't support running the venusian tests under py 183 | 2.4 or 2.5; when it scans the 'classdecorators' fixture, it barfs. To get 184 | around this, we used to depend on ``nose`` in ``setup_requires`` and tell 185 | "setup.py test" to use nose by setting test_suite to "nose.collector" but 186 | we can't anymore because folks use Venusian in systems which install from 187 | pip bundles; pip bundles do not support setup_requires. So, sorry, we're 188 | painted into a corner; at this point you just have to know to install nose 189 | and run "setup.py nosetests" rather than "setup.py test". Or just run 190 | "tox" which tests it under all Pythons. 191 | 192 | 0.7 (2011-03-16) 193 | ---------------- 194 | 195 | - Use Pylons theme in documentation. 196 | 197 | - Fix orphaned pyc test on pypy. 198 | 199 | - Fix GitHub Issue #1: subclasses of decorated classes that do not 200 | have any decorations should not inherit the decorations of their 201 | parent classes. 202 | 203 | - Fix GitHub Issue #2: scans should only "find" each object once per 204 | scan, regardless of how many modules that object is imported into. 205 | 206 | 0.6 (2011-01-09) 207 | ---------------- 208 | 209 | - Some metaclasses (Elixir's) don't raise an AttributeError when asked for a 210 | nonexistent attribute during a scan. We now catch all exceptions when 211 | interrogating an object for ``__venusian_callbacks__`` rather than just 212 | AttributeError. 213 | 214 | 0.5 (2010-12-19) 215 | ---------------- 216 | 217 | - Make ``codeinfo`` attribute available as an attribute of the AttachInfo 218 | object. It will be a tuple in the form ``(filename, lineno, function, 219 | sourceline)`` representing the context of the venusian decorator. Eg. 220 | ``('/home/chrism/projects/venusian/tests/test_advice.py', 81, 221 | 'testCallInfo', 'add_handler(foo, bar)')`` 222 | 223 | 0.4 (2010-09-03) 224 | ---------------- 225 | 226 | - Bug fix: when a venusian decorator used as a class decorator was 227 | used against both a class *and* a subclass of that class, the 228 | superclass and subclass would effectively share the same set of 229 | callbacks. This was not the intent: each class declaration should 230 | have its own local set of callbacks; callbacks added via decorations 231 | should not be inherited, and a superclass should not receive its 232 | subclass' decorations. 233 | 234 | - Arrange test fixtures into a single directory. 235 | 236 | 0.3 (2010-06-24) 237 | ---------------- 238 | 239 | - Ignore orphaned modules (``.pyc`` or ``.pyo`` files without a 240 | corresponding ``.py`` file) during a scan. 241 | 242 | 0.2 (2010-04-18) 243 | ---------------- 244 | 245 | - Add the concept of scan categories (see the "Scan Categories" 246 | section of the documentation) to allow an application to make use of 247 | more than one Venusian-using framework simultaneously. 248 | 249 | 0.1 (2010-02-15) 250 | ---------------- 251 | 252 | - Initial release. 253 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. _venusian: 2 | 3 | Venusian 4 | ======== 5 | 6 | Venusian is a library which allows you to defer the action of decorators. 7 | Instead of taking actions when a function, method, or class decorator is 8 | executed at import time, you can defer the action until a separate "scan" 9 | phase. 10 | 11 | This library is most useful for framework authors. It is compatible with 12 | CPython versions 3.7+. It is also known to work on PyPy (Compatible with Python 13 | 3.7+). 14 | 15 | .. note:: 16 | 17 | The name "Venusian" is a riff on a library named :term:`Martian` 18 | (which had its genesis in the :term:`Grok` web framework), from 19 | which the idea for Venusian was stolen. Venusian is similar to 20 | Martian, but it offers less functionality, making it slightly 21 | simpler to use. 22 | 23 | Overview 24 | -------- 25 | 26 | Offering a decorator that wraps a function, method, or class can be a 27 | convenience to your framework's users. But the very purpose of a 28 | decorator makes it likely to impede testability of the function or 29 | class it decorates: use of a decorator often prevents the function it 30 | decorates from being called with the originally passed arguments, or a 31 | decorator may modify the return value of the decorated function. Such 32 | modifications to behavior are "hidden" in the decorator code itself. 33 | 34 | For example, let's suppose your framework defines a decorator function 35 | named ``jsonify`` which can wrap a function that returns an arbitrary 36 | Python data structure and renders it to a JSON serialization: 37 | 38 | .. code-block:: python 39 | :linenos: 40 | 41 | import json 42 | 43 | def jsonify(wrapped): 44 | def json_wrapper(request): 45 | result = wrapped(request) 46 | dumped = json.dumps(result) 47 | return dumped 48 | return json_wrapper 49 | 50 | Let's also suppose a user has written an application using your 51 | framework, and he has imported your jsonify decorator function, and 52 | uses it to decorate an application function: 53 | 54 | .. code-block:: python 55 | :linenos: 56 | 57 | from theframework import jsonify 58 | 59 | @jsonify 60 | def logged_in(request): 61 | return {'result':'Logged in'} 62 | 63 | As a result of an import of the module containing the ``logged_in`` 64 | function, a few things happen: 65 | 66 | - The user's ``logged_in`` function is replaced by the 67 | ``json_wrapper`` function. 68 | 69 | - The only reference left to the original ``logged_in`` function is 70 | inside the frame stack of the call to the ``jsonify`` decorator. 71 | 72 | This means, from the perspective of the application developer that the 73 | original ``logged_in`` function has effectively "disappeared" when it 74 | is decorated with your ``jsonify`` decorator. Without bothersome 75 | hackery, it can no longer be imported or retrieved by its original 76 | author. 77 | 78 | More importantly, it also means that if the developer wants to unit 79 | test the ``logged_in`` function, he'll need to do so only indirectly: 80 | he'll need to call the ``json_wrapper`` wrapper decorator function and 81 | test that the json returned by the function contains the expected 82 | values. This will often imply using the ``json.loads`` function to 83 | turn the result of the function *back* into a Python dictionary from 84 | the JSON representation serialized by the decorator. 85 | 86 | If the developer is a stickler for unit testing, however, he'll want 87 | to test *only* the function he has actually defined, not the wrapper 88 | code implied by the decorator your framework has provided. This is 89 | the very definition of unit testing (testing a "unit" without any 90 | other integration with other code). In this case, it is also more 91 | convenient for him to be able to test the function without the 92 | decorator: he won't need to use the ``json.loads`` function to turn 93 | the result back into a dictionary to make test assertions against. 94 | It's likely such a developer will try to find ways to get at the 95 | original function for testing purposes. 96 | 97 | To do so, he might refactor his code to look like this: 98 | 99 | .. code-block:: python 100 | :linenos: 101 | 102 | from theframework import jsonify 103 | 104 | @jsonify 105 | def logged_in(request): 106 | return _logged_in(request) 107 | 108 | def _logged_in(request): 109 | return {'result':'Logged in'} 110 | 111 | Then in test code he might import only the ``_logged_in`` function 112 | instead of the decorated ``logged_in`` function for purposes of unit 113 | testing. In such a scenario, the conscientious unit testing app 114 | developer has to define two functions for each decorated function. If 115 | you're thinking "that looks pretty tedious", you're right. 116 | 117 | To give the intrepid tester an "out", you might be tempted as a 118 | framework author to leave a reference to the original function around 119 | somewhere that the unit tester can import and use only for testing 120 | purposes. You might modify the ``jsonify`` decorator like so in order 121 | to do that: 122 | 123 | .. code-block:: python 124 | :linenos: 125 | 126 | import json 127 | def jsonify(wrapped): 128 | def json_wrapper(request): 129 | result = wrapped(request) 130 | dumped = json.dumps(result) 131 | return dumped 132 | json_wrapper.original_function = wrapped 133 | return json_wrapper 134 | 135 | The line ``json_wrapper.original_function = wrapped`` is the 136 | interesting one above. It means that the application developer has a 137 | chance to grab a reference to his original function: 138 | 139 | .. code-block:: python 140 | :linenos: 141 | 142 | from myapp import logged_in 143 | result = logged_in.original_func(None) 144 | self.assertEqual(result['result'], 'Logged in') 145 | 146 | That works. But it's just a little weird. Since the ``jsonify`` 147 | decorator function has been imported by the developer from a module in 148 | your framework, the developer probably shouldn't really need to know 149 | how it works. If he needs to read its code, or understand 150 | documentation about how the decorator functions for testing purposes, 151 | your framework *might* be less valuable to him on some level. This is 152 | arguable, really. If you use some consistent pattern like this for 153 | all your decorators, it might be a perfectly reasonable solution. 154 | 155 | However, what if the decorators offered by your framework were passive 156 | until activated explicitly? This is the promise of using Venusian 157 | within your decorator implementations. You may use Venusian within 158 | your decorators to associate a wrapped function, class, or method with 159 | a callback. Then you can return the originally wrapped function. 160 | Instead of your decorators being "active", the callback associated 161 | with the decorator is passive until a "scan" is initiated. 162 | 163 | Using Venusian 164 | -------------- 165 | 166 | The most basic use of Venusian within a decorator implementation is 167 | demonstrated below. 168 | 169 | .. code-block:: python 170 | :linenos: 171 | 172 | import venusian 173 | 174 | def jsonify(wrapped): 175 | def callback(scanner, name, ob): 176 | print('jsonified') 177 | venusian.attach(wrapped, callback) 178 | return wrapped 179 | 180 | As you can see, this decorator actually calls into venusian, but then 181 | simply returns the wrapped object. Effectively this means that this 182 | decorator is "passive" when the module is imported. 183 | 184 | Usage of the decorator: 185 | 186 | .. code-block:: python 187 | :linenos: 188 | 189 | from theframework import jsonify 190 | 191 | @jsonify 192 | def logged_in(request): 193 | return {'result':'Logged in'} 194 | 195 | Note that when we import and use the function, the fact that it is 196 | decorated with the ``jsonify`` decorator is immaterial. Our decorator 197 | doesn't actually change its behavior. 198 | 199 | .. code-block:: python 200 | :linenos: 201 | 202 | >>> from theapp import logged_in 203 | >>> logged_in(None) 204 | {'result':'Logged in'} 205 | >>> 206 | 207 | This is the intended result. During unit testing, the original 208 | function can be imported and tested despite the fact that it has been 209 | wrapped with a decorator. 210 | 211 | However, we can cause something to happen when we invoke a :term:`scan`. 212 | 213 | .. code-block:: python 214 | :linenos: 215 | 216 | import venusian 217 | import theapp 218 | 219 | scanner = venusian.Scanner() 220 | scanner.scan(theapp) 221 | 222 | Above we've imported a module named ``theapp``. The ``logged_in`` 223 | function which we decorated with our ``jsonify`` decorator lives in 224 | this module. We've also imported the :mod:`venusian` module, and 225 | we've created an instance of the :class:`venusian.Scanner` class. 226 | Once we've created the instance of :class:`venusian.Scanner`, we 227 | invoke its :meth:`venusian.Scanner.scan` method, passing the 228 | ``theapp`` module as an argument to the method. 229 | 230 | Here's what happens as a result of invoking the 231 | :meth:`venusian.Scanner.scan` method: 232 | 233 | #. Every object defined at module scope within the ``theapp`` Python 234 | module will be inspected to see if it has had a Venusian callback 235 | attached to it. 236 | 237 | #. For every object that *does* have a Venusian callback attached to 238 | it, the callback is called. 239 | 240 | We could have also passed the ``scan`` method a Python *package* 241 | instead of a module. This would recursively import each module in the 242 | package (as well as any modules in subpackages), looking for 243 | callbacks. 244 | 245 | .. note:: During scan, the only Python files that are processed are 246 | Python *source* (``.py``) files. Compiled Python files (``.pyc``, 247 | ``.pyo`` files) without a corresponding source file are ignored. 248 | 249 | In our case, because the callback we defined within the ``jsonify`` 250 | decorator function prints ``jsonified`` when it is invoked, which 251 | means that the word ``jsonified`` will be printed to the console when 252 | we cause :meth:`venusian.Scanner.scan` to be invoked. How is this 253 | useful? It's not! At least not yet. Let's create a more realistic 254 | example. 255 | 256 | Let's change our ``jsonify`` decorator to perform a more useful action 257 | when a scan is invoked by changing the body of its callback. 258 | 259 | .. code-block:: python 260 | :linenos: 261 | 262 | import venusian 263 | 264 | def jsonify(wrapped): 265 | def callback(scanner, name, ob): 266 | def jsonified(request): 267 | result = wrapped(request) 268 | return json.dumps(result) 269 | scanner.registry.add(name, jsonified) 270 | venusian.attach(wrapped, callback) 271 | return wrapped 272 | 273 | Now if we invoke a scan, we'll get an error: 274 | 275 | .. code-block:: python 276 | :linenos: 277 | 278 | import venusian 279 | import theapp 280 | 281 | scanner = venusian.Scanner() 282 | scanner.scan(theapp) 283 | 284 | AttributeError: Scanner has no attribute 'registry'. 285 | 286 | The :class:`venusian.Scanner` class constructor accepts any key-value 287 | pairs; for each key/value pair passed to the scanner's constructor, an 288 | attribute named after the key which points at the value is added to 289 | the scanner instance. So when you do: 290 | 291 | .. code-block:: python 292 | :linenos: 293 | 294 | import venusian 295 | scanner = venusian.Scanner(a=1) 296 | 297 | Thereafter, ``scanner.a`` will equal the integer 1. 298 | 299 | Any number of key-value pairs can be passed to a scanner. The purpose 300 | of being able to pass arbitrary key/value pairs to a scanner is to 301 | allow cooperating decorator callbacks to access these values: each 302 | callback is passed the ``scanner`` constructed when a scan is invoked. 303 | 304 | Let's fix our example by creating an object named ``registry`` that 305 | we'll pass to our scanner's constructor: 306 | 307 | .. code-block:: python 308 | :linenos: 309 | 310 | import venusian 311 | import theapp 312 | 313 | class Registry(object): 314 | def __init__(self): 315 | self.registered = [] 316 | 317 | def add(self, name, ob): 318 | self.registered.append((name, ob)) 319 | 320 | registry = Registry() 321 | scanner = venusian.Scanner(registry=registry) 322 | scanner.scan(theapp) 323 | 324 | At this point, we have a system which, during a scan, for each object 325 | that is wrapped with a Venusian-aware decorator, a tuple will be 326 | appended to the ``registered`` attribute of a ``Registry`` object. 327 | The first element of the tuple will be the decorated object's name, 328 | the second element of the tuple will be a "truly" decorated object. 329 | In our case, this will be a jsonify-decorated callable. 330 | 331 | Our framework can then use the information in the registry to decide 332 | which view function to call when a request comes in. 333 | 334 | Venusian callbacks must accept three arguments: 335 | 336 | ``scanner`` 337 | 338 | This will be the instance of the scanner that has had its ``scan`` 339 | method invoked. 340 | 341 | ``name`` 342 | 343 | This is the module-level name of the object being decorated. 344 | 345 | ``ob`` 346 | 347 | This is the object being decorated if it's a function or an 348 | instance; if the object being decorated is a *method*, however, this 349 | value will be the *class*. 350 | 351 | If you consider that the decorator and the scanner can cooperate, and 352 | can perform arbitrary actions together, you can probably imagine a 353 | system where a registry will be populated that informs some 354 | higher-level system (such as a web framework) about the available 355 | decorated functions. 356 | 357 | Scan Categories 358 | --------------- 359 | 360 | Because an application may use two separate Venusian-using frameworks, 361 | Venusian allows for the concept of "scan categories". 362 | 363 | The :func:`venusian.attach` function accepts an additional argument 364 | named ``category``. 365 | 366 | For example: 367 | 368 | .. code-block:: python 369 | :linenos: 370 | 371 | import venusian 372 | 373 | def jsonify(wrapped): 374 | def callback(scanner, name, ob): 375 | def jsonified(request): 376 | result = wrapped(request) 377 | return json.dumps(result) 378 | scanner.registry.add(name, jsonified) 379 | venusian.attach(wrapped, callback, category='myframework') 380 | return wrapped 381 | 382 | Note the ``category='myframework'`` argument in the call to 383 | :func:`venusian.attach`. This tells Venusian to attach the callback 384 | to the wrapped object under the specific scan category 385 | ``myframework``. The default scan category is ``None``. 386 | 387 | Later, during :meth:`venusian.Scanner.scan`, a user can choose to 388 | activate all the decorators associated only with a particular set of 389 | scan categories by passing a ``categories`` argument. For example: 390 | 391 | .. code-block:: python 392 | :linenos: 393 | 394 | import venusian 395 | scanner = venusian.Scanner(a=1) 396 | scanner.scan(theapp, categories=('myframework',)) 397 | 398 | The default ``categories`` argument is ``None``, which means activate 399 | all Venusian callbacks during a scan regardless of their category. 400 | 401 | ``onerror`` Scan Callback 402 | ------------------------- 403 | 404 | .. versionadded:: 1.0 405 | 406 | By default, when Venusian scans a package, it will propagate all exceptions 407 | raised while attempting to import code. You can use an ``onerror`` callback 408 | argument to :meth:`venusian.Scanner.scan` to change this behavior. 409 | 410 | The ``onerror`` argument should either be ``None`` or a callback function 411 | which behaves the same way as the ``onerror`` callback function described in 412 | http://docs.python.org/library/pkgutil.html#pkgutil.walk_packages . 413 | 414 | Here's an example ``onerror`` callback that ignores all :exc:`ImportError` 415 | exceptions: 416 | 417 | .. code-block:: python 418 | :linenos: 419 | 420 | import sys 421 | def onerror(name): 422 | if not issubclass(sys.exc_info()[0], ImportError): 423 | raise # reraise the last exception 424 | 425 | Here's how we'd use this callback: 426 | 427 | .. code-block:: python 428 | :linenos: 429 | 430 | import venusian 431 | scanner = venusian.Scanner() 432 | scanner.scan(theapp, onerror=onerror) 433 | 434 | The ``onerror`` callback should execute ``raise`` at some point if any 435 | exception is to be propagated, otherwise it can simply return. The ``name`` 436 | passed to ``onerror`` is the module or package dotted name that could not be 437 | imported due to an exception. 438 | 439 | 440 | ``ignore`` Scan Argument 441 | ------------------------ 442 | 443 | .. versionadded:: 1.0a3 444 | 445 | The ``ignore`` to ``scan`` allows you to ignore certain modules, packages, or 446 | global objects during a scan. It should be a sequence containing strings 447 | and/or callables that will be used to match against the full dotted name of 448 | each object encountered during the scanning process. If the ignore value you 449 | provide matches a package name, global objects contained by that package as 450 | well any submodules and subpackages of the package (and any global objects 451 | contained by them) will be ignored. If the ignore value you provide matches 452 | a module name, any global objects in that module will be ignored. If the 453 | ignore value you provide matches a global object that lives in a package or 454 | module, only that particular global object will be ignored. 455 | 456 | The sequence can contain any of these three types of objects: 457 | 458 | - A string representing a full dotted name. To name an object by dotted 459 | name, use a string representing the full dotted name. For example, if you 460 | want to ignore the ``my.package`` package and any of its subobjects during 461 | the scan, pass ``ignore=['my.package']``. If the string matches a global 462 | object (e.g. ``ignore=['my.package.MyClass']``), only that object will be 463 | ignored and the rest of the objects in the module or package that contains 464 | the object will be processed. 465 | 466 | - A string representing a relative dotted name. To name an object relative 467 | to the ``package`` passed to this method, use a string beginning with a 468 | dot. For example, if the ``package`` you've passed is imported as 469 | ``my.package``, and you pass ``ignore=['.mymodule']``, the 470 | ``my.package.mymodule`` module and any of its subobjects will be omitted 471 | during scan processing. If the string matches a global object 472 | (e.g. ``ignore=['my.package.MyClass']``), only that object will be ignored 473 | and the rest of the objects in the module or package that contains the 474 | object will be processed. 475 | 476 | - A callable that accepts a full dotted name string of an object as its 477 | single positional argument and returns ``True`` or ``False``. If the 478 | callable returns ``True`` or anything else truthy, the module, package, or 479 | global object is ignored, if it returns ``False`` or anything else falsy, 480 | it is not ignored. If the callable matches a package name, the package as 481 | well as any of that package's submodules and subpackages (recursively) will 482 | be ignored. If the callable matches a module name, that module and any of 483 | its contained global objects will be ignored. If the callable matches a 484 | global object name, only that object name will be ignored. For example, if 485 | you want to skip all packages, modules, and global objects that have a full 486 | dotted name that ends with the word "tests", you can use 487 | ``ignore=[re.compile('tests$').search]``. 488 | 489 | Here's an example of how we might use the ``ignore`` argument to ``scan`` to 490 | ignore an entire package (and any of its submodules and subpackages) by 491 | absolute dotted name: 492 | 493 | .. code-block:: python 494 | :linenos: 495 | 496 | import venusian 497 | scanner = venusian.Scanner() 498 | scanner.scan(theapp, ignore=['theapp.package']) 499 | 500 | Here's an example of how we might use the ``ignore`` argument to ``scan`` to 501 | ignore an entire package (and any of its submodules and subpackages) by 502 | relative dotted name (``theapp.package``): 503 | 504 | .. code-block:: python 505 | :linenos: 506 | 507 | import venusian 508 | scanner = venusian.Scanner() 509 | scanner.scan(theapp, ignore=['.package']) 510 | 511 | Here's an example of how we might use the ``ignore`` argument to ``scan`` to 512 | ignore a particular class object: 513 | 514 | .. code-block:: python 515 | :linenos: 516 | 517 | import venusian 518 | scanner = venusian.Scanner() 519 | scanner.scan(theapp, ignore=['theapp.package.MyClass']) 520 | 521 | Here's an example of how we might use the ``ignore`` argument to ``scan`` to 522 | ignore any module, package, or global object that has a name which ends 523 | with the string ``tests``: 524 | 525 | .. code-block:: python 526 | :linenos: 527 | 528 | import re 529 | import venusian 530 | scanner = venusian.Scanner() 531 | scanner.scan(theapp, ignore=[re.compile('tests$').search]) 532 | 533 | You can mix and match the three types in the list. For example, 534 | ``scanner.scan(my, ignore=['my.package', '.someothermodule', 535 | re.compile('tests$').search])`` would cause ``my.package`` (and all its 536 | submodules and subobjects) to be ignored, ``my.someothermodule`` to be 537 | ignored, and any modules, packages, or global objects found during the scan 538 | that have a full dotted path that ends with the word ``tests`` to be ignored 539 | beneath the ``my`` package. 540 | 541 | Packages and modules matched by any ignore in the list will not be imported, 542 | and their top-level code will not be run as a result. 543 | 544 | 545 | Limitations and Audience 546 | ------------------------ 547 | 548 | Venusian is not really a tool that is maximally useful to an 549 | application developer. It would be a little silly to use it every 550 | time you needed a decorator. Instead, it's most useful for framework 551 | authors, in order to be able to say to their users "the frobozz 552 | decorator doesn't change the output of your function at all" in 553 | documentation. This is a lot easier than telling them how to test 554 | methods/functions/classes decorated by each individual decorator 555 | offered by your frameworks. 556 | 557 | API Documentation / Glossary 558 | ---------------------------- 559 | 560 | .. toctree:: 561 | :maxdepth: 2 562 | 563 | api.rst 564 | glossary.rst 565 | 566 | Indices and tables 567 | ------------------ 568 | 569 | * :ref:`glossary` 570 | * :ref:`modindex` 571 | * :ref:`search` 572 | -------------------------------------------------------------------------------- /src/venusian/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from inspect import getmembers, getmro, isclass 3 | from pkgutil import iter_modules 4 | 5 | from venusian.advice import getFrameInfo 6 | from venusian.compat import compat_find_loader 7 | 8 | ATTACH_ATTR = "__venusian_callbacks__" 9 | LIFTONLY_ATTR = "__venusian_liftonly_callbacks__" 10 | 11 | 12 | class Scanner(object): 13 | def __init__(self, **kw): 14 | self.__dict__.update(kw) 15 | 16 | def scan(self, package, categories=None, onerror=None, ignore=None): 17 | """Scan a Python package and any of its subpackages. All 18 | top-level objects will be considered; those marked with 19 | venusian callback attributes related to ``category`` will be 20 | processed. 21 | 22 | The ``package`` argument should be a reference to a Python 23 | package or module object. 24 | 25 | The ``categories`` argument should be sequence of Venusian 26 | callback categories (each category usually a string) or the 27 | special value ``None`` which means all Venusian callback 28 | categories. The default is ``None``. 29 | 30 | The ``onerror`` argument should either be ``None`` or a callback 31 | function which behaves the same way as the ``onerror`` callback 32 | function described in 33 | http://docs.python.org/library/pkgutil.html#pkgutil.walk_packages . 34 | By default, during a scan, Venusian will propagate all errors that 35 | happen during its code importing process, including 36 | :exc:`ImportError`. If you use a custom ``onerror`` callback, you 37 | can change this behavior. 38 | 39 | Here's an example ``onerror`` callback that ignores 40 | :exc:`ImportError`:: 41 | 42 | import sys 43 | def onerror(name): 44 | if not issubclass(sys.exc_info()[0], ImportError): 45 | raise # reraise the last exception 46 | 47 | The ``name`` passed to ``onerror`` is the module or package dotted 48 | name that could not be imported due to an exception. 49 | 50 | .. versionadded:: 1.0 51 | the ``onerror`` callback 52 | 53 | The ``ignore`` argument allows you to ignore certain modules, 54 | packages, or global objects during a scan. It should be a sequence 55 | containing strings and/or callables that will be used to match 56 | against the full dotted name of each object encountered during a 57 | scan. The sequence can contain any of these three types of objects: 58 | 59 | - A string representing a full dotted name. To name an object by 60 | dotted name, use a string representing the full dotted name. For 61 | example, if you want to ignore the ``my.package`` package *and any 62 | of its subobjects or subpackages* during the scan, pass 63 | ``ignore=['my.package']``. 64 | 65 | - A string representing a relative dotted name. To name an object 66 | relative to the ``package`` passed to this method, use a string 67 | beginning with a dot. For example, if the ``package`` you've 68 | passed is imported as ``my.package``, and you pass 69 | ``ignore=['.mymodule']``, the ``my.package.mymodule`` mymodule *and 70 | any of its subobjects or subpackages* will be omitted during scan 71 | processing. 72 | 73 | - A callable that accepts a full dotted name string of an object as 74 | its single positional argument and returns ``True`` or ``False``. 75 | For example, if you want to skip all packages, modules, and global 76 | objects with a full dotted path that ends with the word "tests", you 77 | can use ``ignore=[re.compile('tests$').search]``. If the callable 78 | returns ``True`` (or anything else truthy), the object is ignored, 79 | if it returns ``False`` (or anything else falsy) the object is not 80 | ignored. *Note that unlike string matches, ignores that use a 81 | callable don't cause submodules and subobjects of a module or 82 | package represented by a dotted name to also be ignored, they match 83 | individual objects found during a scan, including packages, 84 | modules, and global objects*. 85 | 86 | You can mix and match the three types of strings in the list. For 87 | example, if the package being scanned is ``my``, 88 | ``ignore=['my.package', '.someothermodule', 89 | re.compile('tests$').search]`` would cause ``my.package`` (and all 90 | its submodules and subobjects) to be ignored, ``my.someothermodule`` 91 | to be ignored, and any modules, packages, or global objects found 92 | during the scan that have a full dotted name that ends with the word 93 | ``tests`` to be ignored. 94 | 95 | Note that packages and modules matched by any ignore in the list will 96 | not be imported, and their top-level code will not be run as a result. 97 | 98 | A string or callable alone can also be passed as ``ignore`` without a 99 | surrounding list. 100 | 101 | .. versionadded:: 1.0a3 102 | the ``ignore`` argument 103 | """ 104 | 105 | pkg_name = package.__name__ 106 | 107 | if ignore is not None and ( 108 | isinstance(ignore, str) or not hasattr(ignore, "__iter__") 109 | ): 110 | ignore = [ignore] 111 | elif ignore is None: 112 | ignore = [] 113 | 114 | # non-leading-dotted name absolute object name 115 | str_ignores = [ign for ign in ignore if isinstance(ign, str)] 116 | # leading dotted name relative to scanned package 117 | rel_ignores = [ign for ign in str_ignores if ign.startswith(".")] 118 | # non-leading dotted names 119 | abs_ignores = [ign for ign in str_ignores if not ign.startswith(".")] 120 | # functions, e.g. re.compile('pattern').search 121 | callable_ignores = [ign for ign in ignore if callable(ign)] 122 | 123 | def _ignore(fullname): 124 | for ign in rel_ignores: 125 | if fullname.startswith(pkg_name + ign): 126 | return True 127 | for ign in abs_ignores: 128 | # non-leading-dotted name absolute object name 129 | if fullname.startswith(ign): 130 | return True 131 | for ign in callable_ignores: 132 | if ign(fullname): 133 | return True 134 | return False 135 | 136 | def invoke(mod_name, name, ob): 137 | fullname = mod_name + "." + name 138 | 139 | if _ignore(fullname): 140 | return 141 | 142 | category_keys = categories 143 | try: 144 | # Some metaclasses do insane things when asked for an 145 | # ``ATTACH_ATTR``, like not raising an AttributeError but 146 | # some other arbitary exception. Some even shittier 147 | # introspected code lets us access ``ATTACH_ATTR`` far but 148 | # barfs on a second attribute access for ``attached_to`` 149 | # (still not raising an AttributeError, but some other 150 | # arbitrary exception). Finally, the shittiest code of all 151 | # allows the attribute access of the ``ATTACH_ATTR`` *and* 152 | # ``attached_to``, (say, both ``ob.__getattr__`` and 153 | # ``attached_categories.__getattr__`` returning a proxy for 154 | # any attribute access), which either a) isn't callable or b) 155 | # is callable, but, when called, shits its pants in an 156 | # potentially arbitrary way (although for b, only TypeError 157 | # has been seen in the wild, from PyMongo). Thus the 158 | # catchall except: return here, which in any other case would 159 | # be high treason. 160 | attached_categories = getattr(ob, ATTACH_ATTR) 161 | if not attached_categories.attached_to(mod_name, name, ob): 162 | return 163 | except: 164 | return 165 | if category_keys is None: 166 | category_keys = list(attached_categories.keys()) 167 | try: 168 | # When metaclasses return proxies for any attribute access 169 | # the list may contain keys of different types which might 170 | # not be sortable. In that case we can just return, 171 | # because we're not dealing with a proper venusian 172 | # callback. 173 | category_keys.sort() 174 | except TypeError: # pragma: no cover 175 | return 176 | for category in category_keys: 177 | callbacks = attached_categories.get(category, []) 178 | try: 179 | # Metaclasses might trick us by reaching this far and then 180 | # fail with too little values to unpack. 181 | for callback, cb_mod_name, liftid, scope in callbacks: 182 | if cb_mod_name != mod_name: 183 | # avoid processing objects that were imported into 184 | # this module but were not actually defined there 185 | continue 186 | callback(self, name, ob) 187 | except ValueError: # pragma: nocover 188 | continue 189 | 190 | for name, ob in getmembers(package): 191 | # whether it's a module or a package, we need to scan its 192 | # members; walk_packages only iterates over submodules and 193 | # subpackages 194 | invoke(pkg_name, name, ob) 195 | 196 | if hasattr(package, "__path__"): # package, not module 197 | results = walk_packages( 198 | package.__path__, 199 | package.__name__ + ".", 200 | onerror=onerror, 201 | ignore=_ignore, 202 | ) 203 | 204 | for importer, modname, ispkg in results: 205 | loader = compat_find_loader(importer, modname) 206 | if loader is not None: # happens on pypy with orphaned pyc 207 | try: 208 | get_filename = getattr(loader, "get_filename", None) 209 | if get_filename is None: # pragma: nocover 210 | get_filename = loader._get_filename 211 | try: 212 | fn = get_filename(modname) 213 | except TypeError: # pragma: nocover 214 | fn = get_filename() 215 | 216 | # NB: use __import__(modname) rather than 217 | # loader.load_module(modname) to prevent 218 | # inappropriate double-execution of module code 219 | try: 220 | __import__(modname) 221 | except Exception: 222 | if onerror is not None: 223 | onerror(modname) 224 | else: 225 | raise 226 | module = sys.modules.get(modname) 227 | if module is not None: 228 | for name, ob in getmembers(module, None): 229 | invoke(modname, name, ob) 230 | finally: 231 | if hasattr(loader, "file") and hasattr( 232 | loader.file, "close" 233 | ): # pragma: nocover 234 | loader.file.close() 235 | 236 | 237 | class AttachInfo(object): 238 | """ 239 | An instance of this class is returned by the 240 | :func:`venusian.attach` function. It has the following 241 | attributes: 242 | 243 | ``scope`` 244 | 245 | One of ``exec``, ``module``, ``class``, ``function call`` or 246 | ``unknown`` (each a string). This is the scope detected while 247 | executing the decorator which runs the attach function. 248 | 249 | ``module`` 250 | 251 | The module in which the decorated function was defined. 252 | 253 | ``locals`` 254 | 255 | A dictionary containing decorator frame's f_locals. 256 | 257 | ``globals`` 258 | 259 | A dictionary containing decorator frame's f_globals. 260 | 261 | ``category`` 262 | 263 | The ``category`` argument passed to ``attach`` (or ``None``, the 264 | default). 265 | 266 | ``codeinfo`` 267 | 268 | A tuple in the form ``(filename, lineno, function, sourceline)`` 269 | representing the context of the venusian decorator used. Eg. 270 | ``('/home/chrism/projects/venusian/tests/test_advice.py', 81, 271 | 'testCallInfo', 'add_handler(foo, bar)')`` 272 | 273 | """ 274 | 275 | def __init__(self, **kw): 276 | self.__dict__.update(kw) 277 | 278 | 279 | class Categories(dict): 280 | def __init__(self, attached_to): 281 | super(dict, self).__init__() 282 | if isinstance(attached_to, tuple): 283 | self.attached_id = attached_to 284 | else: 285 | self.attached_id = id(attached_to) 286 | self.lifted = False 287 | 288 | def attached_to(self, mod_name, name, obj): 289 | if isinstance(self.attached_id, int): 290 | return self.attached_id == id(obj) 291 | return self.attached_id == (mod_name, name) 292 | 293 | 294 | def attach(wrapped, callback, category=None, depth=1, name=None): 295 | """Attach a callback to the wrapped object. It will be found 296 | later during a scan. This function returns an instance of the 297 | :class:`venusian.AttachInfo` class. 298 | 299 | ``category`` should be ``None`` or a string representing a decorator 300 | category name. 301 | 302 | ``name`` should be ``None`` or a string representing a subcategory within 303 | the category. This will be used by the ``lift`` class decorator to 304 | determine if decorations of a method should be inherited or overridden. 305 | """ 306 | 307 | frame = sys._getframe(depth + 1) 308 | scope, module, f_locals, f_globals, codeinfo = getFrameInfo(frame) 309 | module_name = getattr(module, "__name__", None) 310 | wrapped_name = getattr(wrapped, "__name__", None) 311 | class_name = codeinfo[2] 312 | 313 | liftid = "%s %s" % (wrapped_name, name) 314 | 315 | if scope == "class": 316 | # we're in the midst of a class statement 317 | categories = f_locals.get(ATTACH_ATTR, None) 318 | if categories is None or not categories.attached_to( 319 | module_name, class_name, None 320 | ): 321 | categories = Categories((module_name, class_name)) 322 | f_locals[ATTACH_ATTR] = categories 323 | callbacks = categories.setdefault(category, []) 324 | else: 325 | categories = getattr(wrapped, ATTACH_ATTR, None) 326 | if categories is None or not categories.attached_to( 327 | module_name, wrapped_name, wrapped 328 | ): 329 | # if there aren't any attached categories, or we've retrieved 330 | # some by inheritance, we need to create new ones 331 | categories = Categories(wrapped) 332 | setattr(wrapped, ATTACH_ATTR, categories) 333 | callbacks = categories.setdefault(category, []) 334 | 335 | callbacks.append((callback, module_name, liftid, scope)) 336 | 337 | return AttachInfo( 338 | scope=scope, 339 | module=module, 340 | locals=f_locals, 341 | globals=f_globals, 342 | category=category, 343 | codeinfo=codeinfo, 344 | ) 345 | 346 | 347 | def walk_packages(path=None, prefix="", onerror=None, ignore=None): 348 | """Yields (module_loader, name, ispkg) for all modules recursively 349 | on path, or, if path is None, all accessible modules. 350 | 351 | 'path' should be either None or a list of paths to look for 352 | modules in. 353 | 354 | 'prefix' is a string to output on the front of every module name 355 | on output. 356 | 357 | Note that this function must import all *packages* (NOT all 358 | modules!) on the given path, in order to access the __path__ 359 | attribute to find submodules. 360 | 361 | 'onerror' is a function which gets called with one argument (the name of 362 | the package which was being imported) if any exception occurs while 363 | trying to import a package. If no onerror function is supplied, any 364 | exception is exceptions propagated, terminating the search. 365 | 366 | 'ignore' is a function fed a fullly dotted name; if it returns True, the 367 | object is skipped and not returned in results (and if it's a package it's 368 | not imported). 369 | 370 | Examples: 371 | 372 | # list all modules python can access 373 | walk_packages() 374 | 375 | # list all submodules of ctypes 376 | walk_packages(ctypes.__path__, ctypes.__name__+'.') 377 | 378 | # NB: we can't just use pkgutils.walk_packages because we need to ignore 379 | # things 380 | """ 381 | 382 | def seen(p, m={}): 383 | if p in m: # pragma: no cover 384 | return True 385 | m[p] = True 386 | 387 | # iter_modules is nonrecursive 388 | for importer, name, ispkg in iter_modules(path, prefix): 389 | if ignore is not None and ignore(name): 390 | # if name is a package, ignoring here will cause 391 | # all subpackages and submodules to be ignored too 392 | continue 393 | 394 | # do any onerror handling before yielding 395 | 396 | if ispkg: 397 | try: 398 | __import__(name) 399 | except Exception: 400 | if onerror is not None: 401 | onerror(name) 402 | else: 403 | raise 404 | else: 405 | yield importer, name, ispkg 406 | path = getattr(sys.modules[name], "__path__", None) or [] 407 | 408 | # don't traverse path items we've seen before 409 | path = [p for p in path if not seen(p)] 410 | 411 | for item in walk_packages(path, name + ".", onerror, ignore): 412 | yield item 413 | else: 414 | yield importer, name, ispkg 415 | 416 | 417 | class lift(object): 418 | """ 419 | A class decorator which 'lifts' superclass venusian configuration 420 | decorations into subclasses. For example:: 421 | 422 | from venusian import lift 423 | from somepackage import venusian_decorator 424 | 425 | class Super(object): 426 | @venusian_decorator() 427 | def boo(self): pass 428 | 429 | @venusian_decorator() 430 | def hiss(self): pass 431 | 432 | @venusian_decorator() 433 | def jump(self): pass 434 | 435 | @lift() 436 | class Sub(Super): 437 | def boo(self): pass 438 | 439 | def hiss(self): pass 440 | 441 | @venusian_decorator() 442 | def smack(self): pass 443 | 444 | The above configuration will cause the callbacks of seven venusian 445 | decorators. The ones attached to Super.boo, Super.hiss, and Super.jump 446 | *plus* ones attached to Sub.boo, Sub.hiss, Sub.hump and Sub.smack. 447 | 448 | If a subclass overrides a decorator on a method, its superclass decorators 449 | will be ignored for the subclass. That means that in this configuration:: 450 | 451 | from venusian import lift 452 | from somepackage import venusian_decorator 453 | 454 | class Super(object): 455 | @venusian_decorator() 456 | def boo(self): pass 457 | 458 | @venusian_decorator() 459 | def hiss(self): pass 460 | 461 | @lift() 462 | class Sub(Super): 463 | 464 | def boo(self): pass 465 | 466 | @venusian_decorator() 467 | def hiss(self): pass 468 | 469 | Only four, not five decorator callbacks will be run: the ones attached to 470 | Super.boo and Super.hiss, the inherited one of Sub.boo and the 471 | non-inherited one of Sub.hiss. The inherited decorator on Super.hiss will 472 | be ignored for the subclass. 473 | 474 | The ``lift`` decorator takes a single argument named 'categories'. If 475 | supplied, it should be a tuple of category names. Only decorators 476 | in this category will be lifted if it is suppled. 477 | 478 | """ 479 | 480 | def __init__(self, categories=None): 481 | self.categories = categories 482 | 483 | def __call__(self, wrapped): 484 | if not isclass(wrapped): 485 | raise RuntimeError( 486 | '"lift" only works as a class decorator; you tried to use ' 487 | "it against %r" % wrapped 488 | ) 489 | frame = sys._getframe(1) 490 | scope, module, f_locals, f_globals, codeinfo = getFrameInfo(frame) 491 | module_name = getattr(module, "__name__", None) 492 | newcategories = Categories(wrapped) 493 | newcategories.lifted = True 494 | for cls in getmro(wrapped): 495 | attached_categories = cls.__dict__.get(ATTACH_ATTR, None) 496 | if attached_categories is None: 497 | attached_categories = cls.__dict__.get(LIFTONLY_ATTR, None) 498 | if attached_categories is not None: 499 | for cname, category in attached_categories.items(): 500 | if cls is not wrapped: 501 | if self.categories and not cname in self.categories: 502 | continue 503 | callbacks = newcategories.get(cname, []) 504 | newcallbacks = [] 505 | for cb, _, liftid, cscope in category: 506 | append = True 507 | toappend = (cb, module_name, liftid, cscope) 508 | if cscope == "class": 509 | for ncb, _, nliftid, nscope in callbacks: 510 | if nscope == "class" and liftid == nliftid: 511 | append = False 512 | if append: 513 | newcallbacks.append(toappend) 514 | newcategory = list(callbacks) + newcallbacks 515 | newcategories[cname] = newcategory 516 | if attached_categories.lifted: 517 | break 518 | if newcategories: # if it has any keys 519 | setattr(wrapped, ATTACH_ATTR, newcategories) 520 | return wrapped 521 | 522 | 523 | class onlyliftedfrom(object): 524 | """ 525 | A class decorator which marks a class as 'only lifted from'. Decorations 526 | made on methods of the class won't have their callbacks called directly, 527 | but classes which inherit from only-lifted-from classes which also use the 528 | ``lift`` class decorator will use the superclass decoration callbacks. 529 | 530 | For example:: 531 | 532 | from venusian import lift, onlyliftedfrom 533 | from somepackage import venusian_decorator 534 | 535 | @onlyliftedfrom() 536 | class Super(object): 537 | @venusian_decorator() 538 | def boo(self): pass 539 | 540 | @venusian_decorator() 541 | def hiss(self): pass 542 | 543 | @lift() 544 | class Sub(Super): 545 | 546 | def boo(self): pass 547 | 548 | def hiss(self): pass 549 | 550 | Only two decorator callbacks will be run: the ones attached to Sub.boo and 551 | Sub.hiss. The inherited decorators on Super.boo and Super.hiss will be 552 | not be registered. 553 | """ 554 | 555 | def __call__(self, wrapped): 556 | if not isclass(wrapped): 557 | raise RuntimeError( 558 | '"onlyliftedfrom" only works as a class decorator; you tried ' 559 | "to use it against %r" % wrapped 560 | ) 561 | cats = getattr(wrapped, ATTACH_ATTR, None) 562 | class_name = wrapped.__name__ 563 | module_name = wrapped.__module__ 564 | key = (module_name, class_name, wrapped) 565 | if cats is None or not cats.attached_to(*key): 566 | # we either have no categories or our categories are defined 567 | # in a superclass 568 | return 569 | delattr(wrapped, ATTACH_ATTR) 570 | setattr(wrapped, LIFTONLY_ATTR, cats) 571 | return wrapped 572 | -------------------------------------------------------------------------------- /tests/test_venusian.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import os 3 | import re 4 | import sys 5 | import unittest 6 | 7 | 8 | @contextlib.contextmanager 9 | def with_entry_in_sys_path(entry): 10 | """Context manager that temporarily puts an entry at head of sys.path""" 11 | sys.path.insert(0, entry) 12 | yield 13 | sys.path.remove(entry) 14 | 15 | 16 | def zip_file_in_sys_path(): 17 | """Context manager that puts zipped.zip at head of sys.path""" 18 | zip_pkg_path = os.path.join(os.path.dirname(__file__), "fixtures", "zipped.zip") 19 | return with_entry_in_sys_path(zip_pkg_path) 20 | 21 | 22 | class _Test(object): 23 | def __init__(self): 24 | self.registrations = [] 25 | 26 | def __call__(self, **kw): 27 | self.registrations.append(kw) 28 | 29 | 30 | class TestScanner(unittest.TestCase): 31 | def _makeOne(self, **kw): 32 | from venusian import Scanner 33 | 34 | return Scanner(**kw) 35 | 36 | def test_package(self): 37 | from tests.fixtures import one 38 | 39 | test = _Test() 40 | scanner = self._makeOne(test=test) 41 | scanner.scan(one) 42 | self.assertEqual(len(test.registrations), 6) 43 | test.registrations.sort(key=lambda x: (x["name"], x["ob"].__module__)) 44 | from tests.fixtures.one.module import Class as Class1 45 | from tests.fixtures.one.module import function as func1 46 | from tests.fixtures.one.module import inst as inst1 47 | from tests.fixtures.one.module2 import Class as Class2 48 | from tests.fixtures.one.module2 import function as func2 49 | from tests.fixtures.one.module2 import inst as inst2 50 | 51 | self.assertEqual(test.registrations[0]["name"], "Class") 52 | self.assertEqual(test.registrations[0]["ob"], Class1) 53 | self.assertEqual(test.registrations[0]["method"], True) 54 | 55 | self.assertEqual(test.registrations[1]["name"], "Class") 56 | self.assertEqual(test.registrations[1]["ob"], Class2) 57 | self.assertEqual(test.registrations[1]["method"], True) 58 | 59 | self.assertEqual(test.registrations[2]["name"], "function") 60 | self.assertEqual(test.registrations[2]["ob"], func1) 61 | self.assertEqual(test.registrations[2]["function"], True) 62 | 63 | self.assertEqual(test.registrations[3]["name"], "function") 64 | self.assertEqual(test.registrations[3]["ob"], func2) 65 | self.assertEqual(test.registrations[3]["function"], True) 66 | 67 | self.assertEqual(test.registrations[4]["name"], "inst") 68 | self.assertEqual(test.registrations[4]["ob"], inst1) 69 | self.assertEqual(test.registrations[4]["instance"], True) 70 | 71 | self.assertEqual(test.registrations[5]["name"], "inst") 72 | self.assertEqual(test.registrations[5]["ob"], inst2) 73 | self.assertEqual(test.registrations[5]["instance"], True) 74 | 75 | def test_module_in_zip(self): 76 | with zip_file_in_sys_path(): 77 | import moduleinzip 78 | test = _Test() 79 | scanner = self._makeOne(test=test) 80 | scanner.scan(moduleinzip) 81 | self.assertEqual(len(test.registrations), 3) 82 | test.registrations.sort(key=lambda x: (x["name"], x["ob"].__module__)) 83 | from tests.fixtures.one.module import Class as Class1 84 | from tests.fixtures.one.module import function as func1 85 | from tests.fixtures.one.module import inst as inst1 86 | from tests.fixtures.one.module2 import Class as Class2 87 | from tests.fixtures.one.module2 import function as func2 88 | from tests.fixtures.one.module2 import inst as inst2 89 | 90 | self.assertEqual(test.registrations[0]["name"], "Class") 91 | self.assertEqual(test.registrations[0]["ob"], moduleinzip.Class) 92 | self.assertEqual(test.registrations[0]["method"], True) 93 | 94 | self.assertEqual(test.registrations[1]["name"], "function") 95 | self.assertEqual(test.registrations[1]["ob"], moduleinzip.function) 96 | self.assertEqual(test.registrations[1]["function"], True) 97 | 98 | self.assertEqual(test.registrations[2]["name"], "inst") 99 | self.assertEqual(test.registrations[2]["ob"], moduleinzip.inst) 100 | self.assertEqual(test.registrations[2]["instance"], True) 101 | 102 | def test_package_in_zip(self): 103 | with zip_file_in_sys_path(): 104 | import packageinzip 105 | test = _Test() 106 | scanner = self._makeOne(test=test) 107 | scanner.scan(packageinzip) 108 | 109 | def test_package_with_orphaned_pyc_file(self): 110 | # There is a module2.pyc file in the "pycfixtures" package; it 111 | # has no corresponding .py source file. Such orphaned .pyc 112 | # files should be ignored during scanning. 113 | from tests.fixtures import pyc 114 | 115 | test = _Test() 116 | scanner = self._makeOne(test=test) 117 | scanner.scan(pyc) 118 | self.assertEqual(len(test.registrations), 4) 119 | test.registrations.sort(key=lambda x: (x["name"], x["ob"].__module__)) 120 | from tests.fixtures.pyc import subpackage 121 | from tests.fixtures.pyc.module import Class as Class1 122 | from tests.fixtures.pyc.module import function as func1 123 | from tests.fixtures.pyc.module import inst as inst1 124 | 125 | self.assertEqual(test.registrations[0]["name"], "Class") 126 | self.assertEqual(test.registrations[0]["ob"], Class1) 127 | self.assertEqual(test.registrations[0]["method"], True) 128 | 129 | self.assertEqual(test.registrations[1]["name"], "function") 130 | self.assertEqual(test.registrations[1]["ob"], func1) 131 | self.assertEqual(test.registrations[1]["function"], True) 132 | 133 | self.assertEqual(test.registrations[2]["name"], "inst") 134 | self.assertEqual(test.registrations[2]["ob"], inst1) 135 | self.assertEqual(test.registrations[2]["instance"], True) 136 | 137 | self.assertEqual(test.registrations[3]["name"], "pkgfunction") 138 | self.assertEqual(test.registrations[3]["ob"], subpackage.pkgfunction) 139 | self.assertEqual(test.registrations[3]["function"], True) 140 | 141 | def test_module(self): 142 | from tests.fixtures.one import module 143 | 144 | test = _Test() 145 | scanner = self._makeOne(test=test) 146 | scanner.scan(module) 147 | self.assertEqual(len(test.registrations), 3) 148 | test.registrations.sort(key=lambda x: (x["name"], x["ob"].__module__)) 149 | from tests.fixtures.one.module import Class as Class1 150 | from tests.fixtures.one.module import function as func1 151 | from tests.fixtures.one.module import inst as inst1 152 | 153 | self.assertEqual(test.registrations[0]["name"], "Class") 154 | self.assertEqual(test.registrations[0]["ob"], Class1) 155 | self.assertEqual(test.registrations[0]["method"], True) 156 | 157 | self.assertEqual(test.registrations[1]["name"], "function") 158 | self.assertEqual(test.registrations[1]["ob"], func1) 159 | self.assertEqual(test.registrations[1]["function"], True) 160 | 161 | self.assertEqual(test.registrations[2]["name"], "inst") 162 | self.assertEqual(test.registrations[2]["ob"], inst1) 163 | self.assertEqual(test.registrations[2]["instance"], True) 164 | 165 | def test_ignore_imported(self): 166 | # even though "twofunction" is imported into "one", it should not 167 | # be registered, because it's only imported in one and not defined 168 | # there 169 | from tests.fixtures.importonly import one, two 170 | 171 | test = _Test() 172 | scanner = self._makeOne(test=test) 173 | scanner.scan(one) 174 | self.assertEqual(len(test.registrations), 1) 175 | scanner.scan(two) 176 | self.assertEqual(len(test.registrations), 2) 177 | 178 | def test_dont_ignore_legit_decorators(self): 179 | # make sure venusian picks up other decorated things from 180 | # imported modules when the whole package is scanned 181 | from tests.fixtures import import_and_scan 182 | 183 | test = _Test() 184 | scanner = self._makeOne(test=test) 185 | scanner.scan(import_and_scan) 186 | self.assertEqual(len(test.registrations), 2) 187 | 188 | def test_one_category(self): 189 | from tests.fixtures import category 190 | 191 | test = _Test() 192 | scanner = self._makeOne(test=test) 193 | scanner.scan(category, categories=("mycategory",)) 194 | self.assertEqual(len(test.registrations), 1) 195 | self.assertEqual(test.registrations[0]["name"], "function") 196 | self.assertEqual(test.registrations[0]["ob"], category.function) 197 | self.assertEqual(test.registrations[0]["function"], True) 198 | 199 | def test_all_categories_implicit(self): 200 | from tests.fixtures import category 201 | 202 | test = _Test() 203 | scanner = self._makeOne(test=test) 204 | scanner.scan(category) 205 | self.assertEqual(len(test.registrations), 2) 206 | self.assertEqual(test.registrations[0]["name"], "function") 207 | self.assertEqual(test.registrations[0]["ob"], category.function) 208 | self.assertEqual(test.registrations[0]["function"], True) 209 | self.assertEqual(test.registrations[1]["name"], "function2") 210 | self.assertEqual(test.registrations[1]["ob"], category.function2) 211 | self.assertEqual(test.registrations[1]["function"], True) 212 | 213 | def test_all_categories_explicit(self): 214 | from tests.fixtures import category 215 | 216 | test = _Test() 217 | scanner = self._makeOne(test=test) 218 | scanner.scan(category, categories=("mycategory", "mycategory2")) 219 | self.assertEqual(len(test.registrations), 2) 220 | self.assertEqual(test.registrations[0]["name"], "function") 221 | self.assertEqual(test.registrations[0]["ob"], category.function) 222 | self.assertEqual(test.registrations[0]["function"], True) 223 | self.assertEqual(test.registrations[1]["name"], "function2") 224 | self.assertEqual(test.registrations[1]["ob"], category.function2) 225 | self.assertEqual(test.registrations[1]["function"], True) 226 | 227 | def test_decorations_arent_inherited(self): 228 | from tests.fixtures import inheritance 229 | 230 | test = _Test() 231 | scanner = self._makeOne(test=test) 232 | scanner.scan(inheritance) 233 | self.assertEqual( 234 | test.registrations, 235 | [ 236 | dict(name="Parent", ob=inheritance.Parent), 237 | ], 238 | ) 239 | 240 | def test_classdecorator(self): 241 | from tests.fixtures import classdecorator 242 | 243 | test = _Test() 244 | scanner = self._makeOne(test=test) 245 | scanner.scan(classdecorator) 246 | test.registrations.sort(key=lambda x: (x["name"], x["ob"].__module__)) 247 | self.assertEqual(len(test.registrations), 2) 248 | self.assertEqual(test.registrations[0]["name"], "SubClass") 249 | self.assertEqual(test.registrations[0]["ob"], classdecorator.SubClass) 250 | self.assertEqual(test.registrations[0]["subclass"], True) 251 | self.assertEqual(test.registrations[1]["name"], "SuperClass") 252 | self.assertEqual(test.registrations[1]["ob"], classdecorator.SuperClass) 253 | self.assertEqual(test.registrations[1]["superclass"], True) 254 | 255 | def test_class_and_method_decorator(self): 256 | from tests.fixtures import class_and_method 257 | 258 | test = _Test() 259 | scanner = self._makeOne(test=test) 260 | scanner.scan(class_and_method) 261 | self.assertEqual(len(test.registrations), 2) 262 | self.assertEqual(test.registrations[0]["name"], "ClassWithMethod") 263 | self.assertEqual(test.registrations[0]["ob"], class_and_method.ClassWithMethod) 264 | self.assertEqual(test.registrations[0]["method"], True) 265 | self.assertEqual(test.registrations[1]["name"], "ClassWithMethod") 266 | self.assertEqual(test.registrations[1]["ob"], class_and_method.ClassWithMethod) 267 | self.assertEqual(test.registrations[1]["class_"], True) 268 | 269 | def test_scan_only_finds_classdecoration_once(self): 270 | from tests.fixtures import two 271 | from tests.fixtures.two.mod1 import Class 272 | 273 | test = _Test() 274 | scanner = self._makeOne(test=test) 275 | scanner.scan(two) 276 | self.assertEqual( 277 | test.registrations, 278 | [ 279 | dict(name="Class", ob=Class), 280 | ], 281 | ) 282 | 283 | def test_importerror_during_scan_default_onerror(self): 284 | from tests.fixtures import importerror 285 | 286 | test = _Test() 287 | scanner = self._makeOne(test=test) 288 | # without a custom onerror, scan will propagate the importerror from 289 | # will_cause_import_error 290 | self.assertRaises(ImportError, scanner.scan, importerror) 291 | 292 | def test_importerror_during_scan_default_onerror_with_ignore(self): 293 | from tests.fixtures import importerror 294 | 295 | test = _Test() 296 | scanner = self._makeOne(test=test) 297 | # scan will ignore the errors from will_cause_import_error due 298 | # to us choosing to ignore that package 299 | scanner.scan( 300 | importerror, ignore="tests.fixtures.importerror.will_cause_import_error" 301 | ) 302 | 303 | def test_importerror_during_scan_custom_onerror(self): 304 | from tests.fixtures import importerror 305 | 306 | test = _Test() 307 | scanner = self._makeOne(test=test) 308 | 309 | # with this custom onerror, scan will not propagate the importerror 310 | # from will_raise_importerror 311 | def onerror(name): 312 | if not issubclass(sys.exc_info()[0], ImportError): 313 | raise 314 | 315 | scanner.scan(importerror, onerror=onerror) 316 | self.assertEqual(len(test.registrations), 1) 317 | from tests.fixtures.importerror import function as func1 318 | 319 | self.assertEqual(test.registrations[0]["name"], "function") 320 | self.assertEqual(test.registrations[0]["ob"], func1) 321 | self.assertEqual(test.registrations[0]["function"], True) 322 | 323 | def test_importerror_in_package_during_scan_custom_onerror(self): 324 | from tests.fixtures import importerror_package 325 | 326 | md("tests.fixtures.importerror_package.will_cause_import_error") 327 | test = _Test() 328 | scanner = self._makeOne(test=test) 329 | 330 | # with this custom onerror, scan will not propagate the importerror 331 | # from will_raise_importerror 332 | def onerror(name): 333 | raise ValueError 334 | 335 | self.assertRaises( 336 | ValueError, scanner.scan, importerror_package, onerror=onerror 337 | ) 338 | self.assertEqual(len(test.registrations), 1) 339 | from tests.fixtures.importerror_package import function as func1 340 | 341 | self.assertEqual(test.registrations[0]["name"], "function") 342 | self.assertEqual(test.registrations[0]["ob"], func1) 343 | self.assertEqual(test.registrations[0]["function"], True) 344 | 345 | def test_attrerror_during_scan_custom_onerror(self): 346 | from tests.fixtures import attrerror 347 | 348 | test = _Test() 349 | scanner = self._makeOne(test=test) 350 | 351 | # with this custom onerror, scan will not propagate the importerror 352 | # from will_raise_importerror 353 | def onerror(name): 354 | if not issubclass(sys.exc_info()[0], ImportError): 355 | raise 356 | 357 | self.assertRaises(AttributeError, scanner.scan, attrerror, onerror=onerror) 358 | self.assertEqual(len(test.registrations), 1) 359 | from tests.fixtures.attrerror import function as func1 360 | 361 | self.assertEqual(test.registrations[0]["name"], "function") 362 | self.assertEqual(test.registrations[0]["ob"], func1) 363 | self.assertEqual(test.registrations[0]["function"], True) 364 | 365 | def test_attrerror_in_package_during_scan_custom_onerror(self): 366 | from tests.fixtures import attrerror_package 367 | 368 | md("tests.fixtures.attrerror_package.will_cause_import_error") 369 | test = _Test() 370 | scanner = self._makeOne(test=test) 371 | 372 | # with this custom onerror, scan will not propagate the importerror 373 | # from will_raise_importerror 374 | def onerror(name): 375 | if not issubclass(sys.exc_info()[0], ImportError): 376 | raise 377 | 378 | self.assertRaises( 379 | AttributeError, scanner.scan, attrerror_package, onerror=onerror 380 | ) 381 | self.assertEqual(len(test.registrations), 1) 382 | from tests.fixtures.attrerror_package import function as func1 383 | 384 | self.assertEqual(test.registrations[0]["name"], "function") 385 | self.assertEqual(test.registrations[0]["ob"], func1) 386 | self.assertEqual(test.registrations[0]["function"], True) 387 | 388 | def test_attrerror_in_package_during_scan_no_custom_onerror(self): 389 | from tests.fixtures import attrerror_package 390 | 391 | md("tests.fixtures.attrerror_package.will_cause_import_error") 392 | test = _Test() 393 | scanner = self._makeOne(test=test) 394 | self.assertRaises(AttributeError, scanner.scan, attrerror_package) 395 | self.assertEqual(len(test.registrations), 1) 396 | from tests.fixtures.attrerror_package import function as func1 397 | 398 | self.assertEqual(test.registrations[0]["name"], "function") 399 | self.assertEqual(test.registrations[0]["ob"], func1) 400 | self.assertEqual(test.registrations[0]["function"], True) 401 | 402 | def test_onerror_used_to_swallow_all_exceptions(self): 403 | from tests.fixtures import subpackages 404 | 405 | test = _Test() 406 | scanner = self._makeOne(test=test) 407 | # onerror can also be used to skip errors while scanning submodules 408 | # e.g.: test modules under a given library 409 | swallowed = [] 410 | 411 | def ignore_child(name): 412 | swallowed.append(name) 413 | 414 | scanner.scan(subpackages, onerror=ignore_child) 415 | self.assertEqual( 416 | swallowed, 417 | [ 418 | "tests.fixtures.subpackages.childpackage.will_cause_import_error", 419 | "tests.fixtures.subpackages.mod2", 420 | ], 421 | ) 422 | self.assertEqual(len(test.registrations), 1) 423 | from tests.fixtures.subpackages import function as func1 424 | 425 | self.assertEqual(test.registrations[0]["name"], "function") 426 | self.assertEqual(test.registrations[0]["ob"], func1) 427 | self.assertEqual(test.registrations[0]["function"], True) 428 | 429 | def test_ignore_by_full_dotted_name(self): 430 | from tests.fixtures import one 431 | 432 | test = _Test() 433 | scanner = self._makeOne(test=test) 434 | scanner.scan(one, ignore=["tests.fixtures.one.module2"]) 435 | self.assertEqual(len(test.registrations), 3) 436 | from tests.fixtures.one.module import Class as Class1 437 | from tests.fixtures.one.module import function as func1 438 | from tests.fixtures.one.module import inst as inst1 439 | 440 | self.assertEqual(test.registrations[0]["name"], "Class") 441 | self.assertEqual(test.registrations[0]["ob"], Class1) 442 | self.assertEqual(test.registrations[0]["method"], True) 443 | 444 | self.assertEqual(test.registrations[1]["name"], "function") 445 | self.assertEqual(test.registrations[1]["ob"], func1) 446 | self.assertEqual(test.registrations[1]["function"], True) 447 | 448 | self.assertEqual(test.registrations[2]["name"], "inst") 449 | self.assertEqual(test.registrations[2]["ob"], inst1) 450 | self.assertEqual(test.registrations[2]["instance"], True) 451 | 452 | def test_ignore_by_full_dotted_name2(self): 453 | from tests.fixtures import nested 454 | 455 | test = _Test() 456 | scanner = self._makeOne(test=test) 457 | scanner.scan(nested, ignore=["tests.fixtures.nested.sub1"]) 458 | self.assertEqual(len(test.registrations), 3) 459 | from tests.fixtures.nested import function as func1 460 | from tests.fixtures.nested.sub2 import function as func2 461 | from tests.fixtures.nested.sub2.subsub2 import function as func3 462 | 463 | self.assertEqual(test.registrations[0]["name"], "function") 464 | self.assertEqual(test.registrations[0]["ob"], func1) 465 | self.assertEqual(test.registrations[0]["function"], True) 466 | 467 | self.assertEqual(test.registrations[1]["name"], "function") 468 | self.assertEqual(test.registrations[1]["ob"], func2) 469 | self.assertEqual(test.registrations[1]["function"], True) 470 | 471 | self.assertEqual(test.registrations[2]["name"], "function") 472 | self.assertEqual(test.registrations[2]["ob"], func3) 473 | self.assertEqual(test.registrations[2]["function"], True) 474 | 475 | def test_ignore_by_full_dotted_name3(self): 476 | from tests.fixtures import nested 477 | 478 | test = _Test() 479 | scanner = self._makeOne(test=test) 480 | scanner.scan( 481 | nested, ignore=["tests.fixtures.nested.sub1", "tests.fixtures.nested.sub2"] 482 | ) 483 | self.assertEqual(len(test.registrations), 1) 484 | from tests.fixtures.nested import function as func1 485 | 486 | self.assertEqual(test.registrations[0]["name"], "function") 487 | self.assertEqual(test.registrations[0]["ob"], func1) 488 | self.assertEqual(test.registrations[0]["function"], True) 489 | 490 | def test_ignore_by_full_dotted_name4(self): 491 | from tests.fixtures import nested 492 | 493 | test = _Test() 494 | scanner = self._makeOne(test=test) 495 | scanner.scan( 496 | nested, 497 | ignore=["tests.fixtures.nested.sub1", "tests.fixtures.nested.function"], 498 | ) 499 | self.assertEqual(len(test.registrations), 2) 500 | from tests.fixtures.nested.sub2 import function as func2 501 | from tests.fixtures.nested.sub2.subsub2 import function as func3 502 | 503 | self.assertEqual(test.registrations[0]["name"], "function") 504 | self.assertEqual(test.registrations[0]["ob"], func2) 505 | self.assertEqual(test.registrations[0]["function"], True) 506 | 507 | self.assertEqual(test.registrations[1]["name"], "function") 508 | self.assertEqual(test.registrations[1]["ob"], func3) 509 | self.assertEqual(test.registrations[1]["function"], True) 510 | 511 | def test_ignore_by_relative_dotted_name(self): 512 | from tests.fixtures import one 513 | 514 | test = _Test() 515 | scanner = self._makeOne(test=test) 516 | scanner.scan(one, ignore=[".module2"]) 517 | self.assertEqual(len(test.registrations), 3) 518 | from tests.fixtures.one.module import Class as Class1 519 | from tests.fixtures.one.module import function as func1 520 | from tests.fixtures.one.module import inst as inst1 521 | 522 | self.assertEqual(test.registrations[0]["name"], "Class") 523 | self.assertEqual(test.registrations[0]["ob"], Class1) 524 | self.assertEqual(test.registrations[0]["method"], True) 525 | 526 | self.assertEqual(test.registrations[1]["name"], "function") 527 | self.assertEqual(test.registrations[1]["ob"], func1) 528 | self.assertEqual(test.registrations[1]["function"], True) 529 | 530 | self.assertEqual(test.registrations[2]["name"], "inst") 531 | self.assertEqual(test.registrations[2]["ob"], inst1) 532 | self.assertEqual(test.registrations[2]["instance"], True) 533 | 534 | def test_ignore_by_relative_dotted_name2(self): 535 | from tests.fixtures import nested 536 | 537 | test = _Test() 538 | scanner = self._makeOne(test=test) 539 | scanner.scan(nested, ignore=[".sub1"]) 540 | self.assertEqual(len(test.registrations), 3) 541 | from tests.fixtures.nested import function as func1 542 | from tests.fixtures.nested.sub2 import function as func2 543 | from tests.fixtures.nested.sub2.subsub2 import function as func3 544 | 545 | self.assertEqual(test.registrations[0]["name"], "function") 546 | self.assertEqual(test.registrations[0]["ob"], func1) 547 | self.assertEqual(test.registrations[0]["function"], True) 548 | 549 | self.assertEqual(test.registrations[1]["name"], "function") 550 | self.assertEqual(test.registrations[1]["ob"], func2) 551 | self.assertEqual(test.registrations[1]["function"], True) 552 | 553 | self.assertEqual(test.registrations[2]["name"], "function") 554 | self.assertEqual(test.registrations[2]["ob"], func3) 555 | self.assertEqual(test.registrations[2]["function"], True) 556 | 557 | def test_ignore_by_relative_dotted_name3(self): 558 | from tests.fixtures import nested 559 | 560 | test = _Test() 561 | scanner = self._makeOne(test=test) 562 | scanner.scan(nested, ignore=[".sub1", ".sub2"]) 563 | self.assertEqual(len(test.registrations), 1) 564 | from tests.fixtures.nested import function as func1 565 | 566 | self.assertEqual(test.registrations[0]["name"], "function") 567 | self.assertEqual(test.registrations[0]["ob"], func1) 568 | self.assertEqual(test.registrations[0]["function"], True) 569 | 570 | def test_ignore_by_relative_dotted_name4(self): 571 | from tests.fixtures import nested 572 | 573 | test = _Test() 574 | scanner = self._makeOne(test=test) 575 | scanner.scan(nested, ignore=[".sub1", ".function"]) 576 | self.assertEqual(len(test.registrations), 2) 577 | from tests.fixtures.nested.sub2 import function as func2 578 | from tests.fixtures.nested.sub2.subsub2 import function as func3 579 | 580 | self.assertEqual(test.registrations[0]["name"], "function") 581 | self.assertEqual(test.registrations[0]["ob"], func2) 582 | self.assertEqual(test.registrations[0]["function"], True) 583 | 584 | self.assertEqual(test.registrations[1]["name"], "function") 585 | self.assertEqual(test.registrations[1]["ob"], func3) 586 | self.assertEqual(test.registrations[1]["function"], True) 587 | 588 | def test_ignore_by_function(self): 589 | from tests.fixtures import one 590 | 591 | test = _Test() 592 | scanner = self._makeOne(test=test) 593 | scanner.scan( 594 | one, ignore=[re.compile("Class").search, re.compile("inst").search] 595 | ) 596 | self.assertEqual(len(test.registrations), 2) 597 | from tests.fixtures.one.module import function as func1 598 | from tests.fixtures.one.module2 import function as func2 599 | 600 | self.assertEqual(test.registrations[0]["name"], "function") 601 | self.assertEqual(test.registrations[0]["ob"], func1) 602 | self.assertEqual(test.registrations[0]["function"], True) 603 | 604 | self.assertEqual(test.registrations[1]["name"], "function") 605 | self.assertEqual(test.registrations[1]["ob"], func2) 606 | self.assertEqual(test.registrations[1]["function"], True) 607 | 608 | def test_ignore_by_function_nested(self): 609 | from tests.fixtures import nested 610 | 611 | test = _Test() 612 | scanner = self._makeOne(test=test) 613 | scanner.scan(nested, ignore=[re.compile(".function$").search]) 614 | self.assertEqual(len(test.registrations), 0) 615 | 616 | def test_ignore_by_function_nested2(self): 617 | from tests.fixtures import nested 618 | 619 | test = _Test() 620 | scanner = self._makeOne(test=test) 621 | scanner.scan( 622 | nested, 623 | ignore=[re.compile("sub2$").search, re.compile("nested.function$").search], 624 | ) 625 | self.assertEqual(len(test.registrations), 2) 626 | 627 | from tests.fixtures.nested.sub1 import function as func2 628 | from tests.fixtures.nested.sub1.subsub1 import function as func3 629 | 630 | self.assertEqual(test.registrations[0]["name"], "function") 631 | self.assertEqual(test.registrations[0]["ob"], func2) 632 | self.assertEqual(test.registrations[0]["function"], True) 633 | 634 | self.assertEqual(test.registrations[1]["name"], "function") 635 | self.assertEqual(test.registrations[1]["ob"], func3) 636 | self.assertEqual(test.registrations[1]["function"], True) 637 | 638 | def test_ignore_as_string(self): 639 | from tests.fixtures import one 640 | 641 | test = _Test() 642 | scanner = self._makeOne(test=test) 643 | scanner.scan(one, ignore="tests.fixtures.one.module2") 644 | self.assertEqual(len(test.registrations), 3) 645 | from tests.fixtures.one.module import Class as Class1 646 | from tests.fixtures.one.module import function as func1 647 | from tests.fixtures.one.module import inst as inst1 648 | 649 | self.assertEqual(test.registrations[0]["name"], "Class") 650 | self.assertEqual(test.registrations[0]["ob"], Class1) 651 | self.assertEqual(test.registrations[0]["method"], True) 652 | 653 | self.assertEqual(test.registrations[1]["name"], "function") 654 | self.assertEqual(test.registrations[1]["ob"], func1) 655 | self.assertEqual(test.registrations[1]["function"], True) 656 | 657 | self.assertEqual(test.registrations[2]["name"], "inst") 658 | self.assertEqual(test.registrations[2]["ob"], inst1) 659 | self.assertEqual(test.registrations[2]["instance"], True) 660 | 661 | def test_ignore_mixed_string_and_func(self): 662 | import re 663 | 664 | from tests.fixtures import one 665 | 666 | test = _Test() 667 | scanner = self._makeOne(test=test) 668 | scanner.scan( 669 | one, ignore=["tests.fixtures.one.module2", re.compile("inst").search] 670 | ) 671 | self.assertEqual(len(test.registrations), 2) 672 | from tests.fixtures.one.module import Class as Class1 673 | from tests.fixtures.one.module import function as func1 674 | 675 | self.assertEqual(test.registrations[0]["name"], "Class") 676 | self.assertEqual(test.registrations[0]["ob"], Class1) 677 | self.assertEqual(test.registrations[0]["method"], True) 678 | 679 | self.assertEqual(test.registrations[1]["name"], "function") 680 | self.assertEqual(test.registrations[1]["ob"], func1) 681 | self.assertEqual(test.registrations[1]["function"], True) 682 | 683 | def test_ignore_mixed_string_abs_rel_and_func(self): 684 | import re 685 | 686 | from tests.fixtures import one 687 | 688 | test = _Test() 689 | scanner = self._makeOne(test=test) 690 | scanner.scan( 691 | one, 692 | ignore=["tests.fixtures.one.module2", ".module", re.compile("inst").search], 693 | ) 694 | self.assertEqual(len(test.registrations), 0) 695 | 696 | def test_lifting1(self): 697 | from tests.fixtures import lifting1 698 | 699 | test = _Test() 700 | scanner = self._makeOne(test=test) 701 | scanner.scan(lifting1) 702 | test.registrations.sort( 703 | key=lambda x: (x["name"], x["attr"], x["ob"].__module__) 704 | ) 705 | self.assertEqual(len(test.registrations), 11) 706 | 707 | self.assertEqual(test.registrations[0]["attr"], "boo") 708 | self.assertEqual(test.registrations[0]["name"], "Sub") 709 | self.assertEqual(test.registrations[0]["ob"], lifting1.Sub) 710 | 711 | self.assertEqual(test.registrations[1]["attr"], "classname") 712 | self.assertEqual(test.registrations[1]["name"], "Sub") 713 | self.assertEqual(test.registrations[1]["ob"], lifting1.Sub) 714 | 715 | self.assertEqual(test.registrations[2]["attr"], "hiss") 716 | self.assertEqual(test.registrations[2]["name"], "Sub") 717 | self.assertEqual(test.registrations[2]["ob"], lifting1.Sub) 718 | 719 | self.assertEqual(test.registrations[3]["attr"], "jump") 720 | self.assertEqual(test.registrations[3]["name"], "Sub") 721 | self.assertEqual(test.registrations[3]["ob"], lifting1.Sub) 722 | 723 | self.assertEqual(test.registrations[4]["attr"], "ram") 724 | self.assertEqual(test.registrations[4]["name"], "Sub") 725 | self.assertEqual(test.registrations[4]["ob"], lifting1.Sub) 726 | 727 | self.assertEqual(test.registrations[5]["attr"], "smack") 728 | self.assertEqual(test.registrations[5]["name"], "Sub") 729 | self.assertEqual(test.registrations[5]["ob"], lifting1.Sub) 730 | 731 | self.assertEqual(test.registrations[6]["attr"], "boo") 732 | self.assertEqual(test.registrations[6]["name"], "Super1") 733 | self.assertEqual(test.registrations[6]["ob"], lifting1.Super1) 734 | 735 | self.assertEqual(test.registrations[7]["attr"], "classname") 736 | self.assertEqual(test.registrations[7]["name"], "Super1") 737 | self.assertEqual(test.registrations[7]["ob"], lifting1.Super1) 738 | 739 | self.assertEqual(test.registrations[8]["attr"], "ram") 740 | self.assertEqual(test.registrations[8]["name"], "Super1") 741 | self.assertEqual(test.registrations[8]["ob"], lifting1.Super1) 742 | 743 | self.assertEqual(test.registrations[9]["attr"], "hiss") 744 | self.assertEqual(test.registrations[9]["name"], "Super2") 745 | self.assertEqual(test.registrations[9]["ob"], lifting1.Super2) 746 | 747 | self.assertEqual(test.registrations[10]["attr"], "jump") 748 | self.assertEqual(test.registrations[10]["name"], "Super2") 749 | self.assertEqual(test.registrations[10]["ob"], lifting1.Super2) 750 | 751 | def test_lifting2(self): 752 | from tests.fixtures import lifting2 753 | 754 | test = _Test() 755 | scanner = self._makeOne(test=test) 756 | scanner.scan(lifting2) 757 | test.registrations.sort( 758 | key=lambda x: (x["name"], x["attr"], x["ob"].__module__) 759 | ) 760 | self.assertEqual(len(test.registrations), 6) 761 | 762 | self.assertEqual(test.registrations[0]["attr"], "boo") 763 | self.assertEqual(test.registrations[0]["name"], "Sub") 764 | self.assertEqual(test.registrations[0]["ob"], lifting2.Sub) 765 | 766 | self.assertEqual(test.registrations[1]["attr"], "classname") 767 | self.assertEqual(test.registrations[1]["name"], "Sub") 768 | self.assertEqual(test.registrations[1]["ob"], lifting2.Sub) 769 | 770 | self.assertEqual(test.registrations[2]["attr"], "hiss") 771 | self.assertEqual(test.registrations[2]["name"], "Sub") 772 | self.assertEqual(test.registrations[2]["ob"], lifting2.Sub) 773 | 774 | self.assertEqual(test.registrations[3]["attr"], "jump") 775 | self.assertEqual(test.registrations[3]["name"], "Sub") 776 | self.assertEqual(test.registrations[3]["ob"], lifting2.Sub) 777 | 778 | self.assertEqual(test.registrations[4]["attr"], "ram") 779 | self.assertEqual(test.registrations[4]["name"], "Sub") 780 | self.assertEqual(test.registrations[4]["ob"], lifting2.Sub) 781 | 782 | self.assertEqual(test.registrations[5]["attr"], "smack") 783 | self.assertEqual(test.registrations[5]["name"], "Sub") 784 | self.assertEqual(test.registrations[5]["ob"], lifting2.Sub) 785 | 786 | def test_lifting3(self): 787 | from tests.fixtures import lifting3 788 | 789 | test = _Test() 790 | scanner = self._makeOne(test=test) 791 | scanner.scan(lifting3) 792 | test.registrations.sort( 793 | key=lambda x: (x["name"], x["attr"], x["ob"].__module__) 794 | ) 795 | self.assertEqual(len(test.registrations), 8) 796 | 797 | self.assertEqual(test.registrations[0]["attr"], "boo") 798 | self.assertEqual(test.registrations[0]["name"], "Sub") 799 | self.assertEqual(test.registrations[0]["ob"], lifting3.Sub) 800 | 801 | self.assertEqual(test.registrations[1]["attr"], "classname") 802 | self.assertEqual(test.registrations[1]["name"], "Sub") 803 | self.assertEqual(test.registrations[1]["ob"], lifting3.Sub) 804 | 805 | self.assertEqual(test.registrations[2]["attr"], "hiss") 806 | self.assertEqual(test.registrations[2]["name"], "Sub") 807 | self.assertEqual(test.registrations[2]["ob"], lifting3.Sub) 808 | 809 | self.assertEqual(test.registrations[3]["attr"], "jump") 810 | self.assertEqual(test.registrations[3]["name"], "Sub") 811 | self.assertEqual(test.registrations[3]["ob"], lifting3.Sub) 812 | 813 | self.assertEqual(test.registrations[4]["attr"], "ram") 814 | self.assertEqual(test.registrations[4]["name"], "Sub") 815 | self.assertEqual(test.registrations[4]["ob"], lifting3.Sub) 816 | 817 | self.assertEqual(test.registrations[5]["attr"], "smack") 818 | self.assertEqual(test.registrations[5]["name"], "Sub") 819 | self.assertEqual(test.registrations[5]["ob"], lifting3.Sub) 820 | 821 | self.assertEqual(test.registrations[6]["attr"], "hiss") 822 | self.assertEqual(test.registrations[6]["name"], "Super2") 823 | self.assertEqual(test.registrations[6]["ob"], lifting3.Super2) 824 | 825 | self.assertEqual(test.registrations[7]["attr"], "jump") 826 | self.assertEqual(test.registrations[7]["name"], "Super2") 827 | self.assertEqual(test.registrations[7]["ob"], lifting3.Super2) 828 | 829 | def test_lifting4(self): 830 | from tests.fixtures import lifting4 831 | 832 | test = _Test() 833 | scanner = self._makeOne(test=test) 834 | scanner.scan(lifting4) 835 | test.registrations.sort( 836 | key=lambda x: (x["name"], x["attr"], x["ob"].__module__) 837 | ) 838 | self.assertEqual(len(test.registrations), 2) 839 | 840 | self.assertEqual(test.registrations[0]["attr"], "hiss") 841 | self.assertEqual(test.registrations[0]["name"], "Sub") 842 | self.assertEqual(test.registrations[0]["ob"], lifting4.Sub) 843 | 844 | self.assertEqual(test.registrations[1]["attr"], "smack") 845 | self.assertEqual(test.registrations[1]["name"], "Sub") 846 | self.assertEqual(test.registrations[1]["ob"], lifting4.Sub) 847 | 848 | def test_lifting5(self): 849 | from tests.fixtures import lifting5 850 | 851 | test = _Test() 852 | scanner = self._makeOne(test=test) 853 | scanner.scan(lifting5) 854 | test.registrations.sort( 855 | key=lambda x: (x["name"], x["attr"], x["ob"].__module__) 856 | ) 857 | self.assertEqual(len(test.registrations), 15) 858 | 859 | self.assertEqual(test.registrations[0]["attr"], "boo") 860 | self.assertEqual(test.registrations[0]["name"], "Sub") 861 | self.assertEqual(test.registrations[0]["ob"], lifting5.Sub) 862 | 863 | self.assertEqual(test.registrations[1]["attr"], "classname") 864 | self.assertEqual(test.registrations[1]["name"], "Sub") 865 | self.assertEqual(test.registrations[1]["ob"], lifting5.Sub) 866 | 867 | self.assertEqual(test.registrations[2]["attr"], "hiss") 868 | self.assertEqual(test.registrations[2]["name"], "Sub") 869 | self.assertEqual(test.registrations[2]["ob"], lifting5.Sub) 870 | 871 | self.assertEqual(test.registrations[3]["attr"], "jump") 872 | self.assertEqual(test.registrations[3]["name"], "Sub") 873 | self.assertEqual(test.registrations[3]["ob"], lifting5.Sub) 874 | 875 | self.assertEqual(test.registrations[4]["attr"], "ram") 876 | self.assertEqual(test.registrations[4]["name"], "Sub") 877 | self.assertEqual(test.registrations[4]["ob"], lifting5.Sub) 878 | 879 | self.assertEqual(test.registrations[5]["attr"], "smack") 880 | self.assertEqual(test.registrations[5]["name"], "Sub") 881 | self.assertEqual(test.registrations[5]["ob"], lifting5.Sub) 882 | 883 | self.assertEqual(test.registrations[6]["attr"], "boo") 884 | self.assertEqual(test.registrations[6]["name"], "Super1") 885 | self.assertEqual(test.registrations[6]["ob"], lifting5.Super1) 886 | 887 | self.assertEqual(test.registrations[7]["attr"], "classname") 888 | self.assertEqual(test.registrations[7]["name"], "Super1") 889 | self.assertEqual(test.registrations[7]["ob"], lifting5.Super1) 890 | 891 | self.assertEqual(test.registrations[8]["attr"], "jump") 892 | self.assertEqual(test.registrations[8]["name"], "Super1") 893 | self.assertEqual(test.registrations[8]["ob"], lifting5.Super1) 894 | 895 | self.assertEqual(test.registrations[9]["attr"], "ram") 896 | self.assertEqual(test.registrations[9]["name"], "Super1") 897 | self.assertEqual(test.registrations[9]["ob"], lifting5.Super1) 898 | 899 | self.assertEqual(test.registrations[10]["attr"], "boo") 900 | self.assertEqual(test.registrations[10]["name"], "Super2") 901 | self.assertEqual(test.registrations[10]["ob"], lifting5.Super2) 902 | 903 | self.assertEqual(test.registrations[11]["attr"], "classname") 904 | self.assertEqual(test.registrations[11]["name"], "Super2") 905 | self.assertEqual(test.registrations[11]["ob"], lifting5.Super2) 906 | 907 | self.assertEqual(test.registrations[12]["attr"], "hiss") 908 | self.assertEqual(test.registrations[12]["name"], "Super2") 909 | self.assertEqual(test.registrations[12]["ob"], lifting5.Super2) 910 | 911 | self.assertEqual(test.registrations[13]["attr"], "jump") 912 | self.assertEqual(test.registrations[13]["name"], "Super2") 913 | self.assertEqual(test.registrations[13]["ob"], lifting5.Super2) 914 | 915 | self.assertEqual(test.registrations[14]["attr"], "ram") 916 | self.assertEqual(test.registrations[14]["name"], "Super2") 917 | self.assertEqual(test.registrations[14]["ob"], lifting5.Super2) 918 | 919 | def test_subclassing(self): 920 | from tests.fixtures import subclassing 921 | 922 | test = _Test() 923 | scanner = self._makeOne(test=test) 924 | scanner.scan(subclassing) 925 | test.registrations.sort( 926 | key=lambda x: (x["name"], x["attr"], x["ob"].__module__) 927 | ) 928 | self.assertEqual(len(test.registrations), 2) 929 | 930 | self.assertEqual(test.registrations[0]["attr"], "boo") 931 | self.assertEqual(test.registrations[0]["name"], "Super") 932 | self.assertEqual(test.registrations[0]["ob"], subclassing.Super) 933 | 934 | self.assertEqual(test.registrations[1]["attr"], "classname") 935 | self.assertEqual(test.registrations[1]["name"], "Super") 936 | self.assertEqual(test.registrations[1]["ob"], subclassing.Super) 937 | 938 | 939 | class Test_lift(unittest.TestCase): 940 | def _makeOne(self, categories=None): 941 | from venusian import lift 942 | 943 | return lift(categories) 944 | 945 | def test_not_class(self): 946 | inst = self._makeOne() 947 | self.assertRaises(RuntimeError, inst, None) 948 | 949 | 950 | class Test_onlyliftedfrom(unittest.TestCase): 951 | def _makeOne(self): 952 | from venusian import onlyliftedfrom 953 | 954 | return onlyliftedfrom() 955 | 956 | def test_not_class(self): 957 | inst = self._makeOne() 958 | self.assertRaises(RuntimeError, inst, None) 959 | 960 | 961 | def md(name): # pragma: no cover 962 | if name in sys.modules: 963 | del sys.modules[name] 964 | --------------------------------------------------------------------------------