Skip to content

Commit b8e557c

Browse files
onfirstaccess setattr test: use a tmpdir-independ package name
0 parents  commit b8e557c

File tree

15 files changed

+734
-0
lines changed

15 files changed

+734
-0
lines changed

.hgignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
# These lines are suggested according to the svn:ignore property
3+
# Feel free to enable them by uncommenting them
4+
syntax:glob
5+
*.pyc
6+
*.pyo
7+
*.swp
8+
*.html
9+
*.class
10+
11+
.tox
12+
13+
build
14+
dist
15+
apipkg.egg-info
16+

CHANGELOG

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
1.1
2+
----------------------------------------
3+
4+
- copy __doc__ and introduce a new argument to initpkg()
5+
(thanks Ralf Schmitt)
6+
7+
- don't use a "0" default for __version__
8+
9+
1.0
10+
----------------------------------------
11+
12+
- make apipkg memorize the absolute path where a package starts
13+
importing so that subsequent chdir + imports won't break.
14+
15+
- allow to alias modules
16+
17+
- allow to use dotted names in alias specifications (thanks Ralf
18+
Schmitt).
19+
20+
1.0.0b6
21+
----------------------------------------
22+
23+
- fix recursive import issue resulting in a superflous KeyError
24+
- default to __version__ '0' and not set __loader__ or __path__ at all if it
25+
doesn't exist on the underlying init module
26+
27+
1.0.0b5
28+
----------------------------------------
29+
30+
- fixed MANIFEST.in
31+
- also transfer __loader__ attribute (thanks Ralf Schmitt)
32+
- compat fix for BPython
33+
34+
1.0.0b3 (compared to 1.0.0b2)
35+
------------------------------------
36+
37+
- added special __onfirstaccess__ attribute whose value will
38+
be called on the first attribute access of an apimodule.
39+

MANIFEST.in

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
include CHANGELOG
2+
include README.txt
3+
include setup.py
4+
include conftest.py
5+
include test_apipkg.py
6+
prune .svn
7+
prune .hg

README.txt

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
Welcome to apipkg!
2+
------------------------
3+
4+
With apipkg you can control the exported namespace of a
5+
python package and greatly reduce the number of imports for your users.
6+
It is a `small pure python module`_ that works on virtually all Python
7+
versions, including CPython2.3 to Python3.1, Jython and PyPy. It co-operates
8+
well with Python's ``help()`` system, custom importers (PEP302) and common
9+
command line completion tools.
10+
11+
Usage is very simple: you can require 'apipkg' as a dependency or you
12+
can copy paste the <100 Lines of code into your project.
13+
14+
Tutorial example
15+
-------------------
16+
17+
Here is a simple ``mypkg`` package that specifies one namespace
18+
and exports two objects imported from different modules::
19+
20+
# mypkg/__init__.py
21+
import apipkg
22+
apipkg.initpkg(__name__, {
23+
'path': {
24+
'Class1': "_mypkg.somemodule:Class1",
25+
'clsattr': "_mypkg.othermodule:Class2.attr",
26+
}
27+
}
28+
29+
The package is initialized with a dictionary as namespace.
30+
31+
You need to create a ``_mypkg`` package with a ``somemodule.py``
32+
and ``othermodule.py`` containing the respective classes.
33+
The ``_mypkg`` is not special - it's a completely
34+
regular python package.
35+
36+
Namespace dictionaries contain ``name: value`` mappings
37+
where the value may be another namespace dictionary or
38+
a string specifying an import location. On accessing
39+
an namespace attribute an import will be performed::
40+
41+
>>> import mypkg
42+
>>> mypkg.path
43+
<ApiModule 'mypkg.path'>
44+
>>> mypkg.path.Class1 # '_mypkg.somemodule' gets imported now
45+
<class _mypkg.somemodule.Class1 at 0xb7d428fc>
46+
>>> mypkg.path.clsattr # '_mypkg.othermodule' gets imported now
47+
4 # the value of _mypkg.othermodule.Class2.attr
48+
49+
The ``mypkg.path`` namespace and its two entries are
50+
loaded when they are accessed. This means:
51+
52+
* lazy loading - only what is actually needed is ever loaded
53+
54+
* only the root "mypkg" ever needs to be imported to get
55+
access to the complete functionality.
56+
57+
* the underlying modules are also accessible, for example::
58+
59+
from mypkg.sub import Class1
60+
61+
62+
Including apipkg in your package
63+
--------------------------------------
64+
65+
If you don't want to add an ``apipkg`` dependency to your package you
66+
can copy the `apipkg.py`_ file somewhere to your own package,
67+
for example ``_mypkg/apipkg.py`` in the above example. You
68+
then import the ``initpkg`` function from that new place and
69+
are good to go.
70+
71+
.. _`small pure python module`:
72+
.. _`apipkg.py`: http://bitbucket.org/hpk42/apipkg/src/tip/apipkg.py
73+
74+
Feedback?
75+
-----------------------
76+
77+
If you have questions you are welcome to
78+
79+
* join the #pylib channel on irc.freenode.net
80+
* subscribe to the http://codespeak.net/mailman/listinfo/py-dev list.
81+
* create an issue on http://bitbucket.org/hpk42/apipkg/issues
82+
83+
have fun,
84+
holger krekel

apipkg.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""
2+
apipkg: control the exported namespace of a python package.
3+
4+
see http://pypi.python.org/pypi/apipkg
5+
6+
(c) holger krekel, 2009 - MIT license
7+
"""
8+
import os
9+
import sys
10+
from types import ModuleType
11+
12+
__version__ = "1.1"
13+
14+
def initpkg(pkgname, exportdefs, attr=dict()):
15+
""" initialize given package from the export definitions. """
16+
oldmod = sys.modules[pkgname]
17+
d = {}
18+
f = getattr(oldmod, '__file__', None)
19+
if f:
20+
f = os.path.abspath(f)
21+
d['__file__'] = f
22+
if hasattr(oldmod, '__version__'):
23+
d['__version__'] = oldmod.__version__
24+
if hasattr(oldmod, '__loader__'):
25+
d['__loader__'] = oldmod.__loader__
26+
if hasattr(oldmod, '__path__'):
27+
d['__path__'] = [os.path.abspath(p) for p in oldmod.__path__]
28+
if hasattr(oldmod, '__doc__'):
29+
d['__doc__'] = oldmod.__doc__
30+
d.update(attr)
31+
oldmod.__dict__.update(d)
32+
mod = ApiModule(pkgname, exportdefs, implprefix=pkgname, attr=d)
33+
sys.modules[pkgname] = mod
34+
35+
def importobj(modpath, attrname):
36+
module = __import__(modpath, None, None, ['__doc__'])
37+
if not attrname:
38+
return module
39+
40+
retval = module
41+
names = attrname.split(".")
42+
for x in names:
43+
retval = getattr(retval, x)
44+
return retval
45+
46+
class ApiModule(ModuleType):
47+
def __init__(self, name, importspec, implprefix=None, attr=None):
48+
self.__name__ = name
49+
self.__all__ = [x for x in importspec if x != '__onfirstaccess__']
50+
self.__map__ = {}
51+
self.__implprefix__ = implprefix or name
52+
if attr:
53+
for name, val in attr.items():
54+
#print "setting", self.__name__, name, val
55+
setattr(self, name, val)
56+
for name, importspec in importspec.items():
57+
if isinstance(importspec, dict):
58+
subname = '%s.%s'%(self.__name__, name)
59+
apimod = ApiModule(subname, importspec, implprefix)
60+
sys.modules[subname] = apimod
61+
setattr(self, name, apimod)
62+
else:
63+
parts = importspec.split(':')
64+
modpath = parts.pop(0)
65+
attrname = parts and parts[0] or ""
66+
if modpath[0] == '.':
67+
modpath = implprefix + modpath
68+
if name == '__doc__':
69+
self.__doc__ = importobj(modpath, attrname)
70+
else:
71+
self.__map__[name] = (modpath, attrname)
72+
73+
def __repr__(self):
74+
l = []
75+
if hasattr(self, '__version__'):
76+
l.append("version=" + repr(self.__version__))
77+
if hasattr(self, '__file__'):
78+
l.append('from ' + repr(self.__file__))
79+
if l:
80+
return '<ApiModule %r %s>' % (self.__name__, " ".join(l))
81+
return '<ApiModule %r>' % (self.__name__,)
82+
83+
def __makeattr(self, name):
84+
"""lazily compute value for name or raise AttributeError if unknown."""
85+
#print "makeattr", self.__name__, name
86+
target = None
87+
if '__onfirstaccess__' in self.__map__:
88+
target = self.__map__.pop('__onfirstaccess__')
89+
importobj(*target)()
90+
try:
91+
modpath, attrname = self.__map__[name]
92+
except KeyError:
93+
if target is not None and name != '__onfirstaccess__':
94+
# retry, onfirstaccess might have set attrs
95+
return getattr(self, name)
96+
raise AttributeError(name)
97+
else:
98+
result = importobj(modpath, attrname)
99+
setattr(self, name, result)
100+
try:
101+
del self.__map__[name]
102+
except KeyError:
103+
pass # in a recursive-import situation a double-del can happen
104+
return result
105+
106+
__getattr__ = __makeattr
107+
108+
def __dict__(self):
109+
# force all the content of the module to be loaded when __dict__ is read
110+
dictdescr = ModuleType.__dict__['__dict__']
111+
dict = dictdescr.__get__(self)
112+
if dict is not None:
113+
hasattr(self, 'some')
114+
for name in self.__all__:
115+
try:
116+
self.__makeattr(name)
117+
except AttributeError:
118+
pass
119+
return dict
120+
__dict__ = property(__dict__)

conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
def pytest_generate_tests(metafunc):
3+
multi = getattr(metafunc.function, 'multi', None)
4+
if multi is None:
5+
return
6+
assert len(multi.kwargs) == 1
7+
for name, l in multi.kwargs.items():
8+
for val in l:
9+
metafunc.addcall(funcargs={name: val})

example/_mypkg/__init__.py

Whitespace-only changes.

example/_mypkg/othermodule.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
class OtherClass:
3+
pass

example/_mypkg/somemodule.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
from _mypkg.othermodule import OtherClass
3+
4+
class SomeClass:
5+
pass

example/mypkg/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# mypkg/__init__.py
2+
3+
import apipkg
4+
apipkg.initpkg(__name__, {
5+
'SomeClass': '_mypkg.somemodule:SomeClass',
6+
'sub': {
7+
'OtherClass': '_mypkg.somemodule:OtherClass',
8+
}
9+
})

0 commit comments

Comments
 (0)